From 79ab46232f86f7effee55614fb6cb7576e43c592 Mon Sep 17 00:00:00 2001 From: TC Date: Sun, 19 Nov 2023 17:23:17 +0000 Subject: [PATCH 001/270] Update .gitlab-ci.yml file --- .gitlab-ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 36ac2e6..a151886 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,17 +22,18 @@ build: - git clone https://gitlab.com/veilid/veilid.git ../veilid - curl –proto ‘=https’ –tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - source "$HOME/.cargo/env" - - brew install capnp cmake wabt llvm protobuf openjdk@17 jq + - brew install capnp cmake wabt llvm protobuf openjdk@17 jq cocoapods - cargo install wasm-bindgen-cli wasm-pack cargo-edit - - sudo gem install cocoapods - wget https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_3.13.5-stable.zip - unzip flutter_macos_arm64_3.13.5-stable.zip && export PATH="$PATH:`pwd`/flutter/bin" - #- yes | flutter doctor --android-licenses + - flutter update + - yes | flutter doctor --android-licenses - flutter config --enable-macos-desktop --enable-ios - flutter config --no-analytics - dart --disable-analytics - flutter doctor -v - flutter build ipa + - flutter build appbundle when: manual #test: From c8a36a6ccfa081c5b36c78cde62cb3efe8dee0a7 Mon Sep 17 00:00:00 2001 From: TC Date: Sun, 19 Nov 2023 18:10:44 +0000 Subject: [PATCH 002/270] Update .gitlab-ci.yml file --- .gitlab-ci.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a151886..948e3e9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,20 +20,20 @@ build: - echo "place holder for build" - sudo softwareupdate --install-rosetta --agree-to-license - git clone https://gitlab.com/veilid/veilid.git ../veilid - - curl –proto ‘=https’ –tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - - source "$HOME/.cargo/env" - - brew install capnp cmake wabt llvm protobuf openjdk@17 jq cocoapods - - cargo install wasm-bindgen-cli wasm-pack cargo-edit + #- curl –proto ‘=https’ –tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + #- source "$HOME/.cargo/env" + #- brew install capnp cmake wabt llvm protobuf openjdk@17 jq cocoapods + #- cargo install wasm-bindgen-cli wasm-pack cargo-edit - wget https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_3.13.5-stable.zip - unzip flutter_macos_arm64_3.13.5-stable.zip && export PATH="$PATH:`pwd`/flutter/bin" - - flutter update + - flutter upgrade - yes | flutter doctor --android-licenses - flutter config --enable-macos-desktop --enable-ios - flutter config --no-analytics - dart --disable-analytics - flutter doctor -v - - flutter build ipa - - flutter build appbundle + #- flutter build ipa + #- flutter build appbundle when: manual #test: From 080e411643d424536f3a595708c9ae3de25ac625 Mon Sep 17 00:00:00 2001 From: Kyle Hamilton Date: Wed, 22 Nov 2023 09:29:22 -0500 Subject: [PATCH 003/270] Fixing non-release build signingConfig --- android/app/build.gradle | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 11a434c..c2eb2d7 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -25,10 +25,13 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" +def buildConfig = 'debug' + def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if (keystorePropertiesFile.exists()) { keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) + buildConfig = 'release' } android { @@ -70,9 +73,13 @@ android { buildTypes { release { - shrinkResources false - minifyEnabled false - signingConfig signingConfigs.release + shrinkResources false + minifyEnabled false + if (buildConfig == 'release') { + signingConfig signingConfigs.release + } else { + signingConfig signingConfigs.debug + } } } From c749025e9046a332f46febf5c92852a71ef3c6e0 Mon Sep 17 00:00:00 2001 From: Caleb Jasik Date: Mon, 27 Nov 2023 15:39:21 +0000 Subject: [PATCH 004/270] Fix link to veilid chat in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5437152..82631b7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ VeilidChat is a chat application written for the Veilid (https://www.veilid.com) distributed application platform. It has a familiar and simple interface and is designed for private, and secure person-to-person communications. -For more information about VeilidChat: https://veilid.chat +For more information about VeilidChat: https://veilid.com/chat/ For more information about the Veilid network protocol and app development platform: https://veilid.com From d3ecae011323b5dc89a867fea3df87657f53774e Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 18 Dec 2023 20:05:23 -0500 Subject: [PATCH 005/270] 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 006/270] 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 007/270] 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 008/270] 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 009/270] 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 010/270] 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 011/270] 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 012/270] 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 013/270] 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 014/270] 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 015/270] 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 016/270] 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 017/270] 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 018/270] 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 019/270] 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 020/270] 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 021/270] 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 022/270] 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 023/270] 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 024/270] 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 025/270] 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 026/270] 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 027/270] 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 028/270] 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 029/270] 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 030/270] 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 031/270] 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 032/270] 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 033/270] 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 034/270] 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 035/270] 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 036/270] 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 037/270] 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 038/270] 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 039/270] 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 040/270] 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 041/270] 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 042/270] 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 043/270] 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 044/270] 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 045/270] 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 046/270] 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 047/270] 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 048/270] 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 049/270] 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 050/270] 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 051/270] 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 052/270] 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 053/270] 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 054/270] 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 055/270] 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 056/270] 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 057/270] 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 058/270] 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 059/270] 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 060/270] 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 061/270] 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 062/270] 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 063/270] 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 064/270] 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 065/270] 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 066/270] 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 067/270] 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 068/270] 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 069/270] 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 070/270] 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 071/270] 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 072/270] 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 From 858a42a63c4926d4bf8b0b25963fcd8533325160 Mon Sep 17 00:00:00 2001 From: Paul Sajna Date: Sat, 17 Feb 2024 18:47:08 -0800 Subject: [PATCH 073/270] flatpak build definition --- .gitignore | 7 +- flatpak/README.md | 84 +++++++++++++++++++++ flatpak/build-flatpak.sh | 41 ++++++++++ flatpak/com.veilid.veilidchat.desktop | 9 +++ flatpak/com.veilid.veilidchat.metainfo.xml | 35 +++++++++ flatpak/com.veilid.veilidchat.png | Bin 0 -> 25795 bytes flatpak/com.veilid.veilidchat.yml | 37 +++++++++ 7 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 flatpak/README.md create mode 100755 flatpak/build-flatpak.sh create mode 100755 flatpak/com.veilid.veilidchat.desktop create mode 100644 flatpak/com.veilid.veilidchat.metainfo.xml create mode 100644 flatpak/com.veilid.veilidchat.png create mode 100644 flatpak/com.veilid.veilidchat.yml diff --git a/.gitignore b/.gitignore index dc159e4..46dd51f 100644 --- a/.gitignore +++ b/.gitignore @@ -51,8 +51,13 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release +/android/key.properties # WASM /web/wasm/ -android/key.properties \ No newline at end of file +# Flatpak +flatpak/build-dir/ +flatpak/repo/ +flatpak/.flatpak-builder/ +*.flatpak diff --git a/flatpak/README.md b/flatpak/README.md new file mode 100644 index 0000000..f85d0fe --- /dev/null +++ b/flatpak/README.md @@ -0,0 +1,84 @@ +- [Building the Flatpak](#building-the-flatpak) + - [Prerequisites](#prereq) + - [Build](#build) + - [Create Flatpak repo of the app](#create-flatpak-repo-of-the-app) + - [Publish to app store](#publish-to-app-store) + - [Bundle the Flatpak repo into an installable `.flatpak` file](#bundle-the-flatpak-repo-into-an-installable-flatpak-file) + - [We now have a `.flatpak` file that we can install on any machine with](#we-now-have-a-flatpak-file-that-we-can-install-on-any-machine-with) + - [We can see that it is installed:](#we-can-see-that-it-is-installed) + +# Prerequisites +`flatpak install -y org.gnome.Platform/x86_64/45` +`flatpak install -y org.gnome.Sdk/x86_64/45` + +# Building the Flatpak + +We imagine this is a separate git repo containing the information specifically +for building the flatpak, as that is how an app is built for FlatHub. + +Important configuration files are as follows: + +- `com.veilid.veilidchat.yml` -- Flatpak manifest, contains the Flatpak + configuration and information on where to get the build files +- `build-flatpak.sh` -- Shell script that will be called by the manifest to assemble the flatpak + + +## Build + +**This should be built on an older version on Linux so that it will run on the +widest possible set of Linux installations. Recommend docker or a CI pipeline +like GitHub actions using the oldest supported Ubuntu LTS.** + +### Create Flatpak repo of the app + +This is esentially what will happen when being built by FlatHub. + +```bash +flatpak-builder --force-clean build-dir com.veilid.veilidchat.yml --repo=repo +``` + +#### Publish to app store + +When this succeeds you can proceed to [submit to an app store like Flathub](https://github.com/flathub/flathub/wiki/App-Submission). + + +
+ +--- + +
+ +*The remainder is optional if we want to try installing locally, however only +the first step is needed to succeed in order to publish to FlatHub.* + +### Bundle the Flatpak repo into an installable `.flatpak` file + +This part is not done when building for FlatHub. + +```bash +flatpak build-bundle repo com.veilid.veilidchat.flatpak com.veilid.veilidchat +``` + +### We now have a `.flatpak` file that we can install on any machine with + Flatpak: + +```bash +flatpak install --user com.veilid.veilidchat.flatpak +``` + +### We can see that it is installed: + +```bash +flatpak list --app | grep com.veilid.veilidchat +``` + +> Flutter App com.veilid.veilidchat 1.0.0 master flutterapp-origin user + +If we search for "Flutter App" in the system application menu there should be an +entry for the app with the proper name and icon. + +We can also uninstall our test flatpak: + +```bash +flatpak remove com.veilid.veilidchat +``` diff --git a/flatpak/build-flatpak.sh b/flatpak/build-flatpak.sh new file mode 100755 index 0000000..9cc28fe --- /dev/null +++ b/flatpak/build-flatpak.sh @@ -0,0 +1,41 @@ +#!/bin/bash + + +# Convert the archive of the Flutter app to a Flatpak. + + +# Exit if any command fails +set -e + +# Echo all commands for debug purposes +set -x + + +# No spaces in project name. +projectName=VeilidChat +projectId=com.veilid.veilidchat +executableName=veilidchat + + +# ------------------------------- Build Flatpak ----------------------------- # + +# Copy the portable app to the Flatpak-based location. +cp -r bundle/ /app/$projectName +chmod +x /app/$projectName/$executableName +mkdir -p /app/bin +ln -s /app/$projectName/$executableName /app/bin/$executableName + +# Install the icon. +iconDir=/app/share/icons/hicolor/256x256/apps +mkdir -p $iconDir +cp $projectId.png $iconDir/$projectId.png + +# Install the desktop file. +desktopFileDir=/app/share/applications +mkdir -p $desktopFileDir +cp -r $projectId.desktop $desktopFileDir/ + +# Install the AppStream metadata file. +metadataDir=/app/share/metainfo +mkdir -p $metadataDir +cp -r $projectId.metainfo.xml $metadataDir/ diff --git a/flatpak/com.veilid.veilidchat.desktop b/flatpak/com.veilid.veilidchat.desktop new file mode 100755 index 0000000..4b8dd99 --- /dev/null +++ b/flatpak/com.veilid.veilidchat.desktop @@ -0,0 +1,9 @@ +#!/usr/bin/env xdg-open +[Desktop Entry] +Name=VeilidChat +Comment=VeilidChat Private Messaging +Exec=veilidchat +Icon=com.veilid.veilidchat +Terminal=false +Type=Application +Categories=Network; diff --git a/flatpak/com.veilid.veilidchat.metainfo.xml b/flatpak/com.veilid.veilidchat.metainfo.xml new file mode 100644 index 0000000..28a949c --- /dev/null +++ b/flatpak/com.veilid.veilidchat.metainfo.xml @@ -0,0 +1,35 @@ + + + + com.veilid.veilidchat + VeilidChat + VeilidChat Private Messaging + + com.veilid.veilidchat.png + + Veilid Foundation Inc + https://veilid.com/chat + MIT + MPL-2.0 + + pointing + keyboard + touch + + +

TODO

+
+ com.veilid.veilidchat.desktop + + + TODO + + + + + + +
diff --git a/flatpak/com.veilid.veilidchat.png b/flatpak/com.veilid.veilidchat.png new file mode 100644 index 0000000000000000000000000000000000000000..ebcb4cf778ae52fd7a7d740ca818a6c882f78b69 GIT binary patch literal 25795 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 S5!w6z0000 Date: Sun, 18 Feb 2024 18:29:37 +0000 Subject: [PATCH 074/270] flatpak gitlab-ci --- .gitlab-ci.yml | 43 +++++++++++++++++++++- flatpak/com.veilid.veilidchat.metainfo.xml | 3 -- flatpak/com.veilid.veilidchat.yml | 1 - 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 948e3e9..468b12d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,6 +3,7 @@ stages: - build + - build_flatpak # - test .macos_saas_runners: @@ -12,7 +13,7 @@ stages: before_script: - echo "started by ${GITLAB_USER_NAME}" -build: +build_macos: extends: - .macos_saas_runners stage: build @@ -36,10 +37,48 @@ build: #- flutter build appbundle when: manual +build_linux_amd64_bundle: + tags: + - saas-linux-medium-amd64 + image: ghcr.io/cirruslabs/flutter:3.16.9 + stage: build + script: + - apt-get update + - apt-get install -y --no-install-recommends cmake ninja-build clang build-essential pkg-config libgtk-3-dev liblzma-dev lcov rustc cargo + - flutter config --enable-linux-desktop + - git clone https://gitlab.com/veilid/veilid.git ../veilid + - ../veilid/scripts/earthly/install_capnproto.sh + - ../veilid/scripts/earthly/install_protoc.sh + - dart pub get + - flutter build linux + artifacts: + paths: + - build/linux/x64/release/bundle/ + when: manual + +build_linux_amd64_flatpak: + tags: + - saas-linux-small-amd64 + image: ubuntu:23.04 + stage: build_flatpak + dependencies: [build_linux_amd64_bundle] + script: + - apt-get update + - apt-get install -y --no-install-recommends flatpak flatpak-builder gnupg2 elfutils ca-certificates + - flatpak remote-add --no-gpg-verify --if-not-exists flathub http://flathub.org/repo/flathub.flatpakrepo + - flatpak install -y --noninteractive org.gnome.Sdk/x86_64/45 org.gnome.Platform/x86_64/45 app/org.flathub.flatpak-external-data-checker/x86_64/stable org.freedesktop.appstream-glib + - pushd flatpak/ + - flatpak-builder --force-clean build-dir com.veilid.veilidchat.yml --repo=repo + - flatpak build-bundle repo com.veilid.veilidchat.flatpak com.veilid.veilidchat + - popd + artifacts: + paths: + - flatpak/com.veilid.veilidchat.flatpak + when: manual + #test: # extends: # - .macos_saas_runners # stage: test # script: # - echo "place holder for test" - diff --git a/flatpak/com.veilid.veilidchat.metainfo.xml b/flatpak/com.veilid.veilidchat.metainfo.xml index 28a949c..6df997f 100644 --- a/flatpak/com.veilid.veilidchat.metainfo.xml +++ b/flatpak/com.veilid.veilidchat.metainfo.xml @@ -7,9 +7,6 @@ com.veilid.veilidchat VeilidChat VeilidChat Private Messaging - - com.veilid.veilidchat.png - Veilid Foundation Inc https://veilid.com/chat MIT diff --git a/flatpak/com.veilid.veilidchat.yml b/flatpak/com.veilid.veilidchat.yml index 3dc624f..af45e4b 100644 --- a/flatpak/com.veilid.veilidchat.yml +++ b/flatpak/com.veilid.veilidchat.yml @@ -16,7 +16,6 @@ finish-args: - --share=network - --talk-name=org.freedesktop.secrets modules: - # FlutterApp - name: VeilidChat buildsystem: simple only-arches: From d79745f736f914753f85c1590ddd41126b6beda3 Mon Sep 17 00:00:00 2001 From: Paul Sajna Date: Wed, 28 Feb 2024 18:04:24 -0800 Subject: [PATCH 075/270] flutter 3.19.0 --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 468b12d..a04b787 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -40,7 +40,7 @@ build_macos: build_linux_amd64_bundle: tags: - saas-linux-medium-amd64 - image: ghcr.io/cirruslabs/flutter:3.16.9 + image: ghcr.io/cirruslabs/flutter:3.19.0 stage: build script: - apt-get update From 560127d6695dbdc8c9b4bfe94bd0efd1451b254c Mon Sep 17 00:00:00 2001 From: Paul Sajna Date: Mon, 1 Apr 2024 12:37:53 -0700 Subject: [PATCH 076/270] re-add changes lost by rebase --- .gitlab-ci.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a04b787..4476fbd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -40,16 +40,13 @@ build_macos: build_linux_amd64_bundle: tags: - saas-linux-medium-amd64 - image: ghcr.io/cirruslabs/flutter:3.19.0 + image: ghcr.io/cirruslabs/flutter:3.19.4 stage: build script: - apt-get update - apt-get install -y --no-install-recommends cmake ninja-build clang build-essential pkg-config libgtk-3-dev liblzma-dev lcov rustc cargo - flutter config --enable-linux-desktop - git clone https://gitlab.com/veilid/veilid.git ../veilid - - ../veilid/scripts/earthly/install_capnproto.sh - - ../veilid/scripts/earthly/install_protoc.sh - - dart pub get - flutter build linux artifacts: paths: @@ -65,7 +62,7 @@ build_linux_amd64_flatpak: script: - apt-get update - apt-get install -y --no-install-recommends flatpak flatpak-builder gnupg2 elfutils ca-certificates - - flatpak remote-add --no-gpg-verify --if-not-exists flathub http://flathub.org/repo/flathub.flatpakrepo + - flatpak remote-add --no-gpg-verify --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo - flatpak install -y --noninteractive org.gnome.Sdk/x86_64/45 org.gnome.Platform/x86_64/45 app/org.flathub.flatpak-external-data-checker/x86_64/stable org.freedesktop.appstream-glib - pushd flatpak/ - flatpak-builder --force-clean build-dir com.veilid.veilidchat.yml --repo=repo From 9bb20f4dd293021c18041942a0647eaf6cb55099 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 3 Apr 2024 21:55:49 -0400 Subject: [PATCH 077/270] concurrency work in prep for speeding things up refactor splash screen to process initialization in a better way more async tools for async cubit constructors greatly improved StateMapFollower class --- .../cubits/active_local_account_cubit.dart | 13 +- .../cubits/local_accounts_cubit.dart | 13 +- .../cubits/user_logins_cubit.dart | 13 +- .../account_repository.dart | 17 ++- lib/app.dart | 112 +++++++++------- .../cubits/single_contact_messages_cubit.dart | 22 ++- .../active_conversations_bloc_map_cubit.dart | 18 +-- ...ve_single_contact_chat_bloc_map_cubit.dart | 7 +- lib/chat_list/cubits/chat_list_cubit.dart | 73 ++++++---- .../cubits/contact_invitation_list_cubit.dart | 32 ++++- .../cubits/contact_request_inbox_cubit.dart | 2 + .../waiting_invitations_bloc_map_cubit.dart | 13 +- .../models/valid_contact_invitation.dart | 4 + lib/contacts/cubits/contact_list_cubit.dart | 34 +++-- lib/contacts/cubits/conversation_cubit.dart | 107 ++++++++++++--- lib/init.dart | 48 ++++--- .../home_account_ready_shell.dart | 6 +- lib/layout/index.dart | 67 ---------- lib/layout/layout.dart | 2 +- lib/layout/splash.dart | 53 ++++++++ lib/main.dart | 4 - lib/router/cubit/router_cubit.dart | 42 ++---- lib/router/cubit/router_cubit.freezed.dart | 34 +---- lib/router/cubit/router_cubit.g.dart | 2 - lib/router/cubit/router_state.dart | 3 +- lib/tick.dart | 5 - packages/async_tools/lib/async_tools.dart | 2 + .../async_tools/lib/src/delayed_wait_set.dart | 18 +++ .../async_tools/lib/src/single_future.dart | 19 +-- .../lib/src/single_state_processor.dart | 30 ++++- packages/async_tools/lib/src/wait_set.dart | 18 +++ packages/bloc_tools/lib/bloc_tools.dart | 2 +- .../lib/src/async_transformer_cubit.dart | 1 + .../bloc_tools/lib/src/bloc_map_cubit.dart | 27 +++- .../bloc_tools/lib/src/state_follower.dart | 63 --------- .../lib/src/state_map_follower.dart | 125 ++++++++++++++++++ .../dht_support/src/dht_record/barrel.dart | 1 + .../dht_record/default_dht_record_cubit.dart | 66 +++++++++ .../src/dht_record/dht_record.dart | 6 +- .../src/dht_record/dht_record_cubit.dart | 70 ++-------- .../src/dht_record/dht_record_pool.dart | 110 ++++++++++----- .../dht_record/dht_record_pool.freezed.dart | 59 ++++++--- .../src/dht_record/dht_record_pool.g.dart | 32 +++-- .../src/dht_short_array/dht_short_array.dart | 21 ++- .../dht_short_array_cubit.dart | 16 +-- .../dht_short_array/dht_short_array_head.dart | 8 +- packages/veilid_support/lib/src/identity.dart | 25 +++- 47 files changed, 886 insertions(+), 579 deletions(-) delete mode 100644 lib/layout/index.dart create mode 100644 lib/layout/splash.dart create mode 100644 packages/async_tools/lib/src/delayed_wait_set.dart create mode 100644 packages/async_tools/lib/src/wait_set.dart delete mode 100644 packages/bloc_tools/lib/src/state_follower.dart create mode 100644 packages/bloc_tools/lib/src/state_map_follower.dart create mode 100644 packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart diff --git a/lib/account_manager/cubits/active_local_account_cubit.dart b/lib/account_manager/cubits/active_local_account_cubit.dart index 29a76c9..843cc59 100644 --- a/lib/account_manager/cubits/active_local_account_cubit.dart +++ b/lib/account_manager/cubits/active_local_account_cubit.dart @@ -3,24 +3,13 @@ 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 { ActiveLocalAccountCubit(AccountRepository accountRepository) : _accountRepository = accountRepository, - super(null) { + super(accountRepository.getActiveLocalAccount()) { // Subscribe to streams - _initAccountRepositorySubscription(); - - // Initialize when we can - Future.delayed(Duration.zero, () async { - await eventualInitialized.future; - emit(_accountRepository.getActiveLocalAccount()); - }); - } - - void _initAccountRepositorySubscription() { _accountRepositorySubscription = _accountRepository.stream.listen((change) { switch (change) { case AccountRepositoryChange.activeLocalAccount: diff --git a/lib/account_manager/cubits/local_accounts_cubit.dart b/lib/account_manager/cubits/local_accounts_cubit.dart index 376c810..484bdbc 100644 --- a/lib/account_manager/cubits/local_accounts_cubit.dart +++ b/lib/account_manager/cubits/local_accounts_cubit.dart @@ -3,25 +3,14 @@ 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'; class LocalAccountsCubit extends Cubit> { LocalAccountsCubit(AccountRepository accountRepository) : _accountRepository = accountRepository, - super(IList()) { + super(accountRepository.getLocalAccounts()) { // Subscribe to streams - _initAccountRepositorySubscription(); - - // Initialize when we can - Future.delayed(Duration.zero, () async { - await eventualInitialized.future; - emit(_accountRepository.getLocalAccounts()); - }); - } - - void _initAccountRepositorySubscription() { _accountRepositorySubscription = _accountRepository.stream.listen((change) { switch (change) { case AccountRepositoryChange.localAccounts: diff --git a/lib/account_manager/cubits/user_logins_cubit.dart b/lib/account_manager/cubits/user_logins_cubit.dart index 30269c1..9fa6974 100644 --- a/lib/account_manager/cubits/user_logins_cubit.dart +++ b/lib/account_manager/cubits/user_logins_cubit.dart @@ -3,25 +3,14 @@ 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'; class UserLoginsCubit extends Cubit> { UserLoginsCubit(AccountRepository accountRepository) : _accountRepository = accountRepository, - super(IList()) { + super(accountRepository.getUserLogins()) { // Subscribe to streams - _initAccountRepositorySubscription(); - - // Initialize when we can - Future.delayed(Duration.zero, () async { - await eventualInitialized.future; - emit(_accountRepository.getUserLogins()); - }); - } - - void _initAccountRepositorySubscription() { _accountRepositorySubscription = _accountRepository.stream.listen((change) { switch (change) { case AccountRepositoryChange.userLogins: diff --git a/lib/account_manager/repository/account_repository/account_repository.dart b/lib/account_manager/repository/account_repository/account_repository.dart index a13b331..056fd5f 100644 --- a/lib/account_manager/repository/account_repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository/account_repository.dart @@ -202,18 +202,24 @@ class AccountRepository { createAccountCallback: (parent) async { // Make empty contact list log.debug('Creating contacts list'); - final contactList = await (await DHTShortArray.create(parent: parent)) + final contactList = await (await DHTShortArray.create( + debugName: 'AccountRepository::_newLocalAccount::Contacts', + parent: parent)) .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.recordPointer); + final contactInvitationRecords = await (await DHTShortArray.create( + debugName: + 'AccountRepository::_newLocalAccount::ContactInvitations', + parent: parent)) + .scope((r) async => r.recordPointer); // Make empty chat record list log.debug('Creating chat records list'); - final chatRecords = await (await DHTShortArray.create(parent: parent)) + final chatRecords = await (await DHTShortArray.create( + debugName: 'AccountRepository::_newLocalAccount::Chats', + parent: parent)) .scope((r) async => r.recordPointer); // Make account object @@ -391,6 +397,7 @@ class AccountRepository { final pool = DHTRecordPool.instance; final record = await pool.openOwned( userLogin.accountRecordInfo.accountRecord, + debugName: 'AccountRepository::openAccountRecord::AccountRecord', parent: localAccount.identityMaster.identityRecordKey); return record; diff --git a/lib/app.dart b/lib/app.dart index 6624602..2a8b07d 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -5,8 +5,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; +import 'package:provider/provider.dart'; import 'account_manager/account_manager.dart'; +import 'init.dart'; +import 'layout/splash.dart'; import 'router/router.dart'; import 'settings/settings.dart'; import 'tick.dart'; @@ -23,57 +26,66 @@ class VeilidChatApp extends StatelessWidget { final ThemeData initialThemeData; @override - Widget build(BuildContext context) { - final localizationDelegate = LocalizedApp.of(context).delegate; - - return ThemeProvider( - initTheme: initialThemeData, - builder: (_, theme) => LocalizationProvider( - state: LocalizationProvider.of(context).state, - child: MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => - ConnectionStateCubit(ProcessorRepository.instance)), - BlocProvider( - create: (context) => - RouterCubit(AccountRepository.instance), - ), - BlocProvider( - create: (context) => - LocalAccountsCubit(AccountRepository.instance), - ), - BlocProvider( - create: (context) => - UserLoginsCubit(AccountRepository.instance), - ), - BlocProvider( - create: (context) => - ActiveLocalAccountCubit(AccountRepository.instance), - ), - BlocProvider( - create: (context) => - PreferencesCubit(PreferencesRepository.instance), - ) - ], - child: BackgroundTicker( - builder: (context) => MaterialApp.router( - debugShowCheckedModeBanner: false, - routerConfig: context.watch().router(), - title: translate('app.title'), - theme: theme, - localizationsDelegates: [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - FormBuilderLocalizations.delegate, - localizationDelegate + Widget build(BuildContext context) => FutureProvider( + initialData: null, + create: (context) async => VeilidChatGlobalInit.initialize(), + builder: (context, child) { + final globalInit = context.watch(); + if (globalInit == null) { + // Splash screen until we're done with init + return const Splash(); + } + // Once init is done, we proceed with the app + final localizationDelegate = LocalizedApp.of(context).delegate; + return ThemeProvider( + initTheme: initialThemeData, + builder: (_, theme) => LocalizationProvider( + state: LocalizationProvider.of(context).state, + child: MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => ConnectionStateCubit( + ProcessorRepository.instance)), + BlocProvider( + create: (context) => + RouterCubit(AccountRepository.instance), + ), + BlocProvider( + create: (context) => + LocalAccountsCubit(AccountRepository.instance), + ), + BlocProvider( + create: (context) => + UserLoginsCubit(AccountRepository.instance), + ), + BlocProvider( + create: (context) => ActiveLocalAccountCubit( + AccountRepository.instance), + ), + BlocProvider( + create: (context) => + PreferencesCubit(PreferencesRepository.instance), + ) ], - supportedLocales: localizationDelegate.supportedLocales, - locale: localizationDelegate.currentLocale, - ), - )), - )); - } + child: BackgroundTicker( + builder: (context) => MaterialApp.router( + debugShowCheckedModeBanner: false, + routerConfig: context.watch().router(), + title: translate('app.title'), + theme: theme, + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + FormBuilderLocalizations.delegate, + localizationDelegate + ], + supportedLocales: + localizationDelegate.supportedLocales, + locale: localizationDelegate.currentLocale, + ), + )), + )); + }); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 4427b89..a3a837d 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -38,11 +38,13 @@ class SingleContactMessagesCubit extends Cubit { _messagesUpdateQueue = StreamController(), super(const AsyncValue.loading()) { // Async Init - Future.delayed(Duration.zero, _init); + _initWait.add(_init); } @override Future close() async { + await _initWait(); + await _messagesUpdateQueue.close(); await _localSubscription?.cancel(); await _remoteSubscription?.cancel(); @@ -89,7 +91,10 @@ class SingleContactMessagesCubit extends Cubit { _localMessagesCubit = DHTShortArrayCubit( open: () async => DHTShortArray.openWrite( _localMessagesRecordKey, writer, - parent: _localConversationRecordKey, crypto: _messagesCrypto), + debugName: + 'SingleContactMessagesCubit::_initLocalMessages::LocalMessages', + parent: _localConversationRecordKey, + crypto: _messagesCrypto), decodeElement: proto.Message.fromBuffer); _localSubscription = _localMessagesCubit!.stream.listen(_updateLocalMessagesState); @@ -100,7 +105,10 @@ class SingleContactMessagesCubit extends Cubit { Future _initRemoteMessages() async { _remoteMessagesCubit = DHTShortArrayCubit( open: () async => DHTShortArray.openRead(_remoteMessagesRecordKey, - parent: _remoteConversationRecordKey, crypto: _messagesCrypto), + debugName: 'SingleContactMessagesCubit::_initRemoteMessages::' + 'RemoteMessages', + parent: _remoteConversationRecordKey, + crypto: _messagesCrypto), decodeElement: proto.Message.fromBuffer); _remoteSubscription = _remoteMessagesCubit!.stream.listen(_updateRemoteMessagesState); @@ -114,6 +122,9 @@ class SingleContactMessagesCubit extends Cubit { _reconciledChatMessagesCubit = DHTShortArrayCubit( open: () async => DHTShortArray.openOwned(_reconciledChatRecord, + debugName: + 'SingleContactMessagesCubit::_initReconciledChatMessages::' + 'ReconciledChat', parent: accountRecordKey), decodeElement: proto.Message.fromBuffer); _reconciledChatSubscription = @@ -237,6 +248,8 @@ class SingleContactMessagesCubit extends Cubit { // Force refresh of messages Future refresh() async { + await _initWait(); + final lcc = _localMessagesCubit; final rcc = _remoteMessagesCubit; @@ -249,10 +262,13 @@ class SingleContactMessagesCubit extends Cubit { } Future addMessage({required proto.Message message}) async { + await _initWait(); + await _localMessagesCubit! .operateWrite((writer) => writer.tryAddItem(message.writeToBuffer())); } + final WaitSet _initWait = WaitSet(); final ActiveAccountInfo _activeAccountInfo; final TypedKey _remoteIdentityPublicKey; final TypedKey _localConversationRecordKey; 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 8ce919a..54cbe38 100644 --- a/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart +++ b/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart @@ -1,13 +1,13 @@ 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'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../contacts/contacts.dart'; import '../../proto/proto.dart' as proto; +import 'cubits.dart'; @immutable class ActiveConversationState extends Equatable { @@ -39,9 +39,7 @@ typedef ActiveConversationsBlocMapState // archived chats or contacts that are not actively in a chat. class ActiveConversationsBlocMapCubit extends BlocMapCubit, ActiveConversationCubit> - with - StateFollower>>, TypedKey, - proto.Chat> { + with StateMapFollower { ActiveConversationsBlocMapCubit( {required ActiveAccountInfo activeAccountInfo, required ContactListCubit contactListCubit}) @@ -77,18 +75,6 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit getStateMap( - BlocBusyState>> state) { - final stateValue = state.state.data?.value; - if (stateValue == null) { - return IMap(); - } - return IMap.fromIterable(stateValue, - keyMapper: (e) => e.remoteConversationRecordKey.toVeilid(), - valueMapper: (e) => e); - } - @override Future removeFromState(TypedKey key) => remove(key); 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 index c6827f3..7211150 100644 --- 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 @@ -18,7 +18,7 @@ import 'chat_list_cubit.dart'; class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit>, SingleContactMessagesCubit> with - StateFollower> { ActiveSingleContactChatBlocMapCubit( {required ActiveAccountInfo activeAccountInfo, @@ -49,11 +49,6 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit> getStateMap( - ActiveConversationsBlocMapState state) => - state; - @override Future removeFromState(TypedKey key) => remove(key); diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index ea6261b..68a55db 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -1,5 +1,8 @@ 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'; @@ -11,8 +14,10 @@ import '../../tools/tools.dart'; ////////////////////////////////////////////////// // Mutable state for per-account chat list +typedef ChatListCubitState = BlocBusyState>>; -class ChatListCubit extends DHTShortArrayCubit { +class ChatListCubit extends DHTShortArrayCubit + with StateMapFollowable { ChatListCubit({ required ActiveAccountInfo activeAccountInfo, required proto.Account account, @@ -30,7 +35,7 @@ class ChatListCubit extends DHTShortArrayCubit { final chatListRecordKey = account.chatList.toVeilid(); final dhtRecord = await DHTShortArray.openOwned(chatListRecordKey, - parent: accountRecordKey); + debugName: 'ChatListCubit::_open::ChatList', parent: accountRecordKey); return dhtRecord; } @@ -61,9 +66,11 @@ class ChatListCubit extends DHTShortArrayCubit { .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); + final reconciledChatRecord = await (await DHTShortArray.create( + debugName: + 'ChatListCubit::getOrCreateChatSingleContact::ReconciledChat', + parent: accountRecordKey)) + .scope((r) async => r.recordPointer); // Create conversation type Chat final chat = proto.Chat() @@ -86,26 +93,30 @@ class ChatListCubit extends DHTShortArrayCubit { // Remove Chat from account's list // if this fails, don't keep retrying, user can try again later - final (deletedItem, success) = await operateWrite((writer) async { - if (activeChatCubit.state == remoteConversationRecordKey) { - activeChatCubit.setActiveChat(null); - } - 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 writer.tryRemoveItem(i) != null) { - return c; - } - return null; - } - } - return null; - }); + final (deletedItem, success) = + // Ensure followers get their changes before we return + await syncFollowers(() => operateWrite((writer) async { + if (activeChatCubit.state == remoteConversationRecordKey) { + activeChatCubit.setActiveChat(null); + } + 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 writer.tryRemoveItem(i) != null) { + return c; + } + return null; + } + } + return null; + })); + // Since followers are synced, we can safetly remove the reconciled + // chat record now if (success && deletedItem != null) { try { await DHTRecordPool.instance @@ -116,6 +127,18 @@ class ChatListCubit extends DHTShortArrayCubit { } } + /// StateMapFollowable ///////////////////////// + @override + IMap getStateMap(ChatListCubitState state) { + final stateValue = state.state.data?.value; + if (stateValue == null) { + return IMap(); + } + return IMap.fromIterable(stateValue, + keyMapper: (e) => e.remoteConversationRecordKey.toVeilid(), + valueMapper: (e) => e); + } + final ActiveChatCubit activeChatCubit; final ActiveAccountInfo _activeAccountInfo; } diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index 1c3f148..1163a03 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -1,5 +1,8 @@ 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:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; import 'package:veilid_support/veilid_support.dart'; @@ -23,11 +26,16 @@ typedef GetEncryptionKeyCallback = Future Function( ////////////////////////////////////////////////// +typedef ContactInvitiationListState + = BlocBusyState>>; ////////////////////////////////////////////////// // Mutable state for per-account contact invitations class ContactInvitationListCubit - extends DHTShortArrayCubit { + extends DHTShortArrayCubit + with + StateMapFollowable { ContactInvitationListCubit({ required ActiveAccountInfo activeAccountInfo, required proto.Account account, @@ -47,6 +55,7 @@ class ContactInvitationListCubit final dhtRecord = await DHTShortArray.openOwned( contactInvitationListRecordPointer, + debugName: 'ContactInvitationListCubit::_open::ContactInvitationList', parent: accountRecordKey); return dhtRecord; @@ -78,6 +87,8 @@ class ContactInvitationListCubit // identity key late final Uint8List signedContactInvitationBytes; await (await pool.create( + debugName: 'ContactInvitationListCubit::createInvitation::' + 'LocalConversation', parent: _activeAccountInfo.accountRecordKey, schema: DHTSchema.smpl(oCnt: 0, members: [ DHTSchemaMember(mKey: conversationWriter.key, mCnt: 1) @@ -105,6 +116,8 @@ class ContactInvitationListCubit // Subkey 0 is the ContactRequest from the initiator // Subkey 1 will contain the invitation response accept/reject eventually await (await pool.create( + debugName: 'ContactInvitationListCubit::createInvitation::' + 'ContactRequestInbox', parent: _activeAccountInfo.accountRecordKey, schema: DHTSchema.smpl(oCnt: 1, members: [ DHTSchemaMember(mCnt: 1, mKey: contactRequestWriter.key) @@ -180,6 +193,8 @@ class ContactInvitationListCubit // Delete the contact request inbox final contactRequestInbox = deletedItem.contactRequestInbox.toVeilid(); await (await pool.openOwned(contactRequestInbox, + debugName: 'ContactInvitationListCubit::deleteInvitation::' + 'ContactRequestInbox', parent: accountRecordKey)) .scope((contactRequestInbox) async { // Wipe out old invitation so it shows up as invalid @@ -229,6 +244,8 @@ class ContactInvitationListCubit -1; await (await pool.openRead(contactRequestInboxKey, + debugName: 'ContactInvitationListCubit::validateInvitation::' + 'ContactRequestInbox', parent: _activeAccountInfo.accountRecordKey)) .maybeDeleteScope(!isSelf, (contactRequestInbox) async { // @@ -282,6 +299,19 @@ class ContactInvitationListCubit return out; } + /// StateMapFollowable ///////////////////////// + @override + IMap getStateMap( + ContactInvitiationListState state) { + final stateValue = state.state.data?.value; + if (stateValue == null) { + return IMap(); + } + return IMap.fromIterable(stateValue, + keyMapper: (e) => e.contactRequestInbox.recordKey.toVeilid(), + valueMapper: (e) => e); + } + // 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 80c18ae..25f49a2 100644 --- a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart +++ b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart @@ -33,6 +33,8 @@ class ContactRequestInboxCubit final writer = TypedKeyPair( kind: recordKey.kind, key: writerKey, secret: writerSecret); return pool.openRead(recordKey, + debugName: 'ContactRequestInboxCubit::_open::' + 'ContactRequestInbox', crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer), parent: accountRecordKey, defaultSubkey: 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 c674a14..bf81b4e 100644 --- a/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart @@ -16,7 +16,7 @@ typedef WaitingInvitationsBlocMapState class WaitingInvitationsBlocMapCubit extends BlocMapCubit, WaitingInvitationCubit> with - StateFollower< + StateMapFollower< BlocBusyState>>, TypedKey, proto.ContactInvitationRecord> { @@ -37,17 +37,6 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit getStateMap( - BlocBusyState>> state) { - final stateValue = state.state.data?.value; - if (stateValue == null) { - return IMap(); - } - return IMap.fromIterable(stateValue, - keyMapper: (e) => e.contactRequestInbox.recordKey.toVeilid(), - valueMapper: (e) => e); - } @override Future removeFromState(TypedKey key) => remove(key); diff --git a/lib/contact_invitation/models/valid_contact_invitation.dart b/lib/contact_invitation/models/valid_contact_invitation.dart index 88f43d9..2a3d140 100644 --- a/lib/contact_invitation/models/valid_contact_invitation.dart +++ b/lib/contact_invitation/models/valid_contact_invitation.dart @@ -38,6 +38,8 @@ class ValidContactInvitation { final accountRecordKey = _activeAccountInfo.accountRecordKey; return (await pool.openWrite(_contactRequestInboxKey, _writer, + debugName: 'ValidContactInvitation::accept::' + 'ContactRequestInbox', parent: accountRecordKey)) // ignore: prefer_expression_function_bodies .maybeDeleteScope(!isSelf, (contactRequestInbox) async { @@ -103,6 +105,8 @@ class ValidContactInvitation { _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; return (await pool.openWrite(_contactRequestInboxKey, _writer, + debugName: 'ValidContactInvitation::reject::' + 'ContactRequestInbox', parent: accountRecordKey)) .maybeDeleteScope(!isSelf, (contactRequestInbox) async { final cs = diff --git a/lib/contacts/cubits/contact_list_cubit.dart b/lib/contacts/cubits/contact_list_cubit.dart index 99f13bf..e30c8ed 100644 --- a/lib/contacts/cubits/contact_list_cubit.dart +++ b/lib/contacts/cubits/contact_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 'conversation_cubit.dart'; ////////////////////////////////////////////////// // Mutable state for per-account contacts @@ -14,7 +15,8 @@ class ContactListCubit extends DHTShortArrayCubit { ContactListCubit({ required ActiveAccountInfo activeAccountInfo, required proto.Account account, - }) : super( + }) : _activeAccountInfo = activeAccountInfo, + super( open: () => _open(activeAccountInfo, account), decodeElement: proto.Contact.fromBuffer); @@ -26,6 +28,7 @@ class ContactListCubit extends DHTShortArrayCubit { final contactListRecordKey = account.contactList.toVeilid(); final dhtRecord = await DHTShortArray.openOwned(contactListRecordKey, + debugName: 'ContactListCubit::_open::ContactList', parent: accountRecordKey); return dhtRecord; @@ -60,9 +63,10 @@ class ContactListCubit extends DHTShortArrayCubit { } Future deleteContact({required proto.Contact contact}) async { - final pool = DHTRecordPool.instance; - final localConversationKey = contact.localConversationRecordKey.toVeilid(); - final remoteConversationKey = + final remoteIdentityPublicKey = contact.identityPublicKey.toVeilid(); + final localConversationRecordKey = + contact.localConversationRecordKey.toVeilid(); + final remoteConversationRecordKey = contact.remoteConversationRecordKey.toVeilid(); // Remove Contact from account's list @@ -85,17 +89,21 @@ class ContactListCubit extends DHTShortArrayCubit { if (success && deletedItem != null) { try { - await pool.delete(localConversationKey); + // Make a conversation cubit to manipulate the conversation + final conversationCubit = ConversationCubit( + activeAccountInfo: _activeAccountInfo, + remoteIdentityPublicKey: remoteIdentityPublicKey, + localConversationRecordKey: localConversationRecordKey, + remoteConversationRecordKey: remoteConversationRecordKey, + ); + + // Delete the local and remote conversation records + await conversationCubit.delete(); } on Exception catch (e) { - log.debug('error removing local conversation record key: $e', e); - } - try { - if (localConversationKey != remoteConversationKey) { - await pool.delete(remoteConversationKey); - } - } on Exception catch (e) { - log.debug('error removing remote conversation record key: $e', e); + log.debug('error deleting conversation records: $e', e); } } } + + final ActiveAccountInfo _activeAccountInfo; } diff --git a/lib/contacts/cubits/conversation_cubit.dart b/lib/contacts/cubits/conversation_cubit.dart index 34c609d..3b5258b 100644 --- a/lib/contacts/cubits/conversation_cubit.dart +++ b/lib/contacts/cubits/conversation_cubit.dart @@ -13,6 +13,7 @@ import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../proto/proto.dart' as proto; +import '../../tools/tools.dart'; @immutable class ConversationState extends Equatable { @@ -36,11 +37,9 @@ class ConversationCubit extends Cubit> { _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 { + _initWait.add(() async { await _setLocalConversation(() async { final accountRecordKey = _activeAccountInfo .userLogin.accountRecordInfo.accountRecord.recordKey; @@ -51,14 +50,16 @@ class ConversationCubit extends Cubit> { final writer = _activeAccountInfo.conversationWriter; final record = await pool.openWrite( _localConversationRecordKey!, writer, - parent: accountRecordKey, crypto: crypto); + debugName: 'ConversationCubit::LocalConversation', + parent: accountRecordKey, + crypto: crypto); return record; }); }); } if (_remoteConversationRecordKey != null) { - Future.delayed(Duration.zero, () async { + _initWait.add(() async { await _setRemoteConversation(() async { final accountRecordKey = _activeAccountInfo .userLogin.accountRecordInfo.accountRecord.recordKey; @@ -67,7 +68,9 @@ class ConversationCubit extends Cubit> { final pool = DHTRecordPool.instance; final crypto = await _cachedConversationCrypto(); final record = await pool.openRead(_remoteConversationRecordKey, - parent: accountRecordKey, crypto: crypto); + debugName: 'ConversationCubit::RemoteConversation', + parent: accountRecordKey, + crypto: crypto); return record; }); }); @@ -76,6 +79,7 @@ class ConversationCubit extends Cubit> { @override Future close() async { + await _initWait(); await _localSubscription?.cancel(); await _remoteSubscription?.cancel(); await _localConversationCubit?.close(); @@ -84,7 +88,7 @@ class ConversationCubit extends Cubit> { await super.close(); } - void updateLocalConversationState(AsyncValue avconv) { + void _updateLocalConversationState(AsyncValue avconv) { final newState = avconv.when( data: (conv) { _incrementalState = ConversationState( @@ -106,7 +110,7 @@ class ConversationCubit extends Cubit> { emit(newState); } - void updateRemoteConversationState(AsyncValue avconv) { + void _updateRemoteConversationState(AsyncValue avconv) { final newState = avconv.when( data: (conv) { _incrementalState = ConversationState( @@ -135,7 +139,7 @@ class ConversationCubit extends Cubit> { _localConversationCubit = DefaultDHTRecordCubit( open: open, decodeState: proto.Conversation.fromBuffer); _localSubscription = - _localConversationCubit!.stream.listen(updateLocalConversationState); + _localConversationCubit!.stream.listen(_updateLocalConversationState); } // Open remote converation key @@ -145,7 +149,57 @@ class ConversationCubit extends Cubit> { _remoteConversationCubit = DefaultDHTRecordCubit( open: open, decodeState: proto.Conversation.fromBuffer); _remoteSubscription = - _remoteConversationCubit!.stream.listen(updateRemoteConversationState); + _remoteConversationCubit!.stream.listen(_updateRemoteConversationState); + } + + Future delete() async { + final pool = DHTRecordPool.instance; + + await _initWait(); + final localConversationCubit = _localConversationCubit; + final remoteConversationCubit = _remoteConversationCubit; + + final deleteSet = DelayedWaitSet(); + + if (localConversationCubit != null) { + final data = localConversationCubit.state.data; + if (data == null) { + log.warning('could not delete local conversation'); + return false; + } + + deleteSet.add(() async { + _localConversationCubit = null; + await localConversationCubit.close(); + final conversation = data.value; + final messagesKey = conversation.messages.toVeilid(); + await pool.delete(messagesKey); + await pool.delete(_localConversationRecordKey!); + _localConversationRecordKey = null; + }); + } + + if (remoteConversationCubit != null) { + final data = remoteConversationCubit.state.data; + if (data == null) { + log.warning('could not delete remote conversation'); + return false; + } + + deleteSet.add(() async { + _remoteConversationCubit = null; + await remoteConversationCubit.close(); + final conversation = data.value; + final messagesKey = conversation.messages.toVeilid(); + await pool.delete(messagesKey); + await pool.delete(_remoteConversationRecordKey!); + }); + } + + // Commit the delete futures + await deleteSet(); + + return true; } // Initialize a local conversation @@ -174,23 +228,25 @@ class ConversationCubit extends Cubit> { if (existingConversationRecordKey != null) { localConversationRecord = await pool.openWrite( existingConversationRecordKey, writer, - parent: accountRecordKey, crypto: crypto); + debugName: + 'ConversationCubit::initLocalConversation::LocalConversation', + parent: accountRecordKey, + crypto: crypto); } else { - final localConversationRecordCreate = await pool.create( + localConversationRecord = await pool.create( + debugName: + 'ConversationCubit::initLocalConversation::LocalConversation', parent: accountRecordKey, crypto: crypto, + writer: writer, 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 initLocalMessages( + return _initLocalMessages( activeAccountInfo: _activeAccountInfo, remoteIdentityPublicKey: _remoteIdentityPublicKey, localConversationKey: localConversation.key, @@ -211,7 +267,7 @@ class ConversationCubit extends Cubit> { final out = await callback(localConversation); // Upon success emit the local conversation record to the state - updateLocalConversationState(AsyncValue.data(conversation)); + _updateLocalConversationState(AsyncValue.data(conversation)); return out; }); @@ -225,7 +281,7 @@ class ConversationCubit extends Cubit> { } // Initialize local messages - Future initLocalMessages({ + Future _initLocalMessages({ required ActiveAccountInfo activeAccountInfo, required TypedKey remoteIdentityPublicKey, required TypedKey localConversationKey, @@ -236,12 +292,17 @@ class ConversationCubit extends Cubit> { final writer = activeAccountInfo.conversationWriter; return (await DHTShortArray.create( - parent: localConversationKey, crypto: crypto, smplWriter: writer)) + debugName: 'ConversationCubit::initLocalMessages::LocalMessages', + parent: localConversationKey, + crypto: crypto, + smplWriter: writer)) .deleteScope((messages) async => await callback(messages)); } // Force refresh of conversation keys Future refresh() async { + await _initWait(); + final lcc = _localConversationCubit; final rcc = _remoteConversationCubit; @@ -260,7 +321,7 @@ class ConversationCubit extends Cubit> { .tryWriteProtobuf(proto.Conversation.fromBuffer, conversation); if (update != null) { - updateLocalConversationState(AsyncValue.data(conversation)); + _updateLocalConversationState(AsyncValue.data(conversation)); } return update; @@ -286,7 +347,9 @@ class ConversationCubit extends Cubit> { DefaultDHTRecordCubit? _remoteConversationCubit; StreamSubscription>? _localSubscription; StreamSubscription>? _remoteSubscription; - ConversationState _incrementalState; + ConversationState _incrementalState = const ConversationState( + localConversation: null, remoteConversation: null); // DHTRecordCrypto? _conversationCrypto; + final WaitSet _initWait = WaitSet(); } diff --git a/lib/init.dart b/lib/init.dart index edc954b..24ec467 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -8,34 +8,38 @@ import 'app.dart'; import 'tools/tools.dart'; import 'veilid_processor/veilid_processor.dart'; -final Completer eventualInitialized = Completer(); +class VeilidChatGlobalInit { + VeilidChatGlobalInit._(); -// Initialize Veilid -Future initializeVeilid() async { - // Init Veilid - Veilid.instance.initializeVeilidCore( - getDefaultVeilidPlatformConfig(kIsWeb, VeilidChatApp.name)); + // Initialize Veilid + Future _initializeVeilid() async { + // Init Veilid + Veilid.instance.initializeVeilidCore( + getDefaultVeilidPlatformConfig(kIsWeb, VeilidChatApp.name)); - // Veilid logging - initVeilidLog(kDebugMode); + // Veilid logging + initVeilidLog(kDebugMode); - // Startup Veilid - await ProcessorRepository.instance.startup(); + // Startup Veilid + await ProcessorRepository.instance.startup(); - // DHT Record Pool - await DHTRecordPool.init(); -} + // DHT Record Pool + await DHTRecordPool.init(); + } // Initialize repositories -Future initializeRepositories() async { - await AccountRepository.instance.init(); -} + Future _initializeRepositories() async { + await AccountRepository.instance.init(); + } -Future initializeVeilidChat() async { - log.info('Initializing Veilid'); - await initializeVeilid(); - log.info('Initializing Repositories'); - await initializeRepositories(); + static Future initialize() async { + final veilidChatGlobalInit = VeilidChatGlobalInit._(); - eventualInitialized.complete(); + log.info('Initializing Veilid'); + await veilidChatGlobalInit._initializeVeilid(); + log.info('Initializing Repositories'); + await veilidChatGlobalInit._initializeRepositories(); + + return veilidChatGlobalInit; + } } 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 2cdce7e..70ccd46 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 @@ -137,17 +137,17 @@ class HomeAccountReadyShellState extends State { create: (context) => ActiveConversationsBlocMapCubit( activeAccountInfo: widget.activeAccountInfo, contactListCubit: context.read()) - ..followBloc(context.read())), + ..follow(context.read())), BlocProvider( create: (context) => ActiveSingleContactChatBlocMapCubit( activeAccountInfo: widget.activeAccountInfo, contactListCubit: context.read(), chatListCubit: context.read()) - ..followBloc(context.read())), + ..follow(context.read())), BlocProvider( create: (context) => WaitingInvitationsBlocMapCubit( activeAccountInfo: widget.activeAccountInfo, account: account) - ..followBloc(context.read())) + ..follow(context.read())) ], child: MultiBlocListener(listeners: [ BlocListener 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); - final textTheme = theme.textTheme; - final monoTextStyle = textTheme.labelSmall! - .copyWith(fontFamily: 'Source Code Pro', fontSize: 11); - final emojiTextStyle = textTheme.labelSmall! - .copyWith(fontFamily: 'Noto Color Emoji', fontSize: 11); - - return Scaffold( - body: DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - RadixColors.dark.plum.step4, - RadixColors.dark.plum.step2, - ])), - child: Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 300), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Hack to preload fonts - Offstage(child: Text('🧱', style: emojiTextStyle)), - // Hack to preload fonts - Offstage(child: Text('A', style: monoTextStyle)), - // Splash Screen - Expanded( - flex: 2, - child: SvgPicture.asset( - 'assets/images/icon.svg', - )), - Expanded( - child: SvgPicture.asset( - 'assets/images/title.svg', - )) - ]))), - )); - } -} diff --git a/lib/layout/layout.dart b/lib/layout/layout.dart index 003d97a..985a099 100644 --- a/lib/layout/layout.dart +++ b/lib/layout/layout.dart @@ -1,4 +1,4 @@ export 'default_app_bar.dart'; export 'home/home.dart'; export 'home/home_account_ready/main_pager/main_pager.dart'; -export 'index.dart'; +export 'splash.dart'; diff --git a/lib/layout/splash.dart b/lib/layout/splash.dart new file mode 100644 index 0000000..aa22c0c --- /dev/null +++ b/lib/layout/splash.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:radix_colors/radix_colors.dart'; + +import '../tools/tools.dart'; + +class Splash extends StatefulWidget { + const Splash({super.key}); + + @override + State createState() => _SplashState(); +} + +class _SplashState extends State { + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + await changeWindowSetup( + TitleBarStyle.hidden, OrientationCapability.normal); + }); + } + + @override + Widget build(BuildContext context) => DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + RadixColors.dark.plum.step4, + RadixColors.dark.plum.step2, + ])), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 300), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Splash Screen + Expanded( + flex: 2, + child: SvgPicture.asset( + 'assets/images/icon.svg', + )), + Expanded( + child: SvgPicture.asset( + 'assets/images/title.svg', + )) + ]))), + ); +} diff --git a/lib/main.dart b/lib/main.dart index 64ee506..ab3feed 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,7 +8,6 @@ import 'package:flutter_translate/flutter_translate.dart'; 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'; @@ -45,9 +44,6 @@ void main() async { fallbackLocale: 'en_US', supportedLocales: ['en_US']); await initializeDateFormatting(); - // Start up Veilid and Veilid processor in the background - unawaited(initializeVeilidChat()); - // Run the app // Hot reloads will only restart this part, not Veilid runApp(LocalizedApp(localizationDelegate, diff --git a/lib/router/cubit/router_cubit.dart b/lib/router/cubit/router_cubit.dart index 296e448..83bc477 100644 --- a/lib/router/cubit/router_cubit.dart +++ b/lib/router/cubit/router_cubit.dart @@ -9,7 +9,6 @@ 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'; @@ -24,19 +23,10 @@ final _homeNavKey = GlobalKey(debugLabel: 'homeNavKey'); class RouterCubit extends Cubit { RouterCubit(AccountRepository accountRepository) - : super(const RouterState( - isInitialized: false, - hasAnyAccount: false, + : super(RouterState( + hasAnyAccount: accountRepository.getLocalAccounts().isNotEmpty, hasActiveChat: false, )) { - // Watch for changes that the router will care about - Future.delayed(Duration.zero, () async { - await eventualInitialized.future; - emit(state.copyWith( - isInitialized: true, - hasAnyAccount: accountRepository.getLocalAccounts().isNotEmpty)); - }); - // Subscribe to repository streams _accountRepositorySubscription = accountRepository.stream.listen((event) { switch (event) { @@ -63,10 +53,6 @@ class RouterCubit extends Cubit { /// Our application routes List get routes => [ - GoRoute( - path: '/', - builder: (context, state) => const IndexPage(), - ), ShellRoute( navigatorKey: _homeNavKey, builder: (context, state, child) => HomeShell( @@ -75,11 +61,11 @@ class RouterCubit extends Cubit { HomeAccountReadyShell(context: context, child: child))), routes: [ GoRoute( - path: '/home', + path: '/', builder: (context, state) => const HomeAccountReadyMain(), ), GoRoute( - path: '/home/chat', + path: '/chat', builder: (context, state) => const HomeAccountReadyChat(), ), ], @@ -103,17 +89,9 @@ class RouterCubit extends Cubit { // No matter where we are, if there's not switch (goRouterState.matchedLocation) { - case '/': - - // Wait for initialization to complete - if (!eventualInitialized.isCompleted) { - return null; - } - - return state.hasAnyAccount ? '/home' : '/new_account'; case '/new_account': - return state.hasAnyAccount ? '/home' : null; - case '/home': + return state.hasAnyAccount ? '/' : null; + case '/': if (!state.hasAnyAccount) { return '/new_account'; } @@ -123,11 +101,11 @@ class RouterCubit extends Cubit { tabletLandscape: false, desktop: false)) { if (state.hasActiveChat) { - return '/home/chat'; + return '/chat'; } } return null; - case '/home/chat': + case '/chat': if (!state.hasAnyAccount) { return '/new_account'; } @@ -137,10 +115,10 @@ class RouterCubit extends Cubit { tabletLandscape: false, desktop: false)) { if (!state.hasActiveChat) { - return '/home'; + return '/'; } } else { - return '/home'; + return '/'; } return null; case '/settings': diff --git a/lib/router/cubit/router_cubit.freezed.dart b/lib/router/cubit/router_cubit.freezed.dart index bbe7a04..c36db7d 100644 --- a/lib/router/cubit/router_cubit.freezed.dart +++ b/lib/router/cubit/router_cubit.freezed.dart @@ -20,7 +20,6 @@ RouterState _$RouterStateFromJson(Map json) { /// @nodoc mixin _$RouterState { - bool get isInitialized => throw _privateConstructorUsedError; bool get hasAnyAccount => throw _privateConstructorUsedError; bool get hasActiveChat => throw _privateConstructorUsedError; @@ -36,7 +35,7 @@ abstract class $RouterStateCopyWith<$Res> { RouterState value, $Res Function(RouterState) then) = _$RouterStateCopyWithImpl<$Res, RouterState>; @useResult - $Res call({bool isInitialized, bool hasAnyAccount, bool hasActiveChat}); + $Res call({bool hasAnyAccount, bool hasActiveChat}); } /// @nodoc @@ -52,15 +51,10 @@ class _$RouterStateCopyWithImpl<$Res, $Val extends RouterState> @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 @@ -81,7 +75,7 @@ abstract class _$$RouterStateImplCopyWith<$Res> __$$RouterStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({bool isInitialized, bool hasAnyAccount, bool hasActiveChat}); + $Res call({bool hasAnyAccount, bool hasActiveChat}); } /// @nodoc @@ -95,15 +89,10 @@ class __$$RouterStateImplCopyWithImpl<$Res> @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 @@ -120,15 +109,11 @@ class __$$RouterStateImplCopyWithImpl<$Res> @JsonSerializable() class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState { const _$RouterStateImpl( - {required this.isInitialized, - required this.hasAnyAccount, - required this.hasActiveChat}); + {required this.hasAnyAccount, required this.hasActiveChat}); factory _$RouterStateImpl.fromJson(Map json) => _$$RouterStateImplFromJson(json); - @override - final bool isInitialized; @override final bool hasAnyAccount; @override @@ -136,7 +121,7 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState { @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'RouterState(isInitialized: $isInitialized, hasAnyAccount: $hasAnyAccount, hasActiveChat: $hasActiveChat)'; + return 'RouterState(hasAnyAccount: $hasAnyAccount, hasActiveChat: $hasActiveChat)'; } @override @@ -144,7 +129,6 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('type', 'RouterState')) - ..add(DiagnosticsProperty('isInitialized', isInitialized)) ..add(DiagnosticsProperty('hasAnyAccount', hasAnyAccount)) ..add(DiagnosticsProperty('hasActiveChat', hasActiveChat)); } @@ -154,8 +138,6 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState { 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) || @@ -164,8 +146,7 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState { @JsonKey(ignore: true) @override - int get hashCode => - Object.hash(runtimeType, isInitialized, hasAnyAccount, hasActiveChat); + int get hashCode => Object.hash(runtimeType, hasAnyAccount, hasActiveChat); @JsonKey(ignore: true) @override @@ -183,15 +164,12 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState { abstract class _RouterState implements RouterState { const factory _RouterState( - {required final bool isInitialized, - required final bool hasAnyAccount, + {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 diff --git a/lib/router/cubit/router_cubit.g.dart b/lib/router/cubit/router_cubit.g.dart index f67c770..31ca24a 100644 --- a/lib/router/cubit/router_cubit.g.dart +++ b/lib/router/cubit/router_cubit.g.dart @@ -8,14 +8,12 @@ part of 'router_cubit.dart'; _$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 index 072f797..ac60c39 100644 --- a/lib/router/cubit/router_state.dart +++ b/lib/router/cubit/router_state.dart @@ -3,8 +3,7 @@ part of 'router_cubit.dart'; @freezed class RouterState with _$RouterState { const factory RouterState( - {required bool isInitialized, - required bool hasAnyAccount, + {required bool hasAnyAccount, required bool hasActiveChat}) = _RouterState; factory RouterState.fromJson(dynamic json) => diff --git a/lib/tick.dart b/lib/tick.dart index 99007e7..279de11 100644 --- a/lib/tick.dart +++ b/lib/tick.dart @@ -4,7 +4,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:veilid_support/veilid_support.dart'; -import 'init.dart'; import 'veilid_processor/veilid_processor.dart'; class BackgroundTicker extends StatefulWidget { @@ -53,10 +52,6 @@ class BackgroundTickerState extends State { } Future _onTick() async { - // Don't tick until we are initialized - if (!eventualInitialized.isCompleted) { - return; - } if (!ProcessorRepository .instance.processorConnectionState.isPublicInternetReady) { return; diff --git a/packages/async_tools/lib/async_tools.dart b/packages/async_tools/lib/async_tools.dart index 70f7b61..6438ff2 100644 --- a/packages/async_tools/lib/async_tools.dart +++ b/packages/async_tools/lib/async_tools.dart @@ -3,7 +3,9 @@ library; export 'src/async_tag_lock.dart'; export 'src/async_value.dart'; +export 'src/delayed_wait_set.dart'; export 'src/serial_future.dart'; export 'src/single_future.dart'; export 'src/single_state_processor.dart'; export 'src/single_stateless_processor.dart'; +export 'src/wait_set.dart'; diff --git a/packages/async_tools/lib/src/delayed_wait_set.dart b/packages/async_tools/lib/src/delayed_wait_set.dart new file mode 100644 index 0000000..75223dc --- /dev/null +++ b/packages/async_tools/lib/src/delayed_wait_set.dart @@ -0,0 +1,18 @@ +class DelayedWaitSet { + DelayedWaitSet(); + + void add(Future Function() closure) { + _closures.add(closure); + } + + Future call() async { + final futures = _closures.map((c) => c()).toList(); + _closures = []; + if (futures.isEmpty) { + return; + } + await futures.wait; + } + + List Function()> _closures = []; +} diff --git a/packages/async_tools/lib/src/single_future.dart b/packages/async_tools/lib/src/single_future.dart index 7e82e7c..11f6005 100644 --- a/packages/async_tools/lib/src/single_future.dart +++ b/packages/async_tools/lib/src/single_future.dart @@ -4,14 +4,14 @@ 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. +/// 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, @@ -40,3 +40,6 @@ void singleFuture(Object tag, Future Function() closure, } }()); } + +Future singleFuturePause(Object tag) async => _keys.lockTag(tag); +void singleFutureResume(Object tag) => _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 index 18798fa..098238c 100644 --- a/packages/async_tools/lib/src/single_state_processor.dart +++ b/packages/async_tools/lib/src/single_state_processor.dart @@ -22,15 +22,19 @@ class SingleStateProcessor { singleFuture(this, () async { var newState = newInputState; + var newClosure = closure; var done = false; while (!done) { - await closure(newState); + await newClosure(newState); // See if there's another state change to process - final next = _nextState; + final nextState = _nextState; + final nextClosure = _nextClosure; _nextState = null; - if (next != null) { - newState = next; + _nextClosure = null; + if (nextState != null) { + newState = nextState; + newClosure = nextClosure!; } else { done = true; } @@ -38,8 +42,26 @@ class SingleStateProcessor { }, onBusy: () { // Keep this state until we process again _nextState = newInputState; + _nextClosure = closure; }); } + Future pause() => singleFuturePause(this); + Future resume() async { + // Process any next state before resuming the singlefuture + try { + final nextState = _nextState; + final nextClosure = _nextClosure; + _nextState = null; + _nextClosure = null; + if (nextState != null) { + await nextClosure!(nextState); + } + } finally { + singleFutureResume(this); + } + } + State? _nextState; + Future Function(State)? _nextClosure; } diff --git a/packages/async_tools/lib/src/wait_set.dart b/packages/async_tools/lib/src/wait_set.dart new file mode 100644 index 0000000..ae79ba9 --- /dev/null +++ b/packages/async_tools/lib/src/wait_set.dart @@ -0,0 +1,18 @@ +class WaitSet { + WaitSet(); + + void add(Future Function() closure) { + _futures.add(Future.delayed(Duration.zero, closure)); + } + + Future call() async { + final futures = _futures; + _futures = []; + if (futures.isEmpty) { + return; + } + await futures.wait; + } + + List> _futures = []; +} diff --git a/packages/bloc_tools/lib/bloc_tools.dart b/packages/bloc_tools/lib/bloc_tools.dart index 4cc7304..507ae58 100644 --- a/packages/bloc_tools/lib/bloc_tools.dart +++ b/packages/bloc_tools/lib/bloc_tools.dart @@ -6,6 +6,6 @@ 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/state_map_follower.dart'; export 'src/stream_wrapper_cubit.dart'; export 'src/transformer_cubit.dart'; diff --git a/packages/bloc_tools/lib/src/async_transformer_cubit.dart b/packages/bloc_tools/lib/src/async_transformer_cubit.dart index fa6eacb..b8bbcab 100644 --- a/packages/bloc_tools/lib/src/async_transformer_cubit.dart +++ b/packages/bloc_tools/lib/src/async_transformer_cubit.dart @@ -14,6 +14,7 @@ class AsyncTransformerCubit extends Cubit> { _asyncTransform(input.state); _subscription = input.stream.listen(_asyncTransform); } + void _asyncTransform(AsyncValue newInputState) { _singleStateProcessor.updateState(newInputState, (newState) async { // Emit the transformed state diff --git a/packages/bloc_tools/lib/src/bloc_map_cubit.dart b/packages/bloc_tools/lib/src/bloc_map_cubit.dart index 2553c66..b99ae2b 100644 --- a/packages/bloc_tools/lib/src/bloc_map_cubit.dart +++ b/packages/bloc_tools/lib/src/bloc_map_cubit.dart @@ -3,6 +3,9 @@ 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:meta/meta.dart'; + +import 'state_map_follower.dart'; typedef BlocMapState = IMap; @@ -18,14 +21,15 @@ class _ItemEntry { // 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 +// V = 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> { +abstract class BlocMapCubit> + extends Cubit> + with StateMapFollowable, K, V> { BlocMapCubit() : _entries = {}, _tagLock = AsyncTagLock(), - super(IMap()); + super(IMap()); @override Future close() async { @@ -34,6 +38,13 @@ abstract class BlocMapCubit> await super.close(); } + @protected + @override + // ignore: unnecessary_overrides + void emit(BlocMapState state) { + super.emit(state); + } + Future add(MapEntry Function() create) { // Create new element final newElement = create(); @@ -56,7 +67,7 @@ abstract class BlocMapCubit> }); } - Future addState(K key, S value) => + Future addState(K key, V value) => _tagLock.protect(key, closure: () async { // Remove entry with the same key if it exists await _internalRemove(key); @@ -107,6 +118,10 @@ abstract class BlocMapCubit> return closure(entry.bloc); }); - final Map> _entries; + /// StateMapFollowable ///////////////////////// + @override + IMap getStateMap(BlocMapState s) => s; + + final Map> _entries; final AsyncTagLock _tagLock; } diff --git a/packages/bloc_tools/lib/src/state_follower.dart b/packages/bloc_tools/lib/src/state_follower.dart deleted file mode 100644 index eebe27b..0000000 --- a/packages/bloc_tools/lib/src/state_follower.dart +++ /dev/null @@ -1,63 +0,0 @@ -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 -// 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 = IMap(); - _updateFollow(initialInputState); - _subscription = stream.listen(_updateFollow); - } - - void followBloc>(B bloc) => - follow(initialInputState: bloc.state, stream: bloc.stream); - - Future close() async { - await _subscription.cancel(); - } - - IMap getStateMap(S state); - Future removeFromState(K key); - Future updateState(K key, V value); - - void _updateFollow(S newInputState) { - _singleStateProcessor.updateState(getStateMap(newInputState), - (newStateMap) async { - 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; - }); - } - - late IMap _lastInputStateMap; - late final StreamSubscription _subscription; - final SingleStateProcessor> _singleStateProcessor = - SingleStateProcessor(); -} diff --git a/packages/bloc_tools/lib/src/state_map_follower.dart b/packages/bloc_tools/lib/src/state_map_follower.dart new file mode 100644 index 0000000..4843f39 --- /dev/null +++ b/packages/bloc_tools/lib/src/state_map_follower.dart @@ -0,0 +1,125 @@ +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:meta/meta.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 +mixin StateMapFollower on Closable { + void follow(StateMapFollowable followable) { + assert(_following == null, 'can only follow one followable at a time'); + _following = followable; + _lastInputStateMap = IMap(); + _subscription = followable.registerFollower(this); + } + + Future unfollow() async { + await _subscription?.cancel(); + _subscription = null; + _following?.unregisterFollower(this); + _following = null; + } + + @override + @mustCallSuper + Future close() async { + await unfollow(); + await super.close(); + } + + Future removeFromState(K key); + Future updateState(K key, V value); + + void _updateFollow(IMap newInputState) { + final following = _following; + if (following == null) { + return; + } + _singleStateProcessor.updateState(newInputState, (newStateMap) async { + 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; + }); + } + + StateMapFollowable? _following; + late IMap _lastInputStateMap; + late StreamSubscription>? _subscription; + final SingleStateProcessor> _singleStateProcessor = + SingleStateProcessor(); +} + +/// Interface that allows a StateMapFollower to follow some other class's +/// state changes +abstract mixin class StateMapFollowable + implements StateStreamable { + IMap getStateMap(S state); + + StreamSubscription> registerFollower( + StateMapFollower follower) { + final stateMapTransformer = StreamTransformer>.fromHandlers( + handleData: (d, s) => s.add(getStateMap(d))); + + if (_followers.isEmpty) { + // start transforming stream + _transformedStream = stream.transform(stateMapTransformer); + } + _followers.add(follower); + follower._updateFollow(getStateMap(state)); + return _transformedStream!.listen((s) => follower._updateFollow(s)); + } + + void unregisterFollower(StateMapFollower follower) { + _followers.remove(follower); + if (_followers.isEmpty) { + // stop transforming stream + _transformedStream = null; + } + } + + Future syncFollowers(Future Function() closure) async { + // pause all followers + await _followers.map((f) => f._singleStateProcessor.pause()).wait; + + // run closure + final out = await closure(); + + // resume all followers and wait for current state map to be updated + final resumeState = getStateMap(state); + await _followers.map((f) async { + // Ensure the latest state has been updated + try { + f._updateFollow(resumeState); + } finally { + // Resume processing of the follower + await f._singleStateProcessor.resume(); + } + }).wait; + + return out; + } + + Stream>? _transformedStream; + final List> _followers = []; +} 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 index a1e3099..2b6736e 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/barrel.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/barrel.dart @@ -1,3 +1,4 @@ +export 'default_dht_record_cubit.dart'; 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/default_dht_record_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart new file mode 100644 index 0000000..1cf97d5 --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart @@ -0,0 +1,66 @@ +import 'dart:typed_data'; + +import '../../../veilid_support.dart'; + +/// Cubit that watches the default subkey value of a dhtrecord +class DefaultDHTRecordCubit extends DHTRecordCubit { + DefaultDHTRecordCubit({ + required super.open, + required T Function(List data) decodeState, + }) : super( + 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) => + (record) async { + final initialData = await record.get(); + if (initialData == null) { + return null; + } + return decodeState(initialData); + }; + + 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 || updatedata == null) { + final maybeData = await record.get(forceRefresh: true); + if (maybeData == null) { + return null; + } + data = maybeData; + } else { + data = updatedata; + } + final newState = decodeState(data); + return newState; + } + return null; + }; + + static WatchFunction _makeWatchFunction() => (record) async { + final defaultSubkey = record.subkeyOrDefault(-1); + await record.watch(subkeys: [ValueSubkeyRange.single(defaultSubkey)]); + }; + + Future refreshDefault() async { + await initWait(); + + final defaultSubkey = record.subkeyOrDefault(-1); + await refresh([ValueSubkeyRange(low: defaultSubkey, high: defaultSubkey)]); + } +} 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 fa98b9d..4ee52bf 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 @@ -16,12 +16,13 @@ class DHTRecordWatchChange extends Equatable { ///////////////////////////////////////////////// class DHTRecord { - DHTRecord( + DHTRecord._( {required VeilidRoutingContext routingContext, required SharedDHTRecordData sharedDHTRecordData, required int defaultSubkey, required KeyPair? writer, - required DHTRecordCrypto crypto}) + required DHTRecordCrypto crypto, + required this.debugName}) : _crypto = crypto, _routingContext = routingContext, _defaultSubkey = defaultSubkey, @@ -34,6 +35,7 @@ class DHTRecord { final int _defaultSubkey; final KeyPair? _writer; final DHTRecordCrypto _crypto; + final String debugName; bool _open; @internal 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 647c431..5cfa721 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 @@ -3,6 +3,7 @@ import 'dart:typed_data'; import 'package:async_tools/async_tools.dart'; import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; import '../../../veilid_support.dart'; @@ -20,7 +21,7 @@ class DHTRecordCubit extends Cubit> { }) : _wantsCloseRecord = false, _stateFunction = stateFunction, super(const AsyncValue.loading()) { - Future.delayed(Duration.zero, () async { + initWait.add(() async { // Do record open/create _record = await open(); _wantsCloseRecord = true; @@ -73,6 +74,7 @@ class DHTRecordCubit extends Cubit> { @override Future close() async { + await initWait(); await _record.cancelWatch(); await _subscription?.cancel(); _subscription = null; @@ -84,6 +86,8 @@ class DHTRecordCubit extends Cubit> { } Future refresh(List subkeys) async { + await initWait(); + var updateSubkeys = [...subkeys]; for (final skr in subkeys) { @@ -107,69 +111,11 @@ class DHTRecordCubit extends Cubit> { DHTRecord get record => _record; + @protected + final WaitSet initWait = WaitSet(); + StreamSubscription? _subscription; late DHTRecord _record; bool _wantsCloseRecord; final StateFunction _stateFunction; } - -// Cubit that watches the default subkey value of a dhtrecord -class DefaultDHTRecordCubit extends DHTRecordCubit { - DefaultDHTRecordCubit({ - required super.open, - required T Function(List data) decodeState, - }) : super( - 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) => - (record) async { - final initialData = await record.get(); - if (initialData == null) { - return null; - } - return decodeState(initialData); - }; - - 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 || updatedata == null) { - final maybeData = await record.get(forceRefresh: true); - if (maybeData == null) { - return null; - } - data = maybeData; - } else { - data = updatedata; - } - final newState = decodeState(data); - return newState; - } - 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/dht_record_pool.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.dart index b78109b..27e26b8 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 @@ -18,14 +18,16 @@ const int watchBackoffMultiplier = 2; const int watchBackoffMax = 30; /// Record pool that managed DHTRecords and allows for tagged deletion +/// String versions of keys due to IMap<> json unsupported in key @freezed class DHTRecordPoolAllocations with _$DHTRecordPoolAllocations { const factory DHTRecordPoolAllocations({ - required IMap> - childrenByParent, // String key due to IMap<> json unsupported in key - required IMap - parentByChild, // String key due to IMap<> json unsupported in key - required ISet rootRecords, + @Default(IMapConst>({})) + IMap> childrenByParent, + @Default(IMapConst({})) + IMap parentByChild, + @Default(ISetConst({})) ISet rootRecords, + @Default(IMapConst({})) IMap debugNames, }) = _DHTRecordPoolAllocations; factory DHTRecordPoolAllocations.fromJson(dynamic json) => @@ -92,10 +94,7 @@ class OpenedRecordInfo { class DHTRecordPool with TableDBBacked { DHTRecordPool._(Veilid veilid, VeilidRoutingContext routingContext) - : _state = DHTRecordPoolAllocations( - childrenByParent: IMap(), - parentByChild: IMap(), - rootRecords: ISet()), + : _state = const DHTRecordPoolAllocations(), _mutex = Mutex(), _opened = {}, _routingContext = routingContext, @@ -129,8 +128,7 @@ class DHTRecordPool with TableDBBacked { @override DHTRecordPoolAllocations valueFromJson(Object? obj) => obj != null ? DHTRecordPoolAllocations.fromJson(obj) - : DHTRecordPoolAllocations( - childrenByParent: IMap(), parentByChild: IMap(), rootRecords: ISet()); + : const DHTRecordPoolAllocations(); @override Object? valueToJson(DHTRecordPoolAllocations val) => val.toJson(); @@ -148,7 +146,8 @@ class DHTRecordPool with TableDBBacked { Veilid get veilid => _veilid; Future _recordCreateInner( - {required VeilidRoutingContext dhtctx, + {required String debugName, + required VeilidRoutingContext dhtctx, required DHTSchema schema, KeyPair? writer, TypedKey? parent}) async { @@ -169,13 +168,18 @@ class DHTRecordPool with TableDBBacked { _opened[recordDescriptor.key] = openedRecordInfo; // Register the dependency - await _addDependencyInner(parent, recordDescriptor.key); + await _addDependencyInner( + parent, + recordDescriptor.key, + debugName: debugName, + ); return openedRecordInfo; } Future _recordOpenInner( - {required VeilidRoutingContext dhtctx, + {required String debugName, + required VeilidRoutingContext dhtctx, required TypedKey recordKey, KeyPair? writer, TypedKey? parent}) async { @@ -198,7 +202,11 @@ class DHTRecordPool with TableDBBacked { _opened[recordDescriptor.key] = newOpenedRecordInfo; // Register the dependency - await _addDependencyInner(parent, recordKey); + await _addDependencyInner( + parent, + recordKey, + debugName: debugName, + ); return newOpenedRecordInfo; } @@ -218,7 +226,11 @@ class DHTRecordPool with TableDBBacked { } // Register the dependency - await _addDependencyInner(parent, recordKey); + await _addDependencyInner( + parent, + recordKey, + debugName: debugName, + ); return openedRecordInfo; } @@ -259,6 +271,18 @@ class DHTRecordPool with TableDBBacked { return allDeps.reversedView; } + void _debugPrintChildren(TypedKey recordKey, {List? allDeps}) { + allDeps ??= _collectChildrenInner(recordKey); + // ignore: avoid_print + print('Parent: $recordKey (${_state.debugNames[recordKey.toString()]})'); + for (final dep in allDeps) { + if (dep != recordKey) { + // ignore: avoid_print + print(' Child: $dep (${_state.debugNames[dep.toString()]})'); + } + } + } + Future _deleteInner(TypedKey recordKey) async { // Remove this child from parents await _removeDependenciesInner([recordKey]); @@ -269,7 +293,10 @@ class DHTRecordPool with TableDBBacked { await _mutex.protect(() async { final allDeps = _collectChildrenInner(recordKey); - assert(allDeps.singleOrNull == recordKey, 'must delete children first'); + if (allDeps.singleOrNull != recordKey) { + _debugPrintChildren(recordKey, allDeps: allDeps); + assert(false, 'must delete children first'); + } final ori = _opened[recordKey]; if (ori != null) { @@ -301,15 +328,17 @@ class DHTRecordPool with TableDBBacked { } } - Future _addDependencyInner(TypedKey? parent, TypedKey child) async { + Future _addDependencyInner(TypedKey? parent, TypedKey child, + {required String debugName}) async { assert(_mutex.isLocked, 'should be locked here'); if (parent == null) { if (_state.rootRecords.contains(child)) { // Dependency already added return; } - _state = await store( - _state.copyWith(rootRecords: _state.rootRecords.add(child))); + _state = await store(_state.copyWith( + rootRecords: _state.rootRecords.add(child), + debugNames: _state.debugNames.add(child.toJson(), debugName))); } else { final childrenOfParent = _state.childrenByParent[parent.toJson()] ?? ISet(); @@ -320,7 +349,8 @@ class DHTRecordPool with TableDBBacked { _state = await store(_state.copyWith( childrenByParent: _state.childrenByParent .add(parent.toJson(), childrenOfParent.add(child)), - parentByChild: _state.parentByChild.add(child.toJson(), parent))); + parentByChild: _state.parentByChild.add(child.toJson(), parent), + debugNames: _state.debugNames.add(child.toJson(), debugName))); } } @@ -331,7 +361,9 @@ class DHTRecordPool with TableDBBacked { for (final child in childList) { if (_state.rootRecords.contains(child)) { - state = state.copyWith(rootRecords: state.rootRecords.remove(child)); + state = state.copyWith( + rootRecords: state.rootRecords.remove(child), + debugNames: state.debugNames.remove(child.toJson())); } else { final parent = state.parentByChild[child.toJson()]; if (parent == null) { @@ -341,12 +373,14 @@ class DHTRecordPool with TableDBBacked { if (children.isEmpty) { state = state.copyWith( childrenByParent: state.childrenByParent.remove(parent.toJson()), - parentByChild: state.parentByChild.remove(child.toJson())); + parentByChild: state.parentByChild.remove(child.toJson()), + debugNames: state.debugNames.remove(child.toJson())); } else { state = state.copyWith( childrenByParent: state.childrenByParent.add(parent.toJson(), children), - parentByChild: state.parentByChild.remove(child.toJson())); + parentByChild: state.parentByChild.remove(child.toJson()), + debugNames: state.debugNames.remove(child.toJson())); } } } @@ -360,6 +394,7 @@ class DHTRecordPool with TableDBBacked { /// Create a root DHTRecord that has no dependent records Future create({ + required String debugName, VeilidRoutingContext? routingContext, TypedKey? parent, DHTSchema schema = const DHTSchema.dflt(oCnt: 1), @@ -371,9 +406,14 @@ class DHTRecordPool with TableDBBacked { final dhtctx = routingContext ?? _routingContext; final openedRecordInfo = await _recordCreateInner( - dhtctx: dhtctx, schema: schema, writer: writer, parent: parent); + debugName: debugName, + dhtctx: dhtctx, + schema: schema, + writer: writer, + parent: parent); - final rec = DHTRecord( + final rec = DHTRecord._( + debugName: debugName, routingContext: dhtctx, defaultSubkey: defaultSubkey, sharedDHTRecordData: openedRecordInfo.shared, @@ -391,7 +431,8 @@ class DHTRecordPool with TableDBBacked { /// Open a DHTRecord readonly Future openRead(TypedKey recordKey, - {VeilidRoutingContext? routingContext, + {required String debugName, + VeilidRoutingContext? routingContext, TypedKey? parent, int defaultSubkey = 0, DHTRecordCrypto? crypto}) async => @@ -399,9 +440,13 @@ class DHTRecordPool with TableDBBacked { final dhtctx = routingContext ?? _routingContext; final openedRecordInfo = await _recordOpenInner( - dhtctx: dhtctx, recordKey: recordKey, parent: parent); + debugName: debugName, + dhtctx: dhtctx, + recordKey: recordKey, + parent: parent); - final rec = DHTRecord( + final rec = DHTRecord._( + debugName: debugName, routingContext: dhtctx, defaultSubkey: defaultSubkey, sharedDHTRecordData: openedRecordInfo.shared, @@ -417,6 +462,7 @@ class DHTRecordPool with TableDBBacked { Future openWrite( TypedKey recordKey, KeyPair writer, { + required String debugName, VeilidRoutingContext? routingContext, TypedKey? parent, int defaultSubkey = 0, @@ -426,12 +472,14 @@ class DHTRecordPool with TableDBBacked { final dhtctx = routingContext ?? _routingContext; final openedRecordInfo = await _recordOpenInner( + debugName: debugName, dhtctx: dhtctx, recordKey: recordKey, parent: parent, writer: writer); - final rec = DHTRecord( + final rec = DHTRecord._( + debugName: debugName, routingContext: dhtctx, defaultSubkey: defaultSubkey, writer: writer, @@ -453,6 +501,7 @@ class DHTRecordPool with TableDBBacked { /// parent must be specified. Future openOwned( OwnedDHTRecordPointer ownedDHTRecordPointer, { + required String debugName, required TypedKey parent, VeilidRoutingContext? routingContext, int defaultSubkey = 0, @@ -461,6 +510,7 @@ class DHTRecordPool with TableDBBacked { openWrite( ownedDHTRecordPointer.recordKey, ownedDHTRecordPointer.owner, + debugName: debugName, routingContext: routingContext, parent: parent, defaultSubkey: defaultSubkey, diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.freezed.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.freezed.dart index 7419c31..e09fc0c 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.freezed.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.freezed.dart @@ -22,11 +22,12 @@ DHTRecordPoolAllocations _$DHTRecordPoolAllocationsFromJson( /// @nodoc mixin _$DHTRecordPoolAllocations { IMap>> get childrenByParent => - throw _privateConstructorUsedError; // String key due to IMap<> json unsupported in key + throw _privateConstructorUsedError; IMap> get parentByChild => - throw _privateConstructorUsedError; // String key due to IMap<> json unsupported in key + throw _privateConstructorUsedError; ISet> get rootRecords => throw _privateConstructorUsedError; + IMap get debugNames => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -43,7 +44,8 @@ abstract class $DHTRecordPoolAllocationsCopyWith<$Res> { $Res call( {IMap>> childrenByParent, IMap> parentByChild, - ISet> rootRecords}); + ISet> rootRecords, + IMap debugNames}); } /// @nodoc @@ -63,6 +65,7 @@ class _$DHTRecordPoolAllocationsCopyWithImpl<$Res, Object? childrenByParent = null, Object? parentByChild = null, Object? rootRecords = null, + Object? debugNames = null, }) { return _then(_value.copyWith( childrenByParent: null == childrenByParent @@ -77,6 +80,10 @@ class _$DHTRecordPoolAllocationsCopyWithImpl<$Res, ? _value.rootRecords : rootRecords // ignore: cast_nullable_to_non_nullable as ISet>, + debugNames: null == debugNames + ? _value.debugNames + : debugNames // ignore: cast_nullable_to_non_nullable + as IMap, ) as $Val); } } @@ -93,7 +100,8 @@ abstract class _$$DHTRecordPoolAllocationsImplCopyWith<$Res> $Res call( {IMap>> childrenByParent, IMap> parentByChild, - ISet> rootRecords}); + ISet> rootRecords, + IMap debugNames}); } /// @nodoc @@ -112,6 +120,7 @@ class __$$DHTRecordPoolAllocationsImplCopyWithImpl<$Res> Object? childrenByParent = null, Object? parentByChild = null, Object? rootRecords = null, + Object? debugNames = null, }) { return _then(_$DHTRecordPoolAllocationsImpl( childrenByParent: null == childrenByParent @@ -126,6 +135,10 @@ class __$$DHTRecordPoolAllocationsImplCopyWithImpl<$Res> ? _value.rootRecords : rootRecords // ignore: cast_nullable_to_non_nullable as ISet>, + debugNames: null == debugNames + ? _value.debugNames + : debugNames // ignore: cast_nullable_to_non_nullable + as IMap, )); } } @@ -134,25 +147,30 @@ class __$$DHTRecordPoolAllocationsImplCopyWithImpl<$Res> @JsonSerializable() class _$DHTRecordPoolAllocationsImpl implements _DHTRecordPoolAllocations { const _$DHTRecordPoolAllocationsImpl( - {required this.childrenByParent, - required this.parentByChild, - required this.rootRecords}); + {this.childrenByParent = const IMapConst>({}), + this.parentByChild = const IMapConst({}), + this.rootRecords = const ISetConst({}), + this.debugNames = const IMapConst({})}); factory _$DHTRecordPoolAllocationsImpl.fromJson(Map json) => _$$DHTRecordPoolAllocationsImplFromJson(json); @override + @JsonKey() final IMap>> childrenByParent; -// String key due to IMap<> json unsupported in key @override + @JsonKey() final IMap> parentByChild; -// String key due to IMap<> json unsupported in key @override + @JsonKey() final ISet> rootRecords; + @override + @JsonKey() + final IMap debugNames; @override String toString() { - return 'DHTRecordPoolAllocations(childrenByParent: $childrenByParent, parentByChild: $parentByChild, rootRecords: $rootRecords)'; + return 'DHTRecordPoolAllocations(childrenByParent: $childrenByParent, parentByChild: $parentByChild, rootRecords: $rootRecords, debugNames: $debugNames)'; } @override @@ -165,13 +183,15 @@ class _$DHTRecordPoolAllocationsImpl implements _DHTRecordPoolAllocations { (identical(other.parentByChild, parentByChild) || other.parentByChild == parentByChild) && const DeepCollectionEquality() - .equals(other.rootRecords, rootRecords)); + .equals(other.rootRecords, rootRecords) && + (identical(other.debugNames, debugNames) || + other.debugNames == debugNames)); } @JsonKey(ignore: true) @override int get hashCode => Object.hash(runtimeType, childrenByParent, parentByChild, - const DeepCollectionEquality().hash(rootRecords)); + const DeepCollectionEquality().hash(rootRecords), debugNames); @JsonKey(ignore: true) @override @@ -190,22 +210,23 @@ class _$DHTRecordPoolAllocationsImpl implements _DHTRecordPoolAllocations { abstract class _DHTRecordPoolAllocations implements DHTRecordPoolAllocations { const factory _DHTRecordPoolAllocations( - {required final IMap>> - childrenByParent, - required final IMap> parentByChild, - required final ISet> - rootRecords}) = _$DHTRecordPoolAllocationsImpl; + {final IMap>> childrenByParent, + final IMap> parentByChild, + final ISet> rootRecords, + final IMap debugNames}) = _$DHTRecordPoolAllocationsImpl; factory _DHTRecordPoolAllocations.fromJson(Map json) = _$DHTRecordPoolAllocationsImpl.fromJson; @override IMap>> get childrenByParent; - @override // String key due to IMap<> json unsupported in key + @override IMap> get parentByChild; - @override // String key due to IMap<> json unsupported in key + @override ISet> get rootRecords; @override + IMap get debugNames; + @override @JsonKey(ignore: true) _$$DHTRecordPoolAllocationsImplCopyWith<_$DHTRecordPoolAllocationsImpl> get copyWith => throw _privateConstructorUsedError; diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.g.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.g.dart index ea2a61b..fadd8b8 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.g.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.g.dart @@ -9,19 +9,29 @@ part of 'dht_record_pool.dart'; _$DHTRecordPoolAllocationsImpl _$$DHTRecordPoolAllocationsImplFromJson( Map json) => _$DHTRecordPoolAllocationsImpl( - childrenByParent: - IMap>>.fromJson( + childrenByParent: json['childrenByParent'] == null + ? const IMapConst>({}) + : IMap>>.fromJson( json['childrenByParent'] as Map, (value) => value as String, (value) => ISet>.fromJson(value, (value) => Typed.fromJson(value))), - parentByChild: IMap>.fromJson( - json['parentByChild'] as Map, - (value) => value as String, - (value) => Typed.fromJson(value)), - rootRecords: ISet>.fromJson( - json['rootRecords'], - (value) => Typed.fromJson(value)), + parentByChild: json['parentByChild'] == null + ? const IMapConst({}) + : IMap>.fromJson( + json['parentByChild'] as Map, + (value) => value as String, + (value) => Typed.fromJson(value)), + rootRecords: json['rootRecords'] == null + ? const ISetConst({}) + : ISet>.fromJson(json['rootRecords'], + (value) => Typed.fromJson(value)), + debugNames: json['debugNames'] == null + ? const IMapConst({}) + : IMap.fromJson( + json['debugNames'] as Map, + (value) => value as String, + (value) => value as String), ); Map _$$DHTRecordPoolAllocationsImplToJson( @@ -40,6 +50,10 @@ Map _$$DHTRecordPoolAllocationsImplToJson( 'rootRecords': instance.rootRecords.toJson( (value) => value, ), + 'debugNames': instance.debugNames.toJson( + (value) => value, + (value) => value, + ), }; _$OwnedDHTRecordPointerImpl _$$OwnedDHTRecordPointerImplFromJson( 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 5b115ae..dcf5cb4 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 @@ -28,7 +28,8 @@ class DHTShortArray { // if smplWriter is specified, uses a SMPL schema with a single writer // rather than the key owner static Future create( - {int stride = maxElements, + {required String debugName, + int stride = maxElements, VeilidRoutingContext? routingContext, TypedKey? parent, DHTRecordCrypto? crypto, @@ -42,6 +43,7 @@ class DHTShortArray { oCnt: 0, members: [DHTSchemaMember(mKey: smplWriter.key, mCnt: stride + 1)]); dhtRecord = await pool.create( + debugName: debugName, parent: parent, routingContext: routingContext, schema: schema, @@ -50,6 +52,7 @@ class DHTShortArray { } else { final schema = DHTSchema.dflt(oCnt: stride + 1); dhtRecord = await pool.create( + debugName: debugName, parent: parent, routingContext: routingContext, schema: schema, @@ -72,11 +75,15 @@ class DHTShortArray { } static Future openRead(TypedKey headRecordKey, - {VeilidRoutingContext? routingContext, + {required String debugName, + VeilidRoutingContext? routingContext, TypedKey? parent, DHTRecordCrypto? crypto}) async { final dhtRecord = await DHTRecordPool.instance.openRead(headRecordKey, - parent: parent, routingContext: routingContext, crypto: crypto); + debugName: debugName, + parent: parent, + routingContext: routingContext, + crypto: crypto); try { final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); await dhtShortArray._head.operate((head) => head._loadHead()); @@ -90,13 +97,17 @@ class DHTShortArray { static Future openWrite( TypedKey headRecordKey, KeyPair writer, { + required String debugName, VeilidRoutingContext? routingContext, TypedKey? parent, DHTRecordCrypto? crypto, }) async { final dhtRecord = await DHTRecordPool.instance.openWrite( headRecordKey, writer, - parent: parent, routingContext: routingContext, crypto: crypto); + debugName: debugName, + parent: parent, + routingContext: routingContext, + crypto: crypto); try { final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); await dhtShortArray._head.operate((head) => head._loadHead()); @@ -109,6 +120,7 @@ class DHTShortArray { static Future openOwned( OwnedDHTRecordPointer ownedDHTRecordPointer, { + required String debugName, required TypedKey parent, VeilidRoutingContext? routingContext, DHTRecordCrypto? crypto, @@ -116,6 +128,7 @@ class DHTShortArray { openWrite( ownedDHTRecordPointer.recordKey, ownedDHTRecordPointer.owner, + debugName: debugName, routingContext: routingContext, parent: parent, crypto: crypto, 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 c145a67..cd5ef23 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 @@ -17,13 +17,13 @@ class DHTShortArrayCubit extends Cubit> required T Function(List data) decodeElement, }) : _decodeElement = decodeElement, super(const BlocBusyState(AsyncValue.loading())) { - _initFuture = Future(() async { + _initWait.add(() async { // Open DHT record _shortArray = await open(); _wantsCloseRecord = true; // Make initial state update - unawaited(_refreshNoWait()); + await _refreshNoWait(); _subscription = await _shortArray.listen(_update); }); } @@ -42,7 +42,7 @@ class DHTShortArrayCubit extends Cubit> // } Future refresh({bool forceRefresh = false}) async { - await _initFuture; + await _initWait(); await _refreshNoWait(forceRefresh: forceRefresh); } @@ -75,7 +75,7 @@ class DHTShortArrayCubit extends Cubit> @override Future close() async { - await _initFuture; + await _initWait(); await _subscription?.cancel(); _subscription = null; if (_wantsCloseRecord) { @@ -85,24 +85,24 @@ class DHTShortArrayCubit extends Cubit> } Future operate(Future Function(DHTShortArrayRead) closure) async { - await _initFuture; + await _initWait(); return _shortArray.operate(closure); } Future<(R?, bool)> operateWrite( Future Function(DHTShortArrayWrite) closure) async { - await _initFuture; + await _initWait(); return _shortArray.operateWrite(closure); } Future operateWriteEventual( Future Function(DHTShortArrayWrite) closure, {Duration? timeout}) async { - await _initFuture; + await _initWait(); return _shortArray.operateWriteEventual(closure, timeout: timeout); } - late final Future _initFuture; + final WaitSet _initWait = WaitSet(); late final DHTShortArray _shortArray; final T Function(List data) _decodeElement; StreamSubscription? _subscription; 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 779cbcb..c01f10b 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 @@ -184,7 +184,7 @@ class _DHTShortArrayHead { final oldRecord = oldRecords[newKey]; if (oldRecord == null) { // Open the new record - final newRecord = await _openLinkedRecord(newKey); + final newRecord = await _openLinkedRecord(newKey, n); newRecords[newKey] = newRecord; updatedLinkedRecords.add(newRecord); } else { @@ -263,6 +263,7 @@ class _DHTShortArrayHead { oCnt: 0, members: [DHTSchemaMember(mKey: smplWriter.key, mCnt: _stride)]); final dhtRecord = await pool.create( + debugName: '${_headRecord.debugName}_linked_$recordNumber', parent: parent, routingContext: routingContext, schema: schema, @@ -279,17 +280,20 @@ class _DHTShortArrayHead { } /// Open a linked record for reading or writing, same as the head record - Future _openLinkedRecord(TypedKey recordKey) async { + Future _openLinkedRecord( + TypedKey recordKey, int recordNumber) async { final writer = _headRecord.writer; return (writer != null) ? await DHTRecordPool.instance.openWrite( recordKey, writer, + debugName: '${_headRecord.debugName}_linked_$recordNumber', parent: _headRecord.key, routingContext: _headRecord.routingContext, ) : await DHTRecordPool.instance.openRead( recordKey, + debugName: '${_headRecord.debugName}_linked_$recordNumber', parent: _headRecord.key, routingContext: _headRecord.routingContext, ); diff --git a/packages/veilid_support/lib/src/identity.dart b/packages/veilid_support/lib/src/identity.dart index e9ad6b6..5810bc5 100644 --- a/packages/veilid_support/lib/src/identity.dart +++ b/packages/veilid_support/lib/src/identity.dart @@ -132,7 +132,10 @@ extension IdentityMasterExtension on IdentityMaster { late final List accountRecordInfo; await (await pool.openRead(identityRecordKey, - parent: masterRecordKey, crypto: identityRecordCrypto)) + debugName: + 'IdentityMaster::readAccountsFromIdentity::IdentityRecord', + parent: masterRecordKey, + crypto: identityRecordCrypto)) .scope((identityRec) async { final identity = await identityRec.getJson(Identity.fromJson); if (identity == null) { @@ -161,14 +164,17 @@ extension IdentityMasterExtension on IdentityMaster { /////// Add account with profile to DHT // Open identity key for writing - veilidLoggy.debug('Opening master identity'); + veilidLoggy.debug('Opening identity record'); return (await pool.openWrite( identityRecordKey, identityWriter(identitySecret), + debugName: 'IdentityMaster::addAccountToIdentity::IdentityRecord', parent: masterRecordKey)) .scope((identityRec) async { // Create new account to insert into identity veilidLoggy.debug('Creating new account'); - return (await pool.create(parent: identityRec.key)) + return (await pool.create( + debugName: 'IdentityMaster::addAccountToIdentity::AccountRecord', + parent: identityRec.key)) .deleteScope((accountRec) async { final account = await createAccountCallback(accountRec.key); // Write account key @@ -222,11 +228,16 @@ class IdentityMasterWithSecrets { // IdentityMaster DHT record is public/unencrypted veilidLoggy.debug('Creating master identity record'); - return (await pool.create(crypto: const DHTRecordCryptoPublic())) + return (await pool.create( + debugName: + 'IdentityMasterWithSecrets::create::IdentityMasterRecord', + crypto: const DHTRecordCryptoPublic())) .deleteScope((masterRec) async { veilidLoggy.debug('Creating identity record'); // Identity record is private - return (await pool.create(parent: masterRec.key)) + return (await pool.create( + debugName: 'IdentityMasterWithSecrets::create::IdentityRecord', + parent: masterRec.key)) .scope((identityRec) async { // Make IdentityMaster final masterRecordKey = masterRec.key; @@ -282,7 +293,9 @@ Future openIdentityMaster( final pool = DHTRecordPool.instance; // IdentityMaster DHT record is public/unencrypted - return (await pool.openRead(identityMasterRecordKey)) + return (await pool.openRead(identityMasterRecordKey, + debugName: + 'IdentityMaster::openIdentityMaster::IdentityMasterRecord')) .deleteScope((masterRec) async { final identityMaster = (await masterRec.getJson(IdentityMaster.fromJson, forceRefresh: true))!; From 5da68b2d94a37b608625ee8ec7d0de4e3b05063d Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 4 Apr 2024 22:31:09 -0400 Subject: [PATCH 078/270] optimization work and crypto overrides for dht operations --- .../cubits/contact_invitation_list_cubit.dart | 7 ++ .../cubits/contact_request_inbox_cubit.dart | 6 +- .../cubits/waiting_invitation_cubit.dart | 7 +- .../src/dht_record/dht_record.dart | 93 ++++++++++++------- 4 files changed, 77 insertions(+), 36 deletions(-) diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index 1163a03..65cf2ef 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -126,6 +126,13 @@ class ContactInvitationListCubit .deleteScope((contactRequestInbox) async { // Store ContactRequest in owner subkey await contactRequestInbox.eventualWriteProtobuf(creq); + // Store an empty invitation response + await contactRequestInbox.eventualWriteBytes(Uint8List(0), + subkey: 1, + writer: contactRequestWriter, + crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair( + TypedKeyPair.fromKeyPair( + contactRequestInbox.key.kind, contactRequestWriter))); // Create ContactInvitation and SignedContactInvitation final cinv = proto.ContactInvitation() diff --git a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart index 25f49a2..a8de4f8 100644 --- a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart +++ b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart @@ -5,14 +5,16 @@ import '../../proto/proto.dart' as proto; // Watch subkey #1 of the ContactRequest record for accept/reject class ContactRequestInboxCubit - extends DefaultDHTRecordCubit { + extends DefaultDHTRecordCubit { ContactRequestInboxCubit( {required this.activeAccountInfo, required this.contactInvitationRecord}) : super( open: () => _open( activeAccountInfo: activeAccountInfo, contactInvitationRecord: contactInvitationRecord), - decodeState: proto.SignedContactResponse.fromBuffer); + decodeState: (buf) => buf.isEmpty + ? null + : proto.SignedContactResponse.fromBuffer(buf)); // ContactRequestInboxCubit.value( // {required super.record, diff --git a/lib/contact_invitation/cubits/waiting_invitation_cubit.dart b/lib/contact_invitation/cubits/waiting_invitation_cubit.dart index 04342e0..bed294a 100644 --- a/lib/contact_invitation/cubits/waiting_invitation_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitation_cubit.dart @@ -23,7 +23,7 @@ class InvitationStatus extends Equatable { } class WaitingInvitationCubit extends AsyncTransformerCubit { + proto.SignedContactResponse?> { WaitingInvitationCubit(ContactRequestInboxCubit super.input, {required ActiveAccountInfo activeAccountInfo, required proto.Account account, @@ -36,10 +36,13 @@ class WaitingInvitationCubit extends AsyncTransformerCubit> _transform( - proto.SignedContactResponse signedContactResponse, + proto.SignedContactResponse? signedContactResponse, {required ActiveAccountInfo activeAccountInfo, required proto.Account account, required proto.ContactInvitationRecord contactInvitationRecord}) async { + if (signedContactResponse == null) { + return const AsyncValue.loading(); + } final pool = DHTRecordPool.instance; final contactResponseBytes = Uint8List.fromList(signedContactResponse.contactResponse); 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 4ee52bf..0e23c5a 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 @@ -102,6 +102,7 @@ class DHTRecord { Future get( {int subkey = -1, + DHTRecordCrypto? crypto, bool forceRefresh = false, bool onlyUpdates = false}) async { subkey = subkeyOrDefault(subkey); @@ -114,17 +115,21 @@ class DHTRecord { if (onlyUpdates && lastSeq != null && valueData.seq <= lastSeq) { return null; } - final out = _crypto.decrypt(valueData.data, subkey); + final out = (crypto ?? _crypto).decrypt(valueData.data, subkey); _sharedDHTRecordData.subkeySeqCache[subkey] = valueData.seq; return out; } Future getJson(T Function(dynamic) fromJson, {int subkey = -1, + DHTRecordCrypto? crypto, bool forceRefresh = false, bool onlyUpdates = false}) async { final data = await get( - subkey: subkey, forceRefresh: forceRefresh, onlyUpdates: onlyUpdates); + subkey: subkey, + crypto: crypto, + forceRefresh: forceRefresh, + onlyUpdates: onlyUpdates); if (data == null) { return null; } @@ -134,10 +139,14 @@ class DHTRecord { Future getProtobuf( T Function(List i) fromBuffer, {int subkey = -1, + DHTRecordCrypto? crypto, bool forceRefresh = false, bool onlyUpdates = false}) async { final data = await get( - subkey: subkey, forceRefresh: forceRefresh, onlyUpdates: onlyUpdates); + subkey: subkey, + crypto: crypto, + forceRefresh: forceRefresh, + onlyUpdates: onlyUpdates); if (data == null) { return null; } @@ -145,14 +154,15 @@ class DHTRecord { } Future tryWriteBytes(Uint8List newValue, - {int subkey = -1}) async { + {int subkey = -1, DHTRecordCrypto? crypto, KeyPair? writer}) async { subkey = subkeyOrDefault(subkey); final lastSeq = _sharedDHTRecordData.subkeySeqCache[subkey]; - final encryptedNewValue = await _crypto.encrypt(newValue, subkey); + final encryptedNewValue = + await (crypto ?? _crypto).encrypt(newValue, subkey); // Set the new data if possible var newValueData = await _routingContext - .setDHTValue(key, subkey, encryptedNewValue, writer: _writer); + .setDHTValue(key, subkey, encryptedNewValue, writer: 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 @@ -177,7 +187,8 @@ class DHTRecord { } // Decrypt value to return it - final decryptedNewValue = await _crypto.decrypt(newValueData.data, subkey); + final decryptedNewValue = + await (crypto ?? _crypto).decrypt(newValueData.data, subkey); if (isUpdated) { DHTRecordPool.instance .processLocalValueChange(key, decryptedNewValue, subkey); @@ -185,17 +196,20 @@ class DHTRecord { return decryptedNewValue; } - Future eventualWriteBytes(Uint8List newValue, {int subkey = -1}) async { + Future eventualWriteBytes(Uint8List newValue, + {int subkey = -1, DHTRecordCrypto? crypto, KeyPair? writer}) async { subkey = subkeyOrDefault(subkey); final lastSeq = _sharedDHTRecordData.subkeySeqCache[subkey]; - final encryptedNewValue = await _crypto.encrypt(newValue, subkey); + final encryptedNewValue = + await (crypto ?? _crypto).encrypt(newValue, subkey); ValueData? newValueData; do { do { // Set the new data - newValueData = await _routingContext - .setDHTValue(key, subkey, encryptedNewValue, writer: _writer); + newValueData = await _routingContext.setDHTValue( + key, subkey, encryptedNewValue, + writer: writer ?? _writer); // Repeat if newer data on the network was found } while (newValueData != null); @@ -223,27 +237,32 @@ class DHTRecord { Future eventualUpdateBytes( Future Function(Uint8List? oldValue) update, - {int subkey = -1}) async { + {int subkey = -1, + DHTRecordCrypto? crypto, + KeyPair? writer}) async { subkey = subkeyOrDefault(subkey); // 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); + var oldValue = await get(subkey: subkey, crypto: crypto); do { // Update the data final updatedValue = await update(oldValue); // Try to write it back to the network - oldValue = await tryWriteBytes(updatedValue, subkey: subkey); + oldValue = await tryWriteBytes(updatedValue, + subkey: subkey, crypto: crypto, writer: writer); // Repeat update if newer data on the network was found } while (oldValue != null); } Future tryWriteJson(T Function(dynamic) fromJson, T newValue, - {int subkey = -1}) => - tryWriteBytes(jsonEncodeBytes(newValue), subkey: subkey).then((out) { + {int subkey = -1, DHTRecordCrypto? crypto, KeyPair? writer}) => + tryWriteBytes(jsonEncodeBytes(newValue), + subkey: subkey, crypto: crypto, writer: writer) + .then((out) { if (out == null) { return null; } @@ -252,30 +271,37 @@ class DHTRecord { Future tryWriteProtobuf( T Function(List) fromBuffer, T newValue, - {int subkey = -1}) => - tryWriteBytes(newValue.writeToBuffer(), subkey: subkey).then((out) { + {int subkey = -1, DHTRecordCrypto? crypto, KeyPair? writer}) => + tryWriteBytes(newValue.writeToBuffer(), + subkey: subkey, crypto: crypto, writer: writer) + .then((out) { if (out == null) { return null; } return fromBuffer(out); }); - Future eventualWriteJson(T newValue, {int subkey = -1}) => - eventualWriteBytes(jsonEncodeBytes(newValue), subkey: subkey); + Future eventualWriteJson(T newValue, + {int subkey = -1, DHTRecordCrypto? crypto, KeyPair? writer}) => + eventualWriteBytes(jsonEncodeBytes(newValue), + subkey: subkey, crypto: crypto, writer: writer); Future eventualWriteProtobuf(T newValue, - {int subkey = -1}) => - eventualWriteBytes(newValue.writeToBuffer(), subkey: subkey); + {int subkey = -1, DHTRecordCrypto? crypto, KeyPair? writer}) => + eventualWriteBytes(newValue.writeToBuffer(), + subkey: subkey, crypto: crypto, writer: writer); Future eventualUpdateJson( T Function(dynamic) fromJson, Future Function(T?) update, - {int subkey = -1}) => - eventualUpdateBytes(jsonUpdate(fromJson, update), subkey: subkey); + {int subkey = -1, DHTRecordCrypto? crypto, KeyPair? writer}) => + eventualUpdateBytes(jsonUpdate(fromJson, update), + subkey: subkey, crypto: crypto, writer: writer); Future eventualUpdateProtobuf( T Function(List) fromBuffer, Future Function(T?) update, - {int subkey = -1}) => - eventualUpdateBytes(protobufUpdate(fromBuffer, update), subkey: subkey); + {int subkey = -1, DHTRecordCrypto? crypto, KeyPair? writer}) => + eventualUpdateBytes(protobufUpdate(fromBuffer, update), + subkey: subkey, crypto: crypto, writer: writer); Future watch( {List? subkeys, @@ -291,10 +317,12 @@ class DHTRecord { } Future> listen( - Future Function( - DHTRecord record, Uint8List? data, List subkeys) - onUpdate, - {bool localChanges = true}) async { + Future Function( + DHTRecord record, Uint8List? data, List subkeys) + onUpdate, { + bool localChanges = true, + DHTRecordCrypto? crypto, + }) async { // Set up watch requirements watchController ??= StreamController.broadcast(onCancel: () { @@ -317,7 +345,8 @@ class DHTRecord { final changeData = change.data; data = changeData == null ? null - : await _crypto.decrypt(changeData, change.subkeys.first.low); + : await (crypto ?? _crypto) + .decrypt(changeData, change.subkeys.first.low); } await onUpdate(this, data, change.subkeys); }); @@ -366,7 +395,7 @@ class DHTRecord { overlappedFirstSubkey == updateFirstSubkey) ? data : null; - // Report only wathced subkeys + // Report only watched subkeys watchController?.add(DHTRecordWatchChange( local: local, data: updatedData, subkeys: overlappedSubkeys)); } From b3e9cbd4f30bca9ad7d7b78f4206e505d3c41607 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 5 Apr 2024 22:03:04 -0400 Subject: [PATCH 079/270] navigation cleanup --- assets/i18n/en.json | 28 ++-- lib/app.dart | 68 ++++---- .../cubits/single_contact_messages_cubit.dart | 4 +- lib/chat/views/chat_component.dart | 8 +- .../active_conversations_bloc_map_cubit.dart | 2 +- ...ve_single_contact_chat_bloc_map_cubit.dart | 4 +- lib/chat_list/cubits/chat_list_cubit.dart | 2 +- .../cubits/contact_invitation_list_cubit.dart | 4 +- .../cubits/waiting_invitation_cubit.dart | 2 +- .../views/contact_invitation_display.dart | 146 ++++++++---------- .../views/contact_invitation_item_widget.dart | 13 +- ...log.dart => create_invitation_dialog.dart} | 57 ++++--- ...ite_dialog.dart => invitation_dialog.dart} | 40 ++--- .../new_contact_invitation_bottom_sheet.dart | 12 +- ...alog.dart => paste_invitation_dialog.dart} | 25 ++- ...ialog.dart => scan_invitation_dialog.dart} | 46 +++--- lib/contact_invitation/views/views.dart | 8 +- lib/contacts/cubits/conversation_cubit.dart | 4 +- .../home_account_ready_shell.dart | 4 +- .../main_pager/account_page.dart | 4 +- .../main_pager/main_pager.dart | 2 +- lib/layout/splash.dart | 6 +- lib/tools/enter_password.dart | 4 +- lib/tools/enter_pin.dart | 4 +- lib/tools/pop_control.dart | 130 ++++++++++++++++ lib/tools/styled_dialog.dart | 58 +++++++ lib/tools/tools.dart | 2 + lib/tools/widget_helpers.dart | 36 ----- packages/async_tools/lib/src/async_value.dart | 26 +++- .../lib/src/async_transformer_cubit.dart | 2 +- pubspec.lock | 34 ++-- pubspec.yaml | 4 +- 32 files changed, 475 insertions(+), 314 deletions(-) rename lib/contact_invitation/views/{send_invite_dialog.dart => create_invitation_dialog.dart} (77%) rename lib/contact_invitation/views/{invite_dialog.dart => invitation_dialog.dart} (90%) rename lib/contact_invitation/views/{paste_invite_dialog.dart => paste_invitation_dialog.dart} (84%) rename lib/contact_invitation/views/{scan_invite_dialog.dart => scan_invitation_dialog.dart} (90%) create mode 100644 lib/tools/pop_control.dart create mode 100644 lib/tools/styled_dialog.dart diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 2b8a5ce..0a3baf7 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -60,16 +60,16 @@ }, "accounts_menu": { "invite_contact": "Invite Contact", - "create_invite": "Create Invite", - "scan_invite": "Scan Invite", - "paste_invite": "Paste Invite" + "create_invite": "Create Invitation", + "scan_invite": "Scan Invitation", + "paste_invite": "Paste Invitation" }, - "send_invite_dialog": { - "title": "Send Contact Invite", + "create_invitation_dialog": { + "title": "Create Contact Invitation", "connect_with_me": "Connect with me on VeilidChat!", - "enter_message_hint": "enter message for contact (optional)", + "enter_message_hint": "Enter message for contact (optional)", "message_to_contact": "Message to send with invitation (not encrypted)", - "generate": "Generate Invite", + "generate": "Generate Invitation", "message": "Message", "unlocked": "Unlocked", "pin": "PIN", @@ -85,23 +85,23 @@ "copy_invitation": "Copy Invitation", "invitation_copied": "Invitation Copied" }, - "invite_dialog": { + "invitation_dialog": { "message_from_contact": "Message from contact", "validating": "Validating...", - "failed_to_accept": "Failed to accept contact invite", - "failed_to_reject": "Failed to reject contact invite", + "failed_to_accept": "Failed to accept contact invitation", + "failed_to_reject": "Failed to reject contact invitation", "invalid_invitation": "Invalid invitation", - "protected_with_pin": "Contact invite is protected with a PIN", - "protected_with_password": "Contact invite is protected with a password", + "protected_with_pin": "Contact invitation is protected with a PIN", + "protected_with_password": "Contact invitation is protected with a password", "invalid_pin": "Invalid PIN", "invalid_password": "Invalid password" }, - "paste_invite_dialog": { + "paste_invitation_dialog": { "title": "Paste Contact Invite", "paste_invite_here": "Paste your contact invite here:", "paste": "Paste" }, - "scan_invite_dialog": { + "scan_invitation_dialog": { "title": "Scan Contact Invite", "instructions": "Position the contact invite QR code in the frame", "scan_qr_here": "Click here to scan a contact invite QR code:", diff --git a/lib/app.dart b/lib/app.dart index 2a8b07d..9dcab2f 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -38,37 +38,37 @@ class VeilidChatApp extends StatelessWidget { // Once init is done, we proceed with the app final localizationDelegate = LocalizedApp.of(context).delegate; return ThemeProvider( - initTheme: initialThemeData, - builder: (_, theme) => LocalizationProvider( - state: LocalizationProvider.of(context).state, - child: MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => ConnectionStateCubit( - ProcessorRepository.instance)), - BlocProvider( - create: (context) => - RouterCubit(AccountRepository.instance), - ), - BlocProvider( - create: (context) => - LocalAccountsCubit(AccountRepository.instance), - ), - BlocProvider( - create: (context) => - UserLoginsCubit(AccountRepository.instance), - ), - BlocProvider( - create: (context) => ActiveLocalAccountCubit( - AccountRepository.instance), - ), - BlocProvider( - create: (context) => - PreferencesCubit(PreferencesRepository.instance), - ) - ], - child: BackgroundTicker( - builder: (context) => MaterialApp.router( + initTheme: initialThemeData, + builder: (_, theme) => LocalizationProvider( + state: LocalizationProvider.of(context).state, + child: MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => + ConnectionStateCubit(ProcessorRepository.instance)), + BlocProvider( + create: (context) => + RouterCubit(AccountRepository.instance), + ), + BlocProvider( + create: (context) => + LocalAccountsCubit(AccountRepository.instance), + ), + BlocProvider( + create: (context) => + UserLoginsCubit(AccountRepository.instance), + ), + BlocProvider( + create: (context) => + ActiveLocalAccountCubit(AccountRepository.instance), + ), + BlocProvider( + create: (context) => + PreferencesCubit(PreferencesRepository.instance), + ) + ], + child: BackgroundTicker( + builder: (context) => MaterialApp.router( debugShowCheckedModeBanner: false, routerConfig: context.watch().router(), title: translate('app.title'), @@ -82,9 +82,9 @@ class VeilidChatApp extends StatelessWidget { supportedLocales: localizationDelegate.supportedLocales, locale: localizationDelegate.currentLocale, - ), - )), - )); + )), + )), + ); }); @override diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index a3a837d..45de817 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -135,7 +135,7 @@ class SingleContactMessagesCubit extends Cubit { // Called when the local messages list gets a change void _updateLocalMessagesState( BlocBusyState>> avmessages) { - final localMessages = avmessages.state.data?.value; + final localMessages = avmessages.state.asData?.value; if (localMessages == null) { return; } @@ -147,7 +147,7 @@ class SingleContactMessagesCubit extends Cubit { // Called when the remote messages list gets a change void _updateRemoteMessagesState( BlocBusyState>> avmessages) { - final remoteMessages = avmessages.state.data?.value; + final remoteMessages = avmessages.state.asData?.value; if (remoteMessages == null) { return; } diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 146185b..068bd75 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -43,12 +43,12 @@ class ChatComponent extends StatelessWidget { // Get all watched dependendies final activeAccountInfo = context.watch(); final accountRecordInfo = - context.watch().state.data?.value; + context.watch().state.asData?.value; if (accountRecordInfo == null) { return debugPage('should always have an account record here'); } final contactList = - context.watch().state.state.data?.value; + context.watch().state.state.asData?.value; if (contactList == null) { return debugPage('should always have a contact list here'); } @@ -58,7 +58,7 @@ class ChatComponent extends StatelessWidget { if (avconversation == null) { return waitingPage(); } - final conversation = avconversation.data?.value; + final conversation = avconversation.asData?.value; if (conversation == null) { return avconversation.buildNotData(); } @@ -140,7 +140,7 @@ class ChatComponent extends StatelessWidget { final textTheme = Theme.of(context).textTheme; final chatTheme = makeChatTheme(scale, textTheme); - final messages = _messagesState.data?.value; + final messages = _messagesState.asData?.value; if (messages == null) { return _messagesState.buildNotData(); } 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 54cbe38..b79191b 100644 --- a/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart +++ b/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart @@ -80,7 +80,7 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit updateState(TypedKey key, proto.Chat value) async { - final contactList = _contactListCubit.state.state.data?.value; + final contactList = _contactListCubit.state.state.asData?.value; if (contactList == null) { await addState(key, const AsyncValue.loading()); return; 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 index 7211150..7ebbdb7 100644 --- 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 @@ -56,7 +56,7 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit updateState( TypedKey key, AsyncValue value) async { // Get the contact object for this single contact chat - final contactList = _contactListCubit.state.state.data?.value; + final contactList = _contactListCubit.state.state.asData?.value; if (contactList == null) { await addState(key, const AsyncValue.loading()); return; @@ -71,7 +71,7 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit /// StateMapFollowable ///////////////////////// @override IMap getStateMap(ChatListCubitState state) { - final stateValue = state.state.data?.value; + final stateValue = state.state.asData?.value; if (stateValue == null) { return IMap(); } diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index 65cf2ef..03df901 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -245,7 +245,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.state.data!.value.indexWhere((cir) => + final isSelf = state.state.asData!.value.indexWhere((cir) => cir.contactRequestInbox.recordKey.toVeilid() == contactRequestInboxKey) != -1; @@ -310,7 +310,7 @@ class ContactInvitationListCubit @override IMap getStateMap( ContactInvitiationListState state) { - final stateValue = state.state.data?.value; + final stateValue = state.state.asData?.value; if (stateValue == null) { return IMap(); } diff --git a/lib/contact_invitation/cubits/waiting_invitation_cubit.dart b/lib/contact_invitation/cubits/waiting_invitation_cubit.dart index bed294a..07c2d63 100644 --- a/lib/contact_invitation/cubits/waiting_invitation_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitation_cubit.dart @@ -82,7 +82,7 @@ class WaitingInvitationCubit extends AsyncTransformerCubit createState() => - _ContactInvitationDisplayDialogState(); - @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(StringProperty('message', message)); - } -} - -class _ContactInvitationDisplayDialogState - extends State { - final focusNode = FocusNode(); - final formKey = GlobalKey(); - - @override - void initState() { - super.initState(); - } - - @override - void dispose() { - focusNode.dispose(); - super.dispose(); + properties + ..add(StringProperty('message', message)) + ..add(DiagnosticsProperty('modalContext', modalContext)); } String makeTextInvite(String message, Uint8List data) { @@ -72,61 +54,67 @@ class _ContactInvitationDisplayDialogState final cardsize = min(MediaQuery.of(context).size.shortestSide - 48.0, 400); - return Dialog( - backgroundColor: Colors.white, - child: ConstrainedBox( - constraints: BoxConstraints( - minWidth: cardsize, - maxWidth: cardsize, - minHeight: cardsize, - maxHeight: cardsize), - child: signedContactInvitationBytesV.when( - loading: buildProgressIndicator, - data: (data) => Form( - key: formKey, - child: Column(children: [ - FittedBox( - child: Text( + return PopControl( + dismissible: !signedContactInvitationBytesV.isLoading, + child: Dialog( + backgroundColor: Colors.white, + child: ConstrainedBox( + constraints: BoxConstraints( + minWidth: cardsize, + maxWidth: cardsize, + minHeight: cardsize, + maxHeight: cardsize), + child: signedContactInvitationBytesV.when( + loading: buildProgressIndicator, + data: (data) => Column(children: [ + FittedBox( + child: Text( + translate( + 'create_invitation_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(message, + softWrap: true, + style: textTheme.labelLarge! + .copyWith(color: Colors.black)) + .paddingAll(8), + ElevatedButton.icon( + icon: const Icon(Icons.copy), + label: Text(translate( + 'create_invitation_dialog.copy_invitation')), + onPressed: () async { + showInfoToast( + context, 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: errorPage))); + 'create_invitation_dialog.invitation_copied')); + await Clipboard.setData(ClipboardData( + text: makeTextInvite(message, data))); + }, + ).paddingAll(16), + ]), + error: errorPage)))); } - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('focusNode', focusNode)) - ..add(DiagnosticsProperty>('formKey', formKey)); + static Future show( + {required BuildContext context, + required InvitationGeneratorCubit Function(BuildContext) create, + required String message}) async { + await showPopControlDialog( + context: context, + builder: (context) => BlocProvider( + create: create, + child: ContactInvitationDisplayDialog._( + modalContext: context, + 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 e633390..8b81890 100644 --- a/lib/contact_invitation/views/contact_invitation_item_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_item_widget.dart @@ -105,15 +105,12 @@ class ContactInvitationItemWidget extends StatelessWidget { if (!context.mounted) { return; } - await showDialog( + await ContactInvitationDisplayDialog.show( context: context, - builder: (context) => BlocProvider( - create: (context) => InvitationGeneratorCubit - .value(Uint8List.fromList( - contactInvitationRecord.invitation)), - child: ContactInvitationDisplayDialog( - message: contactInvitationRecord.message, - ))); + message: contactInvitationRecord.message, + create: (context) => InvitationGeneratorCubit.value( + 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/create_invitation_dialog.dart similarity index 77% rename from lib/contact_invitation/views/send_invite_dialog.dart rename to lib/contact_invitation/views/create_invitation_dialog.dart index fb83254..be814a0 100644 --- a/lib/contact_invitation/views/send_invite_dialog.dart +++ b/lib/contact_invitation/views/create_invitation_dialog.dart @@ -13,17 +13,17 @@ import '../../account_manager/account_manager.dart'; import '../../tools/tools.dart'; import '../contact_invitation.dart'; -class SendInviteDialog extends StatefulWidget { - const SendInviteDialog({required this.modalContext, super.key}); +class CreateInvitationDialog extends StatefulWidget { + const CreateInvitationDialog._({required this.modalContext}); @override - SendInviteDialogState createState() => SendInviteDialogState(); + CreateInvitationDialogState createState() => CreateInvitationDialogState(); static Future show(BuildContext context) async { - await showStyledDialog( + await StyledDialog.show( context: context, - title: translate('send_invite_dialog.title'), - child: SendInviteDialog(modalContext: context)); + title: translate('create_invitation_dialog.title'), + child: CreateInvitationDialog._(modalContext: context)); } final BuildContext modalContext; @@ -36,9 +36,9 @@ class SendInviteDialog extends StatefulWidget { } } -class SendInviteDialogState extends State { +class CreateInvitationDialogState extends State { final _messageTextController = TextEditingController( - text: translate('send_invite_dialog.connect_with_me')); + text: translate('create_invitation_dialog.connect_with_me')); EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none; String _encryptionKey = ''; @@ -58,7 +58,7 @@ class SendInviteDialogState extends State { } Future _onPinEncryptionSelected(bool selected) async { - final description = translate('send_invite_dialog.pin_description'); + final description = translate('create_invitation_dialog.pin_description'); final pin = await showDialog( context: context, builder: (context) => @@ -87,7 +87,7 @@ class SendInviteDialogState extends State { return; } showErrorToast( - context, translate('send_invite_dialog.pin_does_not_match')); + context, translate('create_invitation_dialog.pin_does_not_match')); setState(() { _encryptionKeyType = EncryptionKeyType.none; _encryptionKey = ''; @@ -96,7 +96,8 @@ class SendInviteDialogState extends State { } Future _onPasswordEncryptionSelected(bool selected) async { - final description = translate('send_invite_dialog.password_description'); + final description = + translate('create_invitation_dialog.password_description'); final password = await showDialog( context: context, builder: (context) => EnterPasswordDialog(description: description)); @@ -123,8 +124,8 @@ class SendInviteDialogState extends State { if (!mounted) { return; } - showErrorToast( - context, translate('send_invite_dialog.password_does_not_match')); + showErrorToast(context, + translate('create_invitation_dialog.password_does_not_match')); setState(() { _encryptionKeyType = EncryptionKeyType.none; _encryptionKey = ''; @@ -145,13 +146,10 @@ class SendInviteDialogState extends State { message: _messageTextController.text, expiration: _expiration); - await showDialog( + await ContactInvitationDisplayDialog.show( context: context, - builder: (context) => BlocProvider( - create: (context) => InvitationGeneratorCubit(generator), - child: ContactInvitationDisplayDialog( - message: _messageTextController.text, - ))); + message: _messageTextController.text, + create: (context) => InvitationGeneratorCubit(generator)); navigator.pop(); } @@ -176,7 +174,7 @@ class SendInviteDialogState extends State { mainAxisSize: MainAxisSize.min, children: [ Text( - translate('send_invite_dialog.message_to_contact'), + translate('create_invitation_dialog.message_to_contact'), ).paddingAll(8), TextField( controller: _messageTextController, @@ -185,26 +183,27 @@ class SendInviteDialogState extends State { ], decoration: InputDecoration( border: const OutlineInputBorder(), - hintText: translate('send_invite_dialog.enter_message_hint'), - labelText: translate('send_invite_dialog.message')), + hintText: + translate('create_invitation_dialog.enter_message_hint'), + labelText: translate('create_invitation_dialog.message')), ).paddingAll(8), const SizedBox(height: 10), - Text(translate('send_invite_dialog.protect_this_invitation'), + Text(translate('create_invitation_dialog.protect_this_invitation'), style: textTheme.labelLarge) .paddingAll(8), Wrap(spacing: 5, children: [ ChoiceChip( - label: Text(translate('send_invite_dialog.unlocked')), + label: Text(translate('create_invitation_dialog.unlocked')), selected: _encryptionKeyType == EncryptionKeyType.none, onSelected: _onNoneEncryptionSelected, ), ChoiceChip( - label: Text(translate('send_invite_dialog.pin')), + label: Text(translate('create_invitation_dialog.pin')), selected: _encryptionKeyType == EncryptionKeyType.pin, onSelected: _onPinEncryptionSelected, ), ChoiceChip( - label: Text(translate('send_invite_dialog.password')), + label: Text(translate('create_invitation_dialog.password')), selected: _encryptionKeyType == EncryptionKeyType.password, onSelected: _onPasswordEncryptionSelected, ) @@ -216,13 +215,13 @@ class SendInviteDialogState extends State { child: ElevatedButton( onPressed: _onGenerateButtonPressed, child: Text( - translate('send_invite_dialog.generate'), + translate('create_invitation_dialog.generate'), ), ), ), - Text(translate('send_invite_dialog.note')).paddingAll(8), + Text(translate('create_invitation_dialog.note')).paddingAll(8), Text( - translate('send_invite_dialog.note_text'), + translate('create_invitation_dialog.note_text'), style: Theme.of(context).textTheme.bodySmall, ).paddingAll(8), ], diff --git a/lib/contact_invitation/views/invite_dialog.dart b/lib/contact_invitation/views/invitation_dialog.dart similarity index 90% rename from lib/contact_invitation/views/invite_dialog.dart rename to lib/contact_invitation/views/invitation_dialog.dart index 6e4580b..4e9e5e1 100644 --- a/lib/contact_invitation/views/invite_dialog.dart +++ b/lib/contact_invitation/views/invitation_dialog.dart @@ -11,8 +11,8 @@ import '../../contacts/contacts.dart'; import '../../tools/tools.dart'; import '../contact_invitation.dart'; -class InviteDialog extends StatefulWidget { - const InviteDialog( +class InvitationDialog extends StatefulWidget { + const InvitationDialog( {required this.modalContext, required this.onValidationCancelled, required this.onValidationSuccess, @@ -27,13 +27,13 @@ class InviteDialog extends StatefulWidget { final bool Function() inviteControlIsValid; final Widget Function( BuildContext context, - InviteDialogState dialogState, + InvitationDialogState dialogState, Future Function({required Uint8List inviteData}) validateInviteData) buildInviteControl; final BuildContext modalContext; @override - InviteDialogState createState() => InviteDialogState(); + InvitationDialogState createState() => InvitationDialogState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); @@ -49,7 +49,7 @@ class InviteDialog extends StatefulWidget { ..add(ObjectFlagProperty< Widget Function( BuildContext context, - InviteDialogState dialogState, + InvitationDialogState dialogState, Future Function({required Uint8List inviteData}) validateInviteData)>.has( 'buildInviteControl', buildInviteControl)) @@ -57,7 +57,7 @@ class InviteDialog extends StatefulWidget { } } -class InviteDialogState extends State { +class InvitationDialogState extends State { ValidContactInvitation? _validInvitation; bool _isValidating = false; bool _isAccepting = false; @@ -98,8 +98,8 @@ class InviteDialogState extends State { ); } } else { - if (context.mounted) { - showErrorToast(context, 'invite_dialog.failed_to_accept'); + if (mounted) { + showErrorToast(context, 'invitation_dialog.failed_to_accept'); } } } @@ -120,8 +120,8 @@ class InviteDialogState extends State { if (await validInvitation.reject()) { // do nothing right now } else { - if (context.mounted) { - showErrorToast(context, 'invite_dialog.failed_to_reject'); + if (mounted) { + showErrorToast(context, 'invitation_dialog.failed_to_reject'); } } } @@ -153,8 +153,8 @@ class InviteDialogState extends State { encryptionKey = ''; case EncryptionKeyType.pin: final description = - translate('invite_dialog.protected_with_pin'); - if (!context.mounted) { + translate('invitation_dialog.protected_with_pin'); + if (!mounted) { return null; } final pin = await showDialog( @@ -167,8 +167,8 @@ class InviteDialogState extends State { encryptionKey = pin; case EncryptionKeyType.password: final description = - translate('invite_dialog.protected_with_password'); - if (!context.mounted) { + translate('invitation_dialog.protected_with_password'); + if (!mounted) { return null; } final password = await showDialog( @@ -208,13 +208,13 @@ class InviteDialogState extends State { String errorText; switch (e.type) { case EncryptionKeyType.none: - errorText = translate('invite_dialog.invalid_invitation'); + errorText = translate('invitation_dialog.invalid_invitation'); case EncryptionKeyType.pin: - errorText = translate('invite_dialog.invalid_pin'); + errorText = translate('invitation_dialog.invalid_pin'); case EncryptionKeyType.password: - errorText = translate('invite_dialog.invalid_password'); + errorText = translate('invitation_dialog.invalid_password'); } - if (context.mounted) { + if (mounted) { showErrorToast(context, errorText); } setState(() { @@ -259,7 +259,7 @@ class InviteDialogState extends State { widget.buildInviteControl(context, this, _validateInviteData), if (_isValidating) Column(children: [ - Text(translate('invite_dialog.validating')) + Text(translate('invitation_dialog.validating')) .paddingLTRB(0, 0, 0, 16), buildProgressIndicator().paddingAll(16), ]).toCenter(), @@ -267,7 +267,7 @@ class InviteDialogState extends State { !_isValidating && widget.inviteControlIsValid()) Column(children: [ - Text(translate('invite_dialog.invalid_invitation')), + Text(translate('invitation_dialog.invalid_invitation')), const Icon(Icons.error) ]).paddingAll(16).toCenter(), if (_validInvitation != null && !_isValidating) 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 b0ba5c3..8228245 100644 --- a/lib/contact_invitation/views/new_contact_invitation_bottom_sheet.dart +++ b/lib/contact_invitation/views/new_contact_invitation_bottom_sheet.dart @@ -4,9 +4,9 @@ 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'; +import 'paste_invitation_dialog.dart'; +import 'scan_invitation_dialog.dart'; +import 'create_invitation_dialog.dart'; Widget newContactInvitationBottomSheetBuilder( BuildContext sheetContext, BuildContext context) { @@ -32,7 +32,7 @@ Widget newContactInvitationBottomSheetBuilder( IconButton( onPressed: () async { Navigator.pop(sheetContext); - await SendInviteDialog.show(context); + await CreateInvitationDialog.show(context); }, iconSize: 64, icon: const Icon(Icons.contact_page), @@ -43,7 +43,7 @@ Widget newContactInvitationBottomSheetBuilder( IconButton( onPressed: () async { Navigator.pop(sheetContext); - await ScanInviteDialog.show(context); + await ScanInvitationDialog.show(context); }, iconSize: 64, icon: const Icon(Icons.qr_code_scanner), @@ -54,7 +54,7 @@ Widget newContactInvitationBottomSheetBuilder( IconButton( onPressed: () async { Navigator.pop(sheetContext); - await PasteInviteDialog.show(context); + await PasteInvitationDialog.show(context); }, iconSize: 64, icon: const Icon(Icons.paste), diff --git a/lib/contact_invitation/views/paste_invite_dialog.dart b/lib/contact_invitation/views/paste_invitation_dialog.dart similarity index 84% rename from lib/contact_invitation/views/paste_invite_dialog.dart rename to lib/contact_invitation/views/paste_invitation_dialog.dart index bfd3fcd..50afb51 100644 --- a/lib/contact_invitation/views/paste_invite_dialog.dart +++ b/lib/contact_invitation/views/paste_invitation_dialog.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:typed_data'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; @@ -9,19 +8,19 @@ import 'package:veilid_support/veilid_support.dart'; import '../../theme/theme.dart'; import '../../tools/tools.dart'; -import 'invite_dialog.dart'; +import 'invitation_dialog.dart'; -class PasteInviteDialog extends StatefulWidget { - const PasteInviteDialog({required this.modalContext, super.key}); +class PasteInvitationDialog extends StatefulWidget { + const PasteInvitationDialog({required this.modalContext, super.key}); @override - PasteInviteDialogState createState() => PasteInviteDialogState(); + PasteInvitationDialogState createState() => PasteInvitationDialogState(); static Future show(BuildContext context) async { - await showStyledDialog( + await StyledDialog.show( context: context, - title: translate('paste_invite_dialog.title'), - child: PasteInviteDialog(modalContext: context)); + title: translate('paste_invitation_dialog.title'), + child: PasteInvitationDialog(modalContext: context)); } final BuildContext modalContext; @@ -34,7 +33,7 @@ class PasteInviteDialog extends StatefulWidget { } } -class PasteInviteDialogState extends State { +class PasteInvitationDialogState extends State { final _pasteTextController = TextEditingController(); @override @@ -89,7 +88,7 @@ class PasteInviteDialogState extends State { Widget buildInviteControl( BuildContext context, - InviteDialogState dialogState, + InvitationDialogState dialogState, Future Function({required Uint8List inviteData}) validateInviteData) { final theme = Theme.of(context); @@ -105,7 +104,7 @@ class PasteInviteDialogState extends State { return Column(mainAxisSize: MainAxisSize.min, children: [ Text( - translate('paste_invite_dialog.paste_invite_here'), + translate('paste_invitation_dialog.paste_invite_here'), ).paddingLTRB(0, 0, 0, 8), Container( constraints: const BoxConstraints(maxHeight: 200), @@ -122,7 +121,7 @@ class PasteInviteDialogState extends State { hintText: '--- BEGIN VEILIDCHAT CONTACT INVITE ----\n' 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n' '---- END VEILIDCHAT CONTACT INVITE -----\n', - //labelText: translate('paste_invite_dialog.paste') + //labelText: translate('paste_invitation_dialog.paste') ), )).paddingLTRB(0, 0, 0, 8) ]); @@ -131,7 +130,7 @@ class PasteInviteDialogState extends State { @override // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { - return InviteDialog( + return InvitationDialog( modalContext: widget.modalContext, onValidationCancelled: onValidationCancelled, onValidationSuccess: onValidationSuccess, diff --git a/lib/contact_invitation/views/scan_invite_dialog.dart b/lib/contact_invitation/views/scan_invitation_dialog.dart similarity index 90% rename from lib/contact_invitation/views/scan_invite_dialog.dart rename to lib/contact_invitation/views/scan_invitation_dialog.dart index 70f5b3b..3df0053 100644 --- a/lib/contact_invitation/views/scan_invite_dialog.dart +++ b/lib/contact_invitation/views/scan_invitation_dialog.dart @@ -14,7 +14,7 @@ import 'package:zxing2/qrcode.dart'; import '../../theme/theme.dart'; import '../../tools/tools.dart'; -import 'invite_dialog.dart'; +import 'invitation_dialog.dart'; class BarcodeOverlay extends CustomPainter { BarcodeOverlay({ @@ -103,17 +103,17 @@ class ScannerOverlay extends CustomPainter { bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } -class ScanInviteDialog extends StatefulWidget { - const ScanInviteDialog({required this.modalContext, super.key}); +class ScanInvitationDialog extends StatefulWidget { + const ScanInvitationDialog({required this.modalContext, super.key}); @override - ScanInviteDialogState createState() => ScanInviteDialogState(); + ScanInvitationDialogState createState() => ScanInvitationDialogState(); static Future show(BuildContext context) async { - await showStyledDialog( + await StyledDialog.show( context: context, - title: translate('scan_invite_dialog.title'), - child: ScanInviteDialog(modalContext: context)); + title: translate('scan_invitation_dialog.title'), + child: ScanInvitationDialog(modalContext: context)); } final BuildContext modalContext; @@ -126,7 +126,7 @@ class ScanInviteDialog extends StatefulWidget { } } -class ScanInviteDialogState extends State { +class ScanInvitationDialogState extends State { bool scanned = false; @override @@ -221,7 +221,8 @@ class ScanInviteDialogState extends State { height: 50, child: FittedBox( child: Text( - translate('scan_invite_dialog.instructions'), + translate( + 'scan_invitation_dialog.instructions'), overflow: TextOverflow.fade, style: Theme.of(context) .textTheme @@ -270,12 +271,12 @@ class ScanInviteDialogState extends State { } on MobileScannerException catch (e) { if (e.errorCode == MobileScannerErrorCode.permissionDenied) { showErrorToast( - context, translate('scan_invite_dialog.permission_error')); + context, translate('scan_invitation_dialog.permission_error')); } else { - showErrorToast(context, translate('scan_invite_dialog.error')); + showErrorToast(context, translate('scan_invitation_dialog.error')); } } on Exception catch (_) { - showErrorToast(context, translate('scan_invite_dialog.error')); + showErrorToast(context, translate('scan_invitation_dialog.error')); } return null; @@ -285,7 +286,8 @@ class ScanInviteDialogState extends State { final imageBytes = await Pasteboard.image; if (imageBytes == null) { if (context.mounted) { - showErrorToast(context, translate('scan_invite_dialog.not_an_image')); + showErrorToast( + context, translate('scan_invitation_dialog.not_an_image')); } return null; } @@ -293,8 +295,8 @@ class ScanInviteDialogState extends State { final image = img.decodeImage(imageBytes); if (image == null) { if (context.mounted) { - showErrorToast( - context, translate('scan_invite_dialog.could_not_decode_image')); + showErrorToast(context, + translate('scan_invitation_dialog.could_not_decode_image')); } return null; } @@ -319,7 +321,7 @@ class ScanInviteDialogState extends State { } on Exception catch (_) { if (context.mounted) { showErrorToast( - context, translate('scan_invite_dialog.not_a_valid_qr_code')); + context, translate('scan_invitation_dialog.not_a_valid_qr_code')); } return null; } @@ -327,7 +329,7 @@ class ScanInviteDialogState extends State { Widget buildInviteControl( BuildContext context, - InviteDialogState dialogState, + InvitationDialogState dialogState, Future Function({required Uint8List inviteData}) validateInviteData) { //final theme = Theme.of(context); @@ -339,7 +341,7 @@ class ScanInviteDialogState extends State { return Column(mainAxisSize: MainAxisSize.min, children: [ if (!scanned) Text( - translate('scan_invite_dialog.scan_qr_here'), + translate('scan_invitation_dialog.scan_qr_here'), ).paddingLTRB(0, 0, 0, 8), if (!scanned) Container( @@ -356,14 +358,14 @@ class ScanInviteDialogState extends State { await validateInviteData(inviteData: inviteData); } }, - child: Text(translate('scan_invite_dialog.scan'))), + child: Text(translate('scan_invitation_dialog.scan'))), ).paddingLTRB(0, 0, 0, 8) ]); } return Column(mainAxisSize: MainAxisSize.min, children: [ if (!scanned) Text( - translate('scan_invite_dialog.paste_qr_here'), + translate('scan_invitation_dialog.paste_qr_here'), ).paddingLTRB(0, 0, 0, 8), if (!scanned) Container( @@ -380,7 +382,7 @@ class ScanInviteDialogState extends State { }); } }, - child: Text(translate('scan_invite_dialog.paste'))), + child: Text(translate('scan_invitation_dialog.paste'))), ).paddingLTRB(0, 0, 0, 8) ]); } @@ -388,7 +390,7 @@ class ScanInviteDialogState extends State { @override // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { - return InviteDialog( + return InvitationDialog( modalContext: widget.modalContext, onValidationCancelled: onValidationCancelled, onValidationSuccess: onValidationSuccess, diff --git a/lib/contact_invitation/views/views.dart b/lib/contact_invitation/views/views.dart index d9f599b..3a7b8ec 100644 --- a/lib/contact_invitation/views/views.dart +++ b/lib/contact_invitation/views/views.dart @@ -1,8 +1,8 @@ export 'contact_invitation_display.dart'; export 'contact_invitation_item_widget.dart'; export 'contact_invitation_list_widget.dart'; -export 'invite_dialog.dart'; +export 'create_invitation_dialog.dart'; +export 'invitation_dialog.dart'; export 'new_contact_invitation_bottom_sheet.dart'; -export 'paste_invite_dialog.dart'; -export 'scan_invite_dialog.dart'; -export 'send_invite_dialog.dart'; +export 'paste_invitation_dialog.dart'; +export 'scan_invitation_dialog.dart'; diff --git a/lib/contacts/cubits/conversation_cubit.dart b/lib/contacts/cubits/conversation_cubit.dart index 3b5258b..7b2dde2 100644 --- a/lib/contacts/cubits/conversation_cubit.dart +++ b/lib/contacts/cubits/conversation_cubit.dart @@ -162,7 +162,7 @@ class ConversationCubit extends Cubit> { final deleteSet = DelayedWaitSet(); if (localConversationCubit != null) { - final data = localConversationCubit.state.data; + final data = localConversationCubit.state.asData; if (data == null) { log.warning('could not delete local conversation'); return false; @@ -180,7 +180,7 @@ class ConversationCubit extends Cubit> { } if (remoteConversationCubit != null) { - final data = remoteConversationCubit.state.data; + final data = remoteConversationCubit.state.asData; if (data == null) { log.warning('could not delete remote conversation'); return false; 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 70ccd46..c508568 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 @@ -75,7 +75,7 @@ class HomeAccountReadyShellState extends State { for (final entry in newState.entries) { final contactRequestInboxRecordKey = entry.key; - final invStatus = entry.value.data?.value; + final invStatus = entry.value.asData?.value; // Skip invitations that have not yet been accepted or rejected if (invStatus == null) { continue; @@ -109,7 +109,7 @@ class HomeAccountReadyShellState extends State { @override Widget build(BuildContext context) { - final account = context.watch().state.data?.value; + final account = context.watch().state.asData?.value; if (account == null) { return waitingPage(); } 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 b2c8384..b97ebe1 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 @@ -41,11 +41,11 @@ class AccountPageState extends State { final cilState = context.watch().state; final cilBusy = cilState.busy; final contactInvitationRecordList = - cilState.state.data?.value ?? const IListConst([]); + cilState.state.asData?.value ?? const IListConst([]); final ciState = context.watch().state; final ciBusy = ciState.busy; - final contactList = ciState.state.data?.value ?? const IListConst([]); + final contactList = ciState.state.asData?.value ?? const IListConst([]); return SizedBox( child: Column(children: [ 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 54f794c..5561cfd 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 @@ -121,7 +121,7 @@ class MainPagerState extends State with TickerProviderStateMixin { 'Scan Contact Invite', style: TextStyle(fontSize: 24), ), - content: ScanInviteDialog( + content: ScanInvitationDialog( modalContext: context, )); }); diff --git a/lib/layout/splash.dart b/lib/layout/splash.dart index aa22c0c..2113193 100644 --- a/lib/layout/splash.dart +++ b/lib/layout/splash.dart @@ -23,7 +23,9 @@ class _SplashState extends State { } @override - Widget build(BuildContext context) => DecoratedBox( + Widget build(BuildContext context) => PopScope( + canPop: false, + child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, @@ -49,5 +51,5 @@ class _SplashState extends State { 'assets/images/title.svg', )) ]))), - ); + )); } diff --git a/lib/tools/enter_password.dart b/lib/tools/enter_password.dart index 42880ee..3bce52d 100644 --- a/lib/tools/enter_password.dart +++ b/lib/tools/enter_password.dart @@ -17,7 +17,7 @@ class EnterPasswordDialog extends StatefulWidget { final String? description; @override - EnterPasswordDialogState createState() => EnterPasswordDialogState(); + State createState() => _EnterPasswordDialogState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -28,7 +28,7 @@ class EnterPasswordDialog extends StatefulWidget { } } -class EnterPasswordDialogState extends State { +class _EnterPasswordDialogState extends State { final passwordController = TextEditingController(); final focusNode = FocusNode(); final formKey = GlobalKey(); diff --git a/lib/tools/enter_pin.dart b/lib/tools/enter_pin.dart index 3128710..9961430 100644 --- a/lib/tools/enter_pin.dart +++ b/lib/tools/enter_pin.dart @@ -18,7 +18,7 @@ class EnterPinDialog extends StatefulWidget { final String? description; @override - EnterPinDialogState createState() => EnterPinDialogState(); + State createState() => _EnterPinDialogState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -29,7 +29,7 @@ class EnterPinDialog extends StatefulWidget { } } -class EnterPinDialogState extends State { +class _EnterPinDialogState extends State { final pinController = TextEditingController(); final focusNode = FocusNode(); final formKey = GlobalKey(); diff --git a/lib/tools/pop_control.dart b/lib/tools/pop_control.dart new file mode 100644 index 0000000..8ef984f --- /dev/null +++ b/lib/tools/pop_control.dart @@ -0,0 +1,130 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +class PopControl extends StatelessWidget { + const PopControl({ + required this.child, + required this.dismissible, + super.key, + }); + + void _doDismiss(NavigatorState navigator) { + if (!dismissible) { + return; + } + navigator.pop(); + } + + @override + // ignore: prefer_expression_function_bodies + Widget build(BuildContext context) { + final navigator = Navigator.of(context); + + final route = ModalRoute.of(context); + if (route != null && route is PopControlDialogRoute) { + route.barrierDismissible = dismissible; + } + + return PopScope( + canPop: false, + onPopInvoked: (didPop) { + if (didPop) { + return; + } + _doDismiss(navigator); + }, + child: child); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('dismissible', dismissible)); + } + + final bool dismissible; + final Widget child; +} + +class PopControlDialogRoute extends DialogRoute { + PopControlDialogRoute( + {required super.context, + required super.builder, + super.themes, + super.barrierColor = Colors.black54, + super.barrierDismissible, + super.barrierLabel, + super.useSafeArea, + super.settings, + super.anchorPoint, + super.traversalEdgeBehavior}) + : _barrierDismissible = barrierDismissible; + + @override + bool get barrierDismissible => _barrierDismissible; + + set barrierDismissible(bool d) { + _barrierDismissible = d; + } + + bool _barrierDismissible; +} + +bool _debugIsActive(BuildContext context) { + if (context is Element && !context.debugIsActive) { + throw FlutterError.fromParts([ + ErrorSummary('This BuildContext is no longer valid.'), + ErrorDescription( + 'The showPopControlDialog function context parameter is a ' + 'BuildContext that is no longer valid.'), + ErrorHint( + 'This can commonly occur when the showPopControlDialog function is ' + 'called after awaiting a Future. ' + 'In this situation the BuildContext might refer to a widget that has ' + 'already been disposed during the await. ' + 'Consider using a parent context instead.', + ), + ]); + } + return true; +} + +Future showPopControlDialog({ + required BuildContext context, + required WidgetBuilder builder, + bool barrierDismissible = true, + Color? barrierColor, + String? barrierLabel, + bool useSafeArea = true, + bool useRootNavigator = true, + RouteSettings? routeSettings, + Offset? anchorPoint, + TraversalEdgeBehavior? traversalEdgeBehavior, +}) { + assert(_debugIsActive(context), 'debug is active check'); + assert(debugCheckHasMaterialLocalizations(context), + 'check has material localizations'); + + final themes = InheritedTheme.capture( + from: context, + to: Navigator.of( + context, + rootNavigator: useRootNavigator, + ).context, + ); + + return Navigator.of(context, rootNavigator: useRootNavigator) + .push(PopControlDialogRoute( + context: context, + builder: builder, + barrierColor: barrierColor ?? Colors.black54, + barrierDismissible: barrierDismissible, + barrierLabel: barrierLabel, + useSafeArea: useSafeArea, + settings: routeSettings, + themes: themes, + anchorPoint: anchorPoint, + traversalEdgeBehavior: + traversalEdgeBehavior ?? TraversalEdgeBehavior.closedLoop, + )); +} diff --git a/lib/tools/styled_dialog.dart b/lib/tools/styled_dialog.dart new file mode 100644 index 0000000..25c3c0a --- /dev/null +++ b/lib/tools/styled_dialog.dart @@ -0,0 +1,58 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../theme/theme.dart'; + +class StyledDialog extends StatelessWidget { + const StyledDialog({required this.title, required this.child, super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final textTheme = theme.textTheme; + + return AlertDialog( + elevation: 0, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16)), + ), + contentPadding: const EdgeInsets.all(4), + backgroundColor: scale.primaryScale.border, + title: Text( + title, + style: textTheme.titleMedium, + textAlign: TextAlign.center, + ), + titlePadding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + content: DecoratedBox( + decoration: ShapeDecoration( + color: scale.primaryScale.border, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16))), + child: DecoratedBox( + decoration: ShapeDecoration( + color: scale.primaryScale.appBackground, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12))), + child: child.paddingAll(0)))); + } + + static Future show( + {required BuildContext context, + required String title, + required Widget child}) async => + showDialog( + context: context, + builder: (context) => StyledDialog(title: title, child: child)); + + final String title; + final Widget child; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(StringProperty('title', title)); + } +} diff --git a/lib/tools/tools.dart b/lib/tools/tools.dart index 0457d43..a5cefcf 100644 --- a/lib/tools/tools.dart +++ b/lib/tools/tools.dart @@ -3,10 +3,12 @@ export 'enter_password.dart'; export 'enter_pin.dart'; export 'loggy.dart'; export 'phono_byte.dart'; +export 'pop_control.dart'; export 'responsive.dart'; export 'scanner_error_widget.dart'; export 'shared_preferences.dart'; export 'state_logger.dart'; export 'stream_listenable.dart'; +export 'styled_dialog.dart'; export 'widget_helpers.dart'; export 'window_control.dart'; diff --git a/lib/tools/widget_helpers.dart b/lib/tools/widget_helpers.dart index b3bddb7..61485a7 100644 --- a/lib/tools/widget_helpers.dart +++ b/lib/tools/widget_helpers.dart @@ -164,42 +164,6 @@ Widget styledTitleContainer( ])); } -Future showStyledDialog( - {required BuildContext context, - required String title, - required Widget child}) async { - final theme = Theme.of(context); - final scale = theme.extension()!; - final textTheme = theme.textTheme; - - return showDialog( - context: context, - builder: (context) => AlertDialog( - elevation: 0, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(16)), - ), - contentPadding: const EdgeInsets.all(4), - backgroundColor: scale.primaryScale.border, - title: Text( - title, - style: textTheme.titleMedium, - textAlign: TextAlign.center, - ), - titlePadding: const EdgeInsets.fromLTRB(4, 4, 4, 4), - content: DecoratedBox( - decoration: ShapeDecoration( - color: scale.primaryScale.border, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16))), - child: DecoratedBox( - decoration: ShapeDecoration( - color: scale.primaryScale.appBackground, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12))), - child: child.paddingAll(0))))); -} - bool get isPlatformDark => WidgetsBinding.instance.platformDispatcher.platformBrightness == Brightness.dark; diff --git a/packages/async_tools/lib/src/async_value.dart b/packages/async_tools/lib/src/async_value.dart index aee070d..cc3bd9b 100644 --- a/packages/async_tools/lib/src/async_value.dart +++ b/packages/async_tools/lib/src/async_value.dart @@ -35,7 +35,7 @@ part 'async_value.freezed.dart'; /// ``` /// /// If a consumer of an [AsyncValue] does not care about the loading/error -/// state, consider using [data] to read the state: +/// state, consider using [asData] to read the state: /// /// ```dart /// Widget build(BuildContext context, ScopedReader watch) { @@ -127,7 +127,7 @@ abstract class AsyncValue with _$AsyncValue { /// 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 + /// As such reading [asData] still forces to handle the loading/error cases /// by having to check `data != null`. /// /// ## Why does [AsyncValue.data] return [AsyncData] instead of [T]? @@ -151,12 +151,32 @@ abstract class AsyncValue with _$AsyncValue { /// print(configs.data); // null, currently loading /// print(configs.data.value); // throws null exception /// ``` - AsyncData? get data => map( + AsyncData? get asData => map( data: (data) => data, loading: (_) => null, error: (_) => null, ); + bool get isData => asData != null; + + /// Check if this is loading + AsyncLoading? get asLoading => map( + data: (_) => null, + loading: (loading) => loading, + error: (_) => null, + ); + + bool get isLoading => asLoading != null; + + /// Check if this is an error + AsyncError? get asError => map( + data: (_) => null, + loading: (_) => null, + error: (e) => e, + ); + + bool get isError => asError != null; + /// Shorthand for [when] to handle only the `data` case. AsyncValue whenData(R Function(T value) cb) => when( data: (value) { diff --git a/packages/bloc_tools/lib/src/async_transformer_cubit.dart b/packages/bloc_tools/lib/src/async_transformer_cubit.dart index b8bbcab..f31659d 100644 --- a/packages/bloc_tools/lib/src/async_transformer_cubit.dart +++ b/packages/bloc_tools/lib/src/async_transformer_cubit.dart @@ -24,7 +24,7 @@ class AsyncTransformerCubit extends Cubit> { } else if (newState is AsyncError) { emit(AsyncValue.error(newState.error, newState.stackTrace)); } else { - final transformedState = await transform(newState.data!.value); + final transformedState = await transform(newState.asData!.value); emit(transformedState); } } on Exception catch (e, st) { diff --git a/pubspec.lock b/pubspec.lock index 751e310..b6e252f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -179,10 +179,10 @@ packages: dependency: transitive description: name: built_value - sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.9.1" + version: "8.9.2" cached_network_image: dependency: transitive description: @@ -219,18 +219,18 @@ packages: dependency: transitive description: name: camera_android - sha256: "1100e527b44a96906987a91ef78c8dacb539e34612a8058de89023380acf67f1" + sha256: ae5b9a996dfb8d77b02031b67f5500873d6402f33bd6a5283e932eef08542a51 url: "https://pub.dev" source: hosted - version: "0.10.8+18" + version: "0.10.9" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: "8b113e43ee4434c9244c03c905432a0d5956cedaded3cd7381abaab89ce50297" + sha256: "5d009ae48de1c8ab621b1c4496dadb6e2a83f3223b76c6e6a4a252414105f561" url: "https://pub.dev" source: hosted - version: "0.9.14+1" + version: "0.9.15" camera_platform_interface: dependency: transitive description: @@ -243,10 +243,10 @@ packages: dependency: transitive description: name: camera_web - sha256: f18ccfb33b2a7c49a52ad5aa3f07330b7422faaecbdfd9b9fe8e51182f6ad67d + sha256: "9e9aba2fbab77ce2472924196ff8ac4dd8f9126c4f9a3096171cd1d870d6b26c" url: "https://pub.dev" source: hosted - version: "0.3.2+4" + version: "0.3.3" change_case: dependency: "direct main" description: @@ -594,10 +594,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "57247f692f35f068cae297549a46a9a097100685c6780fe67177503eea5ed4e5" + sha256: "24f77b50776d4285cc4b3a1665bb79852714c09b878363efbe64788c179c4284" url: "https://pub.dev" source: hosted - version: "2.4.7" + version: "2.5.0" freezed_annotation: dependency: "direct main" description: @@ -977,10 +977,10 @@ packages: dependency: transitive description: name: pointycastle - sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" + sha256: "70fe966348fe08c34bf929582f1d8247d9d9408130723206472b4687227e4333" url: "https://pub.dev" source: hosted - version: "3.7.4" + version: "3.8.0" pool: dependency: transitive description: @@ -1270,10 +1270,10 @@ packages: dependency: transitive description: name: sqflite - sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 + sha256: "5ce2e1a15e822c3b4bfb5400455775e421da7098eed8adc8f26298ada7c9308c" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.3" sqflite_common: dependency: transitive description: @@ -1318,10 +1318,10 @@ packages: dependency: "direct main" description: name: stylish_bottom_bar - sha256: "54970e4753b4273239b6dea0d1175c56beabcf39b5c65df4cbf86f1b86568d2b" + sha256: ca72557a5bd8f44caae9017eb3a73002e9189d7a9d2fac598fa55be13724f32b url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.1.0" synchronized: dependency: transitive description: @@ -1504,7 +1504,7 @@ packages: path: "../veilid/veilid-flutter" relative: true source: path - version: "0.3.0" + version: "0.3.1" veilid_support: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 0be9f5d..3853a75 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -76,7 +76,7 @@ dependencies: split_view: ^3.2.1 stack_trace: ^1.11.1 stream_transform: ^2.1.0 - stylish_bottom_bar: ^1.0.3 + stylish_bottom_bar: ^1.1.0 uuid: ^4.3.3 veilid: # veilid: ^0.0.1 @@ -89,7 +89,7 @@ dependencies: dev_dependencies: build_runner: ^2.4.9 - freezed: ^2.4.7 + freezed: ^2.5.0 icons_launcher: ^2.1.7 json_serializable: ^6.7.1 lint_hard: ^4.0.0 From 8335e368765d58e1fa5ad08f6ff684ab682b6c03 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 6 Apr 2024 22:36:30 -0400 Subject: [PATCH 080/270] watch value fix, invitation fix, logging --- .../models/valid_contact_invitation.dart | 19 +-- .../views/invitation_dialog.dart | 108 ++++++++++-------- .../views/paste_invitation_dialog.dart | 18 ++- .../views/scan_invitation_dialog.dart | 8 +- lib/init.dart | 3 +- lib/tools/pop_control.dart | 5 +- .../src/dht_record/dht_record_pool.dart | 49 +++++++- 7 files changed, 133 insertions(+), 77 deletions(-) diff --git a/lib/contact_invitation/models/valid_contact_invitation.dart b/lib/contact_invitation/models/valid_contact_invitation.dart index 2a3d140..639af7f 100644 --- a/lib/contact_invitation/models/valid_contact_invitation.dart +++ b/lib/contact_invitation/models/valid_contact_invitation.dart @@ -73,13 +73,9 @@ class ValidContactInvitation { ..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'); - } + await contactRequestInbox + .eventualWriteProtobuf(signedContactResponse, subkey: 1); + return AcceptedContact( remoteProfile: _contactRequestPrivate.profile, remoteIdentity: _contactIdentityMaster, @@ -129,13 +125,8 @@ class ValidContactInvitation { ..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; - } + await contactRequestInbox.eventualWriteProtobuf(signedContactResponse, + subkey: 1); return true; }); } diff --git a/lib/contact_invitation/views/invitation_dialog.dart b/lib/contact_invitation/views/invitation_dialog.dart index 4e9e5e1..4d304ab 100644 --- a/lib/contact_invitation/views/invitation_dialog.dart +++ b/lib/contact_invitation/views/invitation_dialog.dart @@ -5,6 +5,7 @@ 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 '../../contacts/contacts.dart'; @@ -222,6 +223,16 @@ class InvitationDialogState extends State { _validInvitation = null; widget.onValidationFailed(); }); + } on VeilidAPIException { + final errorText = translate('invitation_dialog.invalid_invitation'); + if (mounted) { + showErrorToast(context, errorText); + } + setState(() { + _isValidating = false; + _validInvitation = null; + widget.onValidationFailed(); + }); } on Exception catch (e) { log.debug('exception: $e', e); setState(() { @@ -233,6 +244,48 @@ class InvitationDialogState extends State { } } + List _buildPreAccept() => [ + if (!_isValidating && _validInvitation == null) + widget.buildInviteControl(context, this, _validateInviteData), + if (_isValidating) + Column(children: [ + Text(translate('invitation_dialog.validating')) + .paddingLTRB(0, 0, 0, 16), + buildProgressIndicator().paddingAll(16), + ]).toCenter(), + if (_validInvitation == null && + !_isValidating && + widget.inviteControlIsValid()) + Column(children: [ + Text(translate('invitation_dialog.invalid_invitation')), + const Icon(Icons.error).paddingAll(16) + ]).toCenter(), + if (_validInvitation != null && !_isValidating) + Column(children: [ + Container( + constraints: const BoxConstraints(maxHeight: 64), + width: double.infinity, + child: + ProfileWidget(profile: _validInvitation!.remoteProfile)) + .paddingLTRB(0, 0, 0, 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton.icon( + icon: const Icon(Icons.check_circle), + label: Text(translate('button.accept')), + onPressed: _onAccept, + ).paddingLTRB(0, 0, 8, 0), + ElevatedButton.icon( + icon: const Icon(Icons.cancel), + label: Text(translate('button.reject')), + onPressed: _onReject, + ).paddingLTRB(8, 0, 0, 0) + ], + ), + ]) + ]; + @override // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { @@ -240,63 +293,20 @@ class InvitationDialogState extends State { // final scale = theme.extension()!; // final textTheme = theme.textTheme; // final height = MediaQuery.of(context).size.height; + final dismissible = !_isAccepting && !_isValidating; - if (_isAccepting) { - return SizedBox( - height: 300, - width: 300, - child: buildProgressIndicator().toCenter()) - .paddingAll(16); - } - return ConstrainedBox( + final dialog = ConstrainedBox( constraints: const BoxConstraints(maxHeight: 400, maxWidth: 400), child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, - children: [ - widget.buildInviteControl(context, this, _validateInviteData), - if (_isValidating) - Column(children: [ - Text(translate('invitation_dialog.validating')) - .paddingLTRB(0, 0, 0, 16), - buildProgressIndicator().paddingAll(16), - ]).toCenter(), - if (_validInvitation == null && - !_isValidating && - widget.inviteControlIsValid()) - Column(children: [ - Text(translate('invitation_dialog.invalid_invitation')), - const Icon(Icons.error) - ]).paddingAll(16).toCenter(), - if (_validInvitation != null && !_isValidating) - Column(children: [ - Container( - constraints: const BoxConstraints(maxHeight: 64), - width: double.infinity, - child: ProfileWidget( - profile: _validInvitation!.remoteProfile)) - .paddingLTRB(0, 0, 0, 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - ElevatedButton.icon( - icon: const Icon(Icons.check_circle), - label: Text(translate('button.accept')), - onPressed: _onAccept, - ), - ElevatedButton.icon( - icon: const Icon(Icons.cancel), - label: Text(translate('button.reject')), - onPressed: _onReject, - ) - ], - ), - ]) - ]), + children: _isAccepting + ? [buildProgressIndicator().paddingAll(16)] + : _buildPreAccept()), ), ); + return PopControl(dismissible: dismissible, child: dialog); } @override diff --git a/lib/contact_invitation/views/paste_invitation_dialog.dart b/lib/contact_invitation/views/paste_invitation_dialog.dart index 50afb51..75a6208 100644 --- a/lib/contact_invitation/views/paste_invitation_dialog.dart +++ b/lib/contact_invitation/views/paste_invitation_dialog.dart @@ -17,10 +17,13 @@ class PasteInvitationDialog extends StatefulWidget { PasteInvitationDialogState createState() => PasteInvitationDialogState(); static Future show(BuildContext context) async { - await StyledDialog.show( + final modalContext = context; + + await showPopControlDialog( context: context, - title: translate('paste_invitation_dialog.title'), - child: PasteInvitationDialog(modalContext: context)); + builder: (context) => StyledDialog( + title: translate('paste_invitation_dialog.title'), + child: PasteInvitationDialog(modalContext: modalContext))); } final BuildContext modalContext; @@ -67,8 +70,13 @@ class PasteInvitationDialogState extends State { .sublist(firstline, lastline) .join() .replaceAll(RegExp(r'[^A-Za-z0-9\-_]'), ''); - final inviteData = base64UrlNoPadDecode(inviteDataBase64); + var inviteData = Uint8List(0); + try { + inviteData = base64UrlNoPadDecode(inviteDataBase64); + } on Exception { + // + } await validateInviteData(inviteData: inviteData); } @@ -105,7 +113,7 @@ class PasteInvitationDialogState extends State { return Column(mainAxisSize: MainAxisSize.min, children: [ Text( translate('paste_invitation_dialog.paste_invite_here'), - ).paddingLTRB(0, 0, 0, 8), + ).paddingLTRB(0, 0, 0, 16), Container( constraints: const BoxConstraints(maxHeight: 200), child: TextField( diff --git a/lib/contact_invitation/views/scan_invitation_dialog.dart b/lib/contact_invitation/views/scan_invitation_dialog.dart index 3df0053..03f6101 100644 --- a/lib/contact_invitation/views/scan_invitation_dialog.dart +++ b/lib/contact_invitation/views/scan_invitation_dialog.dart @@ -110,10 +110,12 @@ class ScanInvitationDialog extends StatefulWidget { ScanInvitationDialogState createState() => ScanInvitationDialogState(); static Future show(BuildContext context) async { - await StyledDialog.show( + final modalContext = context; + await showPopControlDialog( context: context, - title: translate('scan_invitation_dialog.title'), - child: ScanInvitationDialog(modalContext: context)); + builder: (context) => StyledDialog( + title: translate('scan_invitation_dialog.title'), + child: ScanInvitationDialog(modalContext: modalContext))); } final BuildContext modalContext; diff --git a/lib/init.dart b/lib/init.dart index 24ec467..cd01f97 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -24,7 +24,8 @@ class VeilidChatGlobalInit { await ProcessorRepository.instance.startup(); // DHT Record Pool - await DHTRecordPool.init(); + await DHTRecordPool.init( + logger: (message) => log.debug('DHTRecordPool: $message')); } // Initialize repositories diff --git a/lib/tools/pop_control.dart b/lib/tools/pop_control.dart index 8ef984f..29dc562 100644 --- a/lib/tools/pop_control.dart +++ b/lib/tools/pop_control.dart @@ -22,7 +22,9 @@ class PopControl extends StatelessWidget { final route = ModalRoute.of(context); if (route != null && route is PopControlDialogRoute) { - route.barrierDismissible = dismissible; + WidgetsBinding.instance.addPostFrameCallback((_) { + route.barrierDismissible = dismissible; + }); } return PopScope( @@ -65,6 +67,7 @@ class PopControlDialogRoute extends DialogRoute { set barrierDismissible(bool d) { _barrierDismissible = d; + changedInternalState(); } bool _barrierDismissible; 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 27e26b8..2c37cdd 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 @@ -17,6 +17,8 @@ part 'dht_record.dart'; const int watchBackoffMultiplier = 2; const int watchBackoffMax = 30; +typedef DHTRecordPoolLogger = void Function(String message); + /// Record pool that managed DHTRecords and allows for tagged deletion /// String versions of keys due to IMap<> json unsupported in key @freezed @@ -90,6 +92,12 @@ class OpenedRecordInfo { defaultRoutingContext: defaultRoutingContext); SharedDHTRecordData shared; Set records = {}; + + String get debugNames { + final r = records.toList() + ..sort((a, b) => a.key.toString().compareTo(b.key.toString())); + return '[${r.map((x) => x.debugName).join(',')}]'; + } } class DHTRecordPool with TableDBBacked { @@ -100,6 +108,9 @@ class DHTRecordPool with TableDBBacked { _routingContext = routingContext, _veilid = veilid; + // Logger + DHTRecordPoolLogger? _logger; + // Persistent DHT record list DHTRecordPoolAllocations _state; // Create/open Mutex @@ -136,15 +147,21 @@ class DHTRecordPool with TableDBBacked { static DHTRecordPool get instance => _singleton!; - static Future init() async { + static Future init({DHTRecordPoolLogger? logger}) async { final routingContext = await Veilid.instance.routingContext(); final globalPool = DHTRecordPool._(Veilid.instance, routingContext); - globalPool._state = await globalPool.load(); + globalPool + .._logger = logger + .._state = await globalPool.load(); _singleton = globalPool; } Veilid get veilid => _veilid; + void log(String message) { + _logger?.call(message); + } + Future _recordCreateInner( {required String debugName, required VeilidRoutingContext dhtctx, @@ -156,6 +173,8 @@ class DHTRecordPool with TableDBBacked { // Create the record final recordDescriptor = await dhtctx.createDHTRecord(schema); + log('createDHTRecord: debugName=$debugName key=${recordDescriptor.key}'); + // Reopen if a writer is specified to ensure // we switch the default writer if (writer != null) { @@ -185,6 +204,8 @@ class DHTRecordPool with TableDBBacked { TypedKey? parent}) async { assert(_mutex.isLocked, 'should be locked here'); + log('openDHTRecord: debugName=$debugName key=$recordKey'); + // If we are opening a key that already exists // make sure we are using the same parent if one was specified _validateParentInner(parent, recordKey); @@ -238,6 +259,9 @@ class DHTRecordPool with TableDBBacked { Future _recordClosed(DHTRecord record) async { await _mutex.protect(() async { final key = record.key; + + log('closeDHTRecord: debugName=${record.debugName} key=$key'); + final openedRecordInfo = _opened[key]; if (openedRecordInfo == null || !openedRecordInfo.records.remove(record)) { @@ -284,6 +308,8 @@ class DHTRecordPool with TableDBBacked { } Future _deleteInner(TypedKey recordKey) async { + log('deleteDHTRecord: key=$recordKey'); + // Remove this child from parents await _removeDependenciesInner([recordKey]); await _routingContext.deleteDHTRecord(recordKey); @@ -676,9 +702,14 @@ class DHTRecordPool with TableDBBacked { var success = false; try { success = await dhtctx.cancelDHTWatch(openedRecordKey); + + log('cancelDHTWatch: key=$openedRecordKey, success=$success, ' + 'debugNames=${openedRecordInfo.debugNames}'); + openedRecordInfo.shared.needsWatchStateUpdate = false; - } on VeilidAPIException { + } on VeilidAPIException catch (e) { // Failed to cancel DHT watch, try again next tick + log('Exception in watch cancel: $e'); } return success; }); @@ -687,12 +718,21 @@ class DHTRecordPool with TableDBBacked { // Record needs new watch var success = false; try { + final subkeys = watchState.subkeys?.toList(); + final count = watchState.count; + final expiration = watchState.expiration; + final realExpiration = await dhtctx.watchDHTValues( openedRecordKey, subkeys: watchState.subkeys?.toList(), count: watchState.count, expiration: watchState.expiration); + log('watchDHTValues: key=$openedRecordKey, subkeys=$subkeys, ' + 'count=$count, expiration=$expiration, ' + 'realExpiration=$realExpiration, ' + 'debugNames=${openedRecordInfo.debugNames}'); + // Update watch states with real expiration if (realExpiration.value != BigInt.zero) { openedRecordInfo.shared.needsWatchStateUpdate = false; @@ -700,8 +740,9 @@ class DHTRecordPool with TableDBBacked { openedRecordInfo.records, realExpiration); success = true; } - } on VeilidAPIException { + } on VeilidAPIException catch (e) { // Failed to cancel DHT watch, try again next tick + log('Exception in watch update: $e'); } return success; }); From b7f7258c7046387a44ffc3c259b4e23c92ae33ab Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 7 Apr 2024 16:19:45 -0400 Subject: [PATCH 081/270] optimizations --- .../cubits/single_contact_messages_cubit.dart | 42 ++----------------- lib/theme/models/radix_generator.dart | 6 +++ lib/veilid_processor/views/developer.dart | 11 +++++ .../src/dht_record/dht_record_pool.dart | 34 +++++++++++++++ .../dht_short_array/dht_short_array_head.dart | 22 +++++++--- .../dht_short_array/dht_short_array_read.dart | 5 ++- .../dht_short_array_write.dart | 24 ++++++++--- 7 files changed, 93 insertions(+), 51 deletions(-) diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 45de817..1a2a604 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -10,8 +10,7 @@ import '../../account_manager/account_manager.dart'; import '../../proto/proto.dart' as proto; class _SingleContactMessageQueueEntry { - _SingleContactMessageQueueEntry({this.localMessages, this.remoteMessages}); - IList? localMessages; + _SingleContactMessageQueueEntry({this.remoteMessages}); IList? remoteMessages; } @@ -96,9 +95,6 @@ class SingleContactMessagesCubit extends Cubit { parent: _localConversationRecordKey, crypto: _messagesCrypto), decodeElement: proto.Message.fromBuffer); - _localSubscription = - _localMessagesCubit!.stream.listen(_updateLocalMessagesState); - _updateLocalMessagesState(_localMessagesCubit!.state); } // Open remote messages key @@ -132,18 +128,6 @@ class SingleContactMessagesCubit extends Cubit { _updateReconciledChatState(_reconciledChatMessagesCubit!.state); } - // Called when the local messages list gets a change - void _updateLocalMessagesState( - BlocBusyState>> avmessages) { - final localMessages = avmessages.state.asData?.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) { @@ -232,12 +216,6 @@ class SingleContactMessagesCubit extends Cubit { // Merge remote and local messages into the reconciled chat log await reconciledChatMessagesCubit .operateWrite((reconciledMessagesWriter) async { - // xxx for now, keep two lists, but can probable simplify this out soon - if (entry.localMessages != null) { - await _mergeMessagesInner( - reconciledMessagesWriter: reconciledMessagesWriter, - messages: entry.localMessages!); - } if (entry.remoteMessages != null) { await _mergeMessagesInner( reconciledMessagesWriter: reconciledMessagesWriter, @@ -246,24 +224,12 @@ class SingleContactMessagesCubit extends Cubit { }); } - // Force refresh of messages - Future refresh() async { - await _initWait(); - - 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 _initWait(); + await _reconciledChatMessagesCubit!.operateWrite((writer) => + _mergeMessagesInner( + reconciledMessagesWriter: writer, messages: [message].toIList())); await _localMessagesCubit! .operateWrite((writer) => writer.tryAddItem(message.writeToBuffer())); } diff --git a/lib/theme/models/radix_generator.dart b/lib/theme/models/radix_generator.dart index 609f923..e3b0a5a 100644 --- a/lib/theme/models/radix_generator.dart +++ b/lib/theme/models/radix_generator.dart @@ -528,6 +528,12 @@ ChatTheme makeChatTheme(ScaleScheme scale, TextTheme textTheme) => inputPadding: const EdgeInsets.all(9), inputTextColor: scale.primaryScale.text, attachmentButtonIcon: const Icon(Icons.attach_file), + receivedMessageBodyTextStyle: const TextStyle( + color: neutral0, + fontSize: 16, + fontWeight: FontWeight.w500, + height: 1.5, + ), ); ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) { diff --git a/lib/veilid_processor/views/developer.dart b/lib/veilid_processor/views/developer.dart index 8154da5..143da47 100644 --- a/lib/veilid_processor/views/developer.dart +++ b/lib/veilid_processor/views/developer.dart @@ -69,12 +69,23 @@ class _DeveloperPageState extends State { } Future _sendDebugCommand(String debugCommand) async { + if (debugCommand == 'pool allocations') { + DHTRecordPool.instance.debugPrintAllocations(); + return; + } + + if (debugCommand == 'pool opened') { + DHTRecordPool.instance.debugPrintOpened(); + return; + } + if (debugCommand == 'ellet') { setState(() { _showEllet = !_showEllet; }); return; } + _debugOut('DEBUG >>>\n$debugCommand\n'); try { final out = await Veilid.instance.debug(debugCommand); 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 2c37cdd..bcdbc42 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 @@ -98,6 +98,15 @@ class OpenedRecordInfo { ..sort((a, b) => a.key.toString().compareTo(b.key.toString())); return '[${r.map((x) => x.debugName).join(',')}]'; } + + String get details { + final r = records.toList() + ..sort((a, b) => a.key.toString().compareTo(b.key.toString())); + return '[${r.map((x) => "writer=${x._writer} " + "defaultSubkey=${x._defaultSubkey}").join(',')}]'; + } + + String get sharedDetails => shared.toString(); } class DHTRecordPool with TableDBBacked { @@ -768,4 +777,29 @@ class DHTRecordPool with TableDBBacked { _inTick = false; } } + + void debugPrintAllocations() { + final sortedAllocations = _state.debugNames.entries.asList() + ..sort((a, b) => a.key.compareTo(b.key)); + + log('DHTRecordPool Allocations: (count=${sortedAllocations.length})'); + + for (final entry in sortedAllocations) { + log(' ${entry.key}: ${entry.value}'); + } + } + + void debugPrintOpened() { + final sortedOpened = _opened.entries.asList() + ..sort((a, b) => a.key.toString().compareTo(b.key.toString())); + + log('DHTRecordPool Opened Records: (count=${sortedOpened.length})'); + + for (final entry in sortedOpened) { + log(' ${entry.key}: \n' + ' debugNames=${entry.value.debugNames}\n' + ' details=${entry.value.details}\n' + ' sharedDetails=${entry.value.sharedDetails}\n'); + } + } } 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 c01f10b..01390ed 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,5 +1,13 @@ part of 'dht_short_array.dart'; +class DHTShortArrayHeadLookup { + DHTShortArrayHeadLookup( + {required this.record, required this.recordSubkey, required this.seq}); + final DHTRecord record; + final int recordSubkey; + final int seq; +} + class _DHTShortArrayHead { _DHTShortArrayHead({required DHTRecord headRecord}) : _headRecord = headRecord, @@ -299,16 +307,18 @@ class _DHTShortArrayHead { ); } - Future<(DHTRecord, int)> lookupPosition(int pos) async { + Future lookupPosition(int pos) async { final idx = _index[pos]; return lookupIndex(idx); } - Future<(DHTRecord, int)> lookupIndex(int idx) async { + Future lookupIndex(int idx) async { + final seq = idx < _seqs.length ? _seqs[idx] : 0xFFFFFFFF; final recordNumber = idx ~/ _stride; final record = await _getOrCreateLinkedRecord(recordNumber); final recordSubkey = (idx % _stride) + ((recordNumber == 0) ? 1 : 0); - return (record, recordSubkey); + return DHTShortArrayHeadLookup( + record: record, recordSubkey: recordSubkey, seq: seq); } ///////////////////////////////////////////////////////////////////////////// @@ -416,9 +426,9 @@ class _DHTShortArrayHead { /// If a write is happening, update the network copy as well. 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)]); + final lookup = await lookupIndex(idx); + final report = await lookup.record + .inspect(subkeys: [ValueSubkeyRange.single(lookup.recordSubkey)]); while (_localSeqs.length <= idx) { _localSeqs.add(0xFFFFFFFF); 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 index fccdf20..3151e6e 100644 --- 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 @@ -68,10 +68,11 @@ class _DHTShortArrayRead implements DHTShortArrayRead { throw IndexError.withLength(pos, length); } - final (record, recordSubkey) = await _head.lookupPosition(pos); + final lookup = await _head.lookupPosition(pos); final refresh = forceRefresh || _head.positionNeedsRefresh(pos); - final out = record.get(subkey: recordSubkey, forceRefresh: refresh); + final out = + lookup.record.get(subkey: lookup.recordSubkey, forceRefresh: refresh); await _head.updatePositionSeq(pos, false); return out; 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 index 76e9ce2..a91543c 100644 --- 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 @@ -136,6 +136,12 @@ class _DHTShortArrayWrite implements DHTShortArrayWrite { @override Future trySwapItem(int aPos, int bPos) async { + if (aPos < 0 || aPos >= _head.length) { + throw IndexError.withLength(aPos, _head.length); + } + if (bPos < 0 || bPos >= _head.length) { + throw IndexError.withLength(bPos, _head.length); + } // Swap indices _head.swapIndex(aPos, bPos); @@ -144,8 +150,13 @@ class _DHTShortArrayWrite implements DHTShortArrayWrite { @override Future tryRemoveItem(int pos) async { - final (record, recordSubkey) = await _head.lookupPosition(pos); - final result = await record.get(subkey: recordSubkey); + if (pos < 0 || pos >= _head.length) { + throw IndexError.withLength(pos, _head.length); + } + final lookup = await _head.lookupPosition(pos); + final result = lookup.seq == 0xFFFFFFFF + ? null + : await lookup.record.get(subkey: lookup.recordSubkey); if (result == null) { throw StateError('Element does not exist'); } @@ -164,9 +175,12 @@ class _DHTShortArrayWrite implements DHTShortArrayWrite { 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); + final lookup = await _head.lookupPosition(pos); + final oldValue = lookup.seq == 0xFFFFFFFF + ? null + : await lookup.record.get(subkey: lookup.recordSubkey); + final result = await lookup.record + .tryWriteBytes(newValue, subkey: lookup.recordSubkey); if (result != null) { // A result coming back means the element was overwritten already return (result, false); From 1f99279cd236950b6dda4c31ef9d0aa93520c34a Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 7 Apr 2024 23:16:06 -0400 Subject: [PATCH 082/270] ui cleanup --- assets/i18n/en.json | 4 +- lib/chat/views/chat_component.dart | 2 + .../chat_single_contact_item_widget.dart | 16 +- .../chat_single_contact_list_widget.dart | 4 +- .../views/contact_invitation_item_widget.dart | 4 +- .../views/paste_invitation_dialog.dart | 2 +- lib/contacts/views/contact_item_widget.dart | 18 +- lib/contacts/views/contact_list_widget.dart | 2 +- .../home_account_ready_main.dart | 2 +- .../main_pager/main_pager.dart | 10 +- lib/settings/settings_page.dart | 5 +- lib/theme/models/radix_generator.dart | 383 +++++++++++------- lib/theme/models/scale_color.dart | 41 +- lib/tools/enter_password.dart | 2 +- lib/tools/enter_pin.dart | 2 +- lib/tools/state_logger.dart | 3 +- lib/tools/widget_helpers.dart | 16 +- lib/veilid_processor/views/developer.dart | 22 +- .../views/signal_strength_meter.dart | 14 +- macos/Runner.xcodeproj/project.pbxproj | 6 +- macos/Runner/DebugProfile.entitlements | 8 +- macos/Runner/Release.entitlements | 8 +- packages/veilid_support/pubspec.lock | 2 +- 23 files changed, 358 insertions(+), 218 deletions(-) diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 0a3baf7..2551515 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -125,13 +125,13 @@ "password_does_not_match": "Password does not match" }, "contact_list": { - "title": "Contact List", + "title": "Contacts", "invite_people": "Invite people to VeilidChat", "search": "Search contacts", "invitation": "Invitation" }, "chat_list": { - "search": "Search", + "search": "Search chats", "start_a_conversation": "Start a conversation", "chats": "Chats", "groups": "Groups" diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 068bd75..32210a0 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -188,6 +188,8 @@ class ChatComponent extends StatelessWidget { decoration: const BoxDecoration(), child: Chat( theme: chatTheme, + // emojiEnlargementBehavior: + // EmojiEnlargementBehavior.multi, messages: chatMessages, //onAttachmentPressed: _handleAttachmentPressed, //onMessageTap: _handleMessageTap, 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 7daa99c..241791c 100644 --- a/lib/chat_list/views/chat_single_contact_item_widget.dart +++ b/lib/chat_list/views/chat_single_contact_item_widget.dart @@ -38,7 +38,9 @@ class ChatSingleContactItemWidget extends StatelessWidget { margin: const EdgeInsets.fromLTRB(0, 4, 0, 0), clipBehavior: Clip.antiAlias, decoration: ShapeDecoration( - color: scale.tertiaryScale.subtleBorder, + color: selected + ? scale.primaryScale.activeElementBackground + : scale.primaryScale.hoverElementBackground, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), )), @@ -57,7 +59,7 @@ class ChatSingleContactItemWidget extends StatelessWidget { remoteConversationRecordKey); }, backgroundColor: scale.tertiaryScale.background, - foregroundColor: scale.tertiaryScale.text, + foregroundColor: scale.tertiaryScale.foregroundText, icon: Icons.delete, label: translate('button.delete'), padding: const EdgeInsets.all(2)), @@ -88,9 +90,13 @@ class ChatSingleContactItemWidget extends StatelessWidget { subtitle: (_contact.editedProfile.pronouns.isNotEmpty) ? Text(_contact.editedProfile.pronouns) : null, - iconColor: scale.tertiaryScale.background, - textColor: scale.tertiaryScale.text, - selected: selected, + iconColor: selected + ? scale.primaryScale.appText + : scale.primaryScale.subtleText, + textColor: selected + ? scale.primaryScale.appText + : scale.primaryScale.subtleText, + selectedColor: scale.primaryScale.appText, //Text(Timestamp.fromInt64(contactInvitationRecord.expiration) / ), leading: const Icon(Icons.chat)))); } 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 04092b4..c689e22 100644 --- a/lib/chat_list/views/chat_single_contact_list_widget.dart +++ b/lib/chat_list/views/chat_single_contact_list_widget.dart @@ -68,7 +68,7 @@ class ChatSingleContactListWidget extends StatelessWidget { inputDecoration: InputDecoration( labelText: translate('chat_list.search'), contentPadding: const EdgeInsets.all(2), - fillColor: scale.primaryScale.text, + fillColor: scale.primaryScale.elementBackground, focusedBorder: OutlineInputBorder( borderSide: BorderSide( color: scale.primaryScale.hoverBorder, @@ -77,7 +77,7 @@ class ChatSingleContactListWidget extends StatelessWidget { ), ), ).paddingAll(8)))) - .paddingLTRB(8, 8, 8, 65)); + .paddingLTRB(8, 0, 8, 8)); }); } } diff --git a/lib/contact_invitation/views/contact_invitation_item_widget.dart b/lib/contact_invitation/views/contact_invitation_item_widget.dart index 8b81890..fd00c0a 100644 --- a/lib/contact_invitation/views/contact_invitation_item_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_item_widget.dart @@ -66,7 +66,7 @@ class ContactInvitationItemWidget extends StatelessWidget { .toVeilid()); }, backgroundColor: scale.tertiaryScale.background, - foregroundColor: scale.tertiaryScale.text, + foregroundColor: scale.tertiaryScale.appText, icon: Icons.delete, label: translate('button.delete'), padding: const EdgeInsets.all(2)), @@ -119,7 +119,7 @@ class ContactInvitationItemWidget extends StatelessWidget { softWrap: true, ), iconColor: scale.tertiaryScale.background, - textColor: scale.tertiaryScale.text, + textColor: scale.tertiaryScale.appText, //Text(Timestamp.fromInt64(contactInvitationRecord.expiration) / ), leading: const Icon(Icons.person_add)))); } diff --git a/lib/contact_invitation/views/paste_invitation_dialog.dart b/lib/contact_invitation/views/paste_invitation_dialog.dart index 75a6208..3e19c1c 100644 --- a/lib/contact_invitation/views/paste_invitation_dialog.dart +++ b/lib/contact_invitation/views/paste_invitation_dialog.dart @@ -107,7 +107,7 @@ class PasteInvitationDialogState extends State { final monoStyle = TextStyle( fontFamily: 'Source Code Pro', fontSize: 11, - color: scale.primaryScale.text, + color: scale.primaryScale.appText, ); return Column(mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/contacts/views/contact_item_widget.dart b/lib/contacts/views/contact_item_widget.dart index 49d6bb1..1306ef3 100644 --- a/lib/contacts/views/contact_item_widget.dart +++ b/lib/contacts/views/contact_item_widget.dart @@ -29,11 +29,16 @@ class ContactItemWidget extends StatelessWidget { final remoteConversationKey = contact.remoteConversationRecordKey.toVeilid(); + const selected = + false; // xxx: eventually when we have selectable contacts: activeContactCubit.state == remoteConversationRecordKey; + return Container( margin: const EdgeInsets.fromLTRB(0, 4, 0, 0), clipBehavior: Clip.antiAlias, decoration: ShapeDecoration( - color: scale.tertiaryScale.subtleBorder, + color: selected + ? scale.primaryScale.activeElementBackground + : scale.primaryScale.hoverElementBackground, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), )), @@ -60,7 +65,7 @@ class ContactItemWidget extends StatelessWidget { contact: contact); }, backgroundColor: scale.tertiaryScale.background, - foregroundColor: scale.tertiaryScale.text, + foregroundColor: scale.tertiaryScale.appText, icon: Icons.delete, label: translate('button.delete'), padding: const EdgeInsets.all(2)), @@ -96,8 +101,13 @@ class ContactItemWidget extends StatelessWidget { subtitle: (contact.editedProfile.pronouns.isNotEmpty) ? Text(contact.editedProfile.pronouns) : null, - iconColor: scale.tertiaryScale.background, - textColor: scale.tertiaryScale.text, + iconColor: selected + ? scale.primaryScale.appText + : scale.primaryScale.subtleText, + textColor: selected + ? scale.primaryScale.appText + : scale.primaryScale.subtleText, + selectedColor: scale.primaryScale.appText, leading: const Icon(Icons.person)))); } diff --git a/lib/contacts/views/contact_list_widget.dart b/lib/contacts/views/contact_list_widget.dart index df8cf79..f5b775b 100644 --- a/lib/contacts/views/contact_list_widget.dart +++ b/lib/contacts/views/contact_list_widget.dart @@ -58,7 +58,7 @@ class ContactListWidget extends StatelessWidget { inputDecoration: InputDecoration( labelText: translate('contact_list.search'), contentPadding: const EdgeInsets.all(2), - fillColor: scale.primaryScale.text, + fillColor: scale.primaryScale.appText, focusedBorder: OutlineInputBorder( borderSide: BorderSide( color: scale.primaryScale.hoverBorder, 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 d02cfaf..fd5790f 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 @@ -37,7 +37,7 @@ class _HomeAccountReadyMainState extends State { Row(children: [ IconButton( icon: const Icon(Icons.settings), - color: scale.secondaryScale.text, + color: scale.secondaryScale.appText, constraints: const BoxConstraints.expand(height: 64, width: 64), style: ButtonStyle( backgroundColor: 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 5561cfd..0e121a4 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 @@ -85,10 +85,10 @@ class MainPagerState extends State with TickerProviderStateMixin { final scale = theme.extension()!; return BottomBarItem( title: Text(_bottomLabelList[index]), - icon: Icon(_selectedIconList[index], color: scale.primaryScale.text), + icon: Icon(_selectedIconList[index], color: scale.primaryScale.appText), selectedIcon: - Icon(_selectedIconList[index], color: scale.primaryScale.text), - backgroundColor: scale.primaryScale.text, + Icon(_selectedIconList[index], color: scale.primaryScale.appText), + backgroundColor: scale.primaryScale.appText, //unSelectedColor: theme.colorScheme.primaryContainer, //selectedColor: theme.colorScheme.primary, //badge: const Text('9+'), @@ -209,11 +209,11 @@ class MainPagerState extends State with TickerProviderStateMixin { floatingActionButton: BottomSheetActionButton( shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(14))), - foregroundColor: scale.secondaryScale.text, + foregroundColor: scale.secondaryScale.appText, backgroundColor: scale.secondaryScale.hoverBorder, builder: (context) => Icon( _fabIconList[_currentPage], - color: scale.secondaryScale.text, + color: scale.secondaryScale.appText, ), bottomSheetBuilder: (sheetContext) => _bottomSheetBuilder(sheetContext, context)), diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart index 821f9c6..c84e7d7 100644 --- a/lib/settings/settings_page.dart +++ b/lib/settings/settings_page.dart @@ -3,6 +3,7 @@ import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_translate/flutter_translate.dart'; +import 'package:go_router/go_router.dart'; import '../layout/default_app_bar.dart'; import '../theme/theme.dart'; @@ -37,18 +38,16 @@ class SettingsPageState extends State { 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(), + onPressed: () => GoRouterHelper(context).pop(), ), actions: [ const SignalStrengthMeterWidget() .paddingLTRB(16, 0, 16, 0), ]), - body: FormBuilder( key: _formKey, child: ListView( diff --git a/lib/theme/models/radix_generator.dart b/lib/theme/models/radix_generator.dart index e3b0a5a..efc3bdb 100644 --- a/lib/theme/models/radix_generator.dart +++ b/lib/theme/models/radix_generator.dart @@ -270,7 +270,7 @@ RadixColor _radixColorSteps( } extension ToScaleColor on RadixColor { - ScaleColor toScale() => ScaleColor( + ScaleColor toScale(RadixScaleExtra scaleExtra) => ScaleColor( appBackground: step1, subtleBackground: step2, elementBackground: step3, @@ -282,33 +282,53 @@ extension ToScaleColor on RadixColor { background: step9, hoverBackground: step10, subtleText: step11, - text: step12, + appText: step12, + foregroundText: scaleExtra.foregroundText, ); } -class RadixScheme { - RadixScheme( - {required this.primaryScale, - required this.primaryAlphaScale, - required this.secondaryScale, - required this.tertiaryScale, - required this.grayScale, - required this.errorScale}); +class RadixScaleExtra { + RadixScaleExtra({required this.foregroundText}); - RadixColor primaryScale; - RadixColor primaryAlphaScale; - RadixColor secondaryScale; - RadixColor tertiaryScale; - RadixColor grayScale; - RadixColor errorScale; + final Color foregroundText; +} + +class RadixScheme { + const RadixScheme({ + required this.primaryScale, + required this.primaryExtra, + required this.primaryAlphaScale, + required this.primaryAlphaExtra, + required this.secondaryScale, + required this.secondaryExtra, + required this.tertiaryScale, + required this.tertiaryExtra, + required this.grayScale, + required this.grayExtra, + required this.errorScale, + required this.errorExtra, + }); + + final RadixColor primaryScale; + final RadixScaleExtra primaryExtra; + final RadixColor primaryAlphaScale; + final RadixScaleExtra primaryAlphaExtra; + final RadixColor secondaryScale; + final RadixScaleExtra secondaryExtra; + final RadixColor tertiaryScale; + final RadixScaleExtra tertiaryExtra; + final RadixColor grayScale; + final RadixScaleExtra grayExtra; + final RadixColor errorScale; + final RadixScaleExtra errorExtra; ScaleScheme toScale() => ScaleScheme( - primaryScale: primaryScale.toScale(), - primaryAlphaScale: primaryAlphaScale.toScale(), - secondaryScale: secondaryScale.toScale(), - tertiaryScale: tertiaryScale.toScale(), - grayScale: grayScale.toScale(), - errorScale: errorScale.toScale(), + primaryScale: primaryScale.toScale(primaryExtra), + primaryAlphaScale: primaryAlphaScale.toScale(primaryAlphaExtra), + secondaryScale: secondaryScale.toScale(secondaryExtra), + tertiaryScale: tertiaryScale.toScale(tertiaryExtra), + grayScale: grayScale.toScale(grayExtra), + errorScale: errorScale.toScale(errorExtra), ); } @@ -318,231 +338,316 @@ RadixScheme _radixScheme(Brightness brightness, RadixThemeColor themeColor) { // tomato + red + violet case RadixThemeColor.scarlet: radixScheme = RadixScheme( - primaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.tomato), - primaryAlphaScale: - _radixColorSteps(brightness, true, _RadixBaseColor.tomato), - secondaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.red), - tertiaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.violet), - grayScale: _radixGraySteps(brightness, false, _RadixBaseColor.tomato), - errorScale: - _radixColorSteps(brightness, false, _RadixBaseColor.yellow)); + primaryScale: + _radixColorSteps(brightness, false, _RadixBaseColor.tomato), + primaryExtra: RadixScaleExtra(foregroundText: Colors.white), + primaryAlphaScale: + _radixColorSteps(brightness, true, _RadixBaseColor.tomato), + primaryAlphaExtra: RadixScaleExtra(foregroundText: Colors.white), + secondaryScale: + _radixColorSteps(brightness, false, _RadixBaseColor.red), + secondaryExtra: RadixScaleExtra(foregroundText: Colors.white), + tertiaryScale: + _radixColorSteps(brightness, false, _RadixBaseColor.violet), + tertiaryExtra: RadixScaleExtra(foregroundText: Colors.white), + grayScale: _radixGraySteps(brightness, false, _RadixBaseColor.tomato), + grayExtra: RadixScaleExtra(foregroundText: Colors.white), + errorScale: _radixColorSteps(brightness, false, _RadixBaseColor.yellow), + errorExtra: RadixScaleExtra(foregroundText: Colors.black), + ); // crimson + purple + pink case RadixThemeColor.babydoll: radixScheme = RadixScheme( primaryScale: _radixColorSteps(brightness, false, _RadixBaseColor.crimson), + primaryExtra: RadixScaleExtra(foregroundText: Colors.white), primaryAlphaScale: _radixColorSteps(brightness, true, _RadixBaseColor.crimson), + primaryAlphaExtra: RadixScaleExtra(foregroundText: Colors.white), secondaryScale: _radixColorSteps(brightness, false, _RadixBaseColor.purple), + secondaryExtra: RadixScaleExtra(foregroundText: Colors.white), tertiaryScale: _radixColorSteps(brightness, false, _RadixBaseColor.pink), + tertiaryExtra: RadixScaleExtra(foregroundText: Colors.white), grayScale: _radixGraySteps(brightness, false, _RadixBaseColor.crimson), + grayExtra: RadixScaleExtra(foregroundText: Colors.white), errorScale: - _radixColorSteps(brightness, false, _RadixBaseColor.orange)); + _radixColorSteps(brightness, false, _RadixBaseColor.orange), + errorExtra: RadixScaleExtra(foregroundText: Colors.white)); // pink + cyan + plum case RadixThemeColor.vapor: radixScheme = RadixScheme( primaryScale: _radixColorSteps(brightness, false, _RadixBaseColor.pink), + primaryExtra: RadixScaleExtra(foregroundText: Colors.white), primaryAlphaScale: _radixColorSteps(brightness, true, _RadixBaseColor.pink), + primaryAlphaExtra: RadixScaleExtra(foregroundText: Colors.white), secondaryScale: _radixColorSteps(brightness, false, _RadixBaseColor.cyan), + secondaryExtra: RadixScaleExtra(foregroundText: Colors.white), tertiaryScale: _radixColorSteps(brightness, false, _RadixBaseColor.plum), + tertiaryExtra: RadixScaleExtra(foregroundText: Colors.white), grayScale: _radixGraySteps(brightness, false, _RadixBaseColor.pink), - errorScale: _radixColorSteps(brightness, false, _RadixBaseColor.red)); + grayExtra: RadixScaleExtra(foregroundText: Colors.white), + errorScale: _radixColorSteps(brightness, false, _RadixBaseColor.red), + errorExtra: RadixScaleExtra(foregroundText: Colors.white)); // yellow + amber + orange case RadixThemeColor.gold: radixScheme = RadixScheme( primaryScale: _radixColorSteps(brightness, false, _RadixBaseColor.yellow), + primaryExtra: RadixScaleExtra(foregroundText: Colors.black), primaryAlphaScale: _radixColorSteps(brightness, true, _RadixBaseColor.yellow), + primaryAlphaExtra: RadixScaleExtra(foregroundText: Colors.black), secondaryScale: _radixColorSteps(brightness, false, _RadixBaseColor.amber), + secondaryExtra: RadixScaleExtra(foregroundText: Colors.black), tertiaryScale: _radixColorSteps(brightness, false, _RadixBaseColor.orange), + tertiaryExtra: RadixScaleExtra(foregroundText: Colors.black), grayScale: _radixGraySteps(brightness, false, _RadixBaseColor.yellow), - errorScale: _radixColorSteps(brightness, false, _RadixBaseColor.red)); + grayExtra: RadixScaleExtra(foregroundText: Colors.white), + errorScale: _radixColorSteps(brightness, false, _RadixBaseColor.red), + errorExtra: RadixScaleExtra(foregroundText: Colors.white)); // grass + orange + brown case RadixThemeColor.garden: radixScheme = RadixScheme( primaryScale: _radixColorSteps(brightness, false, _RadixBaseColor.grass), + primaryExtra: RadixScaleExtra(foregroundText: Colors.white), primaryAlphaScale: _radixColorSteps(brightness, true, _RadixBaseColor.grass), + primaryAlphaExtra: RadixScaleExtra(foregroundText: Colors.white), secondaryScale: _radixColorSteps(brightness, false, _RadixBaseColor.orange), + secondaryExtra: RadixScaleExtra(foregroundText: Colors.white), tertiaryScale: _radixColorSteps(brightness, false, _RadixBaseColor.brown), + tertiaryExtra: RadixScaleExtra(foregroundText: Colors.white), grayScale: _radixGraySteps(brightness, false, _RadixBaseColor.grass), + grayExtra: RadixScaleExtra(foregroundText: Colors.white), errorScale: - _radixColorSteps(brightness, false, _RadixBaseColor.tomato)); + _radixColorSteps(brightness, false, _RadixBaseColor.tomato), + errorExtra: RadixScaleExtra(foregroundText: Colors.white)); // green + brown + amber case RadixThemeColor.forest: radixScheme = RadixScheme( primaryScale: _radixColorSteps(brightness, false, _RadixBaseColor.green), + primaryExtra: RadixScaleExtra(foregroundText: Colors.white), primaryAlphaScale: _radixColorSteps(brightness, true, _RadixBaseColor.green), + primaryAlphaExtra: RadixScaleExtra(foregroundText: Colors.white), secondaryScale: _radixColorSteps(brightness, false, _RadixBaseColor.brown), + secondaryExtra: RadixScaleExtra(foregroundText: Colors.white), tertiaryScale: _radixColorSteps(brightness, false, _RadixBaseColor.amber), + tertiaryExtra: RadixScaleExtra(foregroundText: Colors.black), grayScale: _radixGraySteps(brightness, false, _RadixBaseColor.green), + grayExtra: RadixScaleExtra(foregroundText: Colors.white), errorScale: - _radixColorSteps(brightness, false, _RadixBaseColor.tomato)); + _radixColorSteps(brightness, false, _RadixBaseColor.tomato), + errorExtra: RadixScaleExtra(foregroundText: Colors.white)); + // sky + teal + violet case RadixThemeColor.arctic: radixScheme = RadixScheme( primaryScale: _radixColorSteps(brightness, false, _RadixBaseColor.sky), + primaryExtra: RadixScaleExtra(foregroundText: Colors.black), primaryAlphaScale: _radixColorSteps(brightness, true, _RadixBaseColor.sky), + primaryAlphaExtra: RadixScaleExtra(foregroundText: Colors.black), secondaryScale: _radixColorSteps(brightness, false, _RadixBaseColor.teal), + secondaryExtra: RadixScaleExtra(foregroundText: Colors.white), tertiaryScale: _radixColorSteps(brightness, false, _RadixBaseColor.violet), + tertiaryExtra: RadixScaleExtra(foregroundText: Colors.white), grayScale: _radixGraySteps(brightness, false, _RadixBaseColor.sky), + grayExtra: RadixScaleExtra(foregroundText: Colors.white), errorScale: - _radixColorSteps(brightness, false, _RadixBaseColor.crimson)); + _radixColorSteps(brightness, false, _RadixBaseColor.crimson), + errorExtra: RadixScaleExtra(foregroundText: Colors.white)); // blue + indigo + mint case RadixThemeColor.lapis: radixScheme = RadixScheme( - primaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.blue), - primaryAlphaScale: - _radixColorSteps(brightness, true, _RadixBaseColor.blue), - secondaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.indigo), - tertiaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.mint), - grayScale: _radixGraySteps(brightness, false, _RadixBaseColor.blue), - errorScale: - _radixColorSteps(brightness, false, _RadixBaseColor.crimson)); + primaryScale: _radixColorSteps(brightness, false, _RadixBaseColor.blue), + primaryExtra: RadixScaleExtra(foregroundText: Colors.white), + primaryAlphaScale: + _radixColorSteps(brightness, true, _RadixBaseColor.blue), + primaryAlphaExtra: RadixScaleExtra(foregroundText: Colors.white), + secondaryScale: + _radixColorSteps(brightness, false, _RadixBaseColor.indigo), + secondaryExtra: RadixScaleExtra(foregroundText: Colors.white), + tertiaryScale: + _radixColorSteps(brightness, false, _RadixBaseColor.mint), + tertiaryExtra: RadixScaleExtra(foregroundText: Colors.black), + grayScale: _radixGraySteps(brightness, false, _RadixBaseColor.blue), + grayExtra: RadixScaleExtra(foregroundText: Colors.white), + errorScale: + _radixColorSteps(brightness, false, _RadixBaseColor.crimson), + errorExtra: RadixScaleExtra(foregroundText: Colors.white), + ); // violet + purple + indigo case RadixThemeColor.eggplant: radixScheme = RadixScheme( - primaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.violet), - primaryAlphaScale: - _radixColorSteps(brightness, true, _RadixBaseColor.violet), - secondaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.purple), - tertiaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.indigo), - grayScale: _radixGraySteps(brightness, false, _RadixBaseColor.violet), - errorScale: - _radixColorSteps(brightness, false, _RadixBaseColor.crimson)); + primaryScale: + _radixColorSteps(brightness, false, _RadixBaseColor.violet), + primaryExtra: RadixScaleExtra(foregroundText: Colors.white), + primaryAlphaScale: + _radixColorSteps(brightness, true, _RadixBaseColor.violet), + primaryAlphaExtra: RadixScaleExtra(foregroundText: Colors.white), + secondaryScale: + _radixColorSteps(brightness, false, _RadixBaseColor.purple), + secondaryExtra: RadixScaleExtra(foregroundText: Colors.white), + tertiaryScale: + _radixColorSteps(brightness, false, _RadixBaseColor.indigo), + tertiaryExtra: RadixScaleExtra(foregroundText: Colors.white), + grayScale: _radixGraySteps(brightness, false, _RadixBaseColor.violet), + grayExtra: RadixScaleExtra(foregroundText: Colors.white), + errorScale: + _radixColorSteps(brightness, false, _RadixBaseColor.crimson), + errorExtra: RadixScaleExtra(foregroundText: Colors.white), + ); // lime + yellow + orange case RadixThemeColor.lime: radixScheme = RadixScheme( - primaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.lime), - primaryAlphaScale: - _radixColorSteps(brightness, true, _RadixBaseColor.lime), - secondaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.yellow), - tertiaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.orange), - grayScale: _radixGraySteps(brightness, false, _RadixBaseColor.lime), - errorScale: _radixColorSteps(brightness, false, _RadixBaseColor.red)); + primaryScale: _radixColorSteps(brightness, false, _RadixBaseColor.lime), + primaryExtra: RadixScaleExtra(foregroundText: Colors.black), + primaryAlphaScale: + _radixColorSteps(brightness, true, _RadixBaseColor.lime), + primaryAlphaExtra: RadixScaleExtra(foregroundText: Colors.black), + secondaryScale: + _radixColorSteps(brightness, false, _RadixBaseColor.yellow), + secondaryExtra: RadixScaleExtra(foregroundText: Colors.black), + tertiaryScale: + _radixColorSteps(brightness, false, _RadixBaseColor.orange), + tertiaryExtra: RadixScaleExtra(foregroundText: Colors.white), + grayScale: _radixGraySteps(brightness, false, _RadixBaseColor.lime), + grayExtra: RadixScaleExtra(foregroundText: Colors.white), + errorScale: _radixColorSteps(brightness, false, _RadixBaseColor.red), + errorExtra: RadixScaleExtra(foregroundText: Colors.white), + ); // mauve + slate + sage case RadixThemeColor.grim: radixScheme = RadixScheme( - primaryScale: - _radixGraySteps(brightness, false, _RadixBaseColor.tomato), - primaryAlphaScale: - _radixColorSteps(brightness, true, _RadixBaseColor.tomato), - secondaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.indigo), - tertiaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.teal), - grayScale: brightness == Brightness.dark - ? RadixColors.dark.gray - : RadixColors.gray, - errorScale: _radixColorSteps(brightness, false, _RadixBaseColor.red)); + primaryScale: + _radixGraySteps(brightness, false, _RadixBaseColor.tomato), + primaryExtra: RadixScaleExtra(foregroundText: Colors.white), + primaryAlphaScale: + _radixColorSteps(brightness, true, _RadixBaseColor.tomato), + primaryAlphaExtra: RadixScaleExtra(foregroundText: Colors.white), + secondaryScale: + _radixColorSteps(brightness, false, _RadixBaseColor.indigo), + secondaryExtra: RadixScaleExtra(foregroundText: Colors.white), + tertiaryScale: + _radixColorSteps(brightness, false, _RadixBaseColor.teal), + tertiaryExtra: RadixScaleExtra(foregroundText: Colors.white), + grayScale: brightness == Brightness.dark + ? RadixColors.dark.gray + : RadixColors.gray, + grayExtra: RadixScaleExtra(foregroundText: Colors.white), + errorScale: _radixColorSteps(brightness, false, _RadixBaseColor.red), + errorExtra: RadixScaleExtra(foregroundText: Colors.white), + ); } return radixScheme; } -ColorScheme _radixColorScheme(Brightness brightness, RadixScheme radix) => +ColorScheme _scaleToColorScheme(Brightness brightness, ScaleScheme scale) => ColorScheme( brightness: brightness, - primary: radix.primaryScale.step9, - onPrimary: radix.primaryScale.step12, - primaryContainer: radix.primaryScale.step4, - onPrimaryContainer: radix.primaryScale.step11, - secondary: radix.secondaryScale.step9, - onSecondary: radix.secondaryScale.step12, - secondaryContainer: radix.secondaryScale.step3, - onSecondaryContainer: radix.secondaryScale.step11, - tertiary: radix.tertiaryScale.step9, - onTertiary: radix.tertiaryScale.step12, - tertiaryContainer: radix.tertiaryScale.step3, - onTertiaryContainer: radix.tertiaryScale.step11, - error: radix.errorScale.step9, - onError: radix.errorScale.step12, - errorContainer: radix.errorScale.step3, - onErrorContainer: radix.errorScale.step11, - background: radix.grayScale.step1, - onBackground: radix.grayScale.step11, - surface: radix.primaryScale.step1, - onSurface: radix.primaryScale.step12, - surfaceVariant: radix.secondaryScale.step2, - onSurfaceVariant: radix.secondaryScale.step11, - outline: radix.primaryScale.step7, - outlineVariant: radix.primaryScale.step6, + primary: scale.primaryScale.background, // reviewed + onPrimary: scale.primaryScale.foregroundText, // reviewed + primaryContainer: + Colors.red, // scale.primaryScale.hoverElementBackground, + onPrimaryContainer: Colors.green, //scale.primaryScale.subtleText, + secondary: scale.secondaryScale.background, + onSecondary: scale.secondaryScale.appText, + secondaryContainer: scale.secondaryScale.hoverElementBackground, + onSecondaryContainer: scale.secondaryScale.subtleText, + tertiary: scale.tertiaryScale.background, + onTertiary: scale.tertiaryScale.appText, + tertiaryContainer: scale.tertiaryScale.hoverElementBackground, + onTertiaryContainer: scale.tertiaryScale.subtleText, + error: scale.errorScale.background, + onError: scale.errorScale.appText, + errorContainer: scale.errorScale.hoverElementBackground, + onErrorContainer: scale.errorScale.subtleText, + background: scale.grayScale.appBackground, // reviewed + onBackground: scale.grayScale.appText, // reviewed + surface: scale.primaryScale.activeElementBackground, // reviewed + onSurface: scale.primaryScale.subtleText, // reviewed + surfaceVariant: scale.primaryScale.elementBackground, + onSurfaceVariant: scale.primaryScale.subtleText, // ?? reviewed a little + outline: scale.primaryScale.border, + outlineVariant: scale.primaryScale.subtleBorder, shadow: RadixColors.dark.gray.step1, - scrim: radix.primaryScale.step9, - inverseSurface: radix.primaryScale.step11, - onInverseSurface: radix.primaryScale.step2, - inversePrimary: radix.primaryScale.step10, - surfaceTint: radix.primaryAlphaScale.step4, + scrim: scale.primaryScale.background, + inverseSurface: scale.primaryScale.subtleText, + onInverseSurface: scale.primaryScale.subtleBackground, + inversePrimary: scale.primaryScale.hoverBackground, + surfaceTint: scale.primaryAlphaScale.hoverElementBackground, ); ChatTheme makeChatTheme(ScaleScheme scale, TextTheme textTheme) => DefaultChatTheme( - primaryColor: scale.primaryScale.background, - secondaryColor: scale.secondaryScale.background, - backgroundColor: scale.grayScale.subtleBackground, - inputBackgroundColor: Colors.blue, - inputBorderRadius: BorderRadius.zero, - inputTextDecoration: InputDecoration( - filled: true, - fillColor: scale.primaryScale.elementBackground, - isDense: true, - contentPadding: const EdgeInsets.fromLTRB(8, 12, 8, 12), - border: const OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.all(Radius.circular(16))), - ), - inputContainerDecoration: BoxDecoration(color: scale.primaryScale.border), - inputPadding: const EdgeInsets.all(9), - inputTextColor: scale.primaryScale.text, - attachmentButtonIcon: const Icon(Icons.attach_file), - receivedMessageBodyTextStyle: const TextStyle( - color: neutral0, - fontSize: 16, - fontWeight: FontWeight.w500, - height: 1.5, - ), - ); + primaryColor: scale.primaryScale.background, + secondaryColor: scale.secondaryScale.background, + backgroundColor: scale.grayScale.appBackground, + inputBackgroundColor: Colors.blue, + inputBorderRadius: BorderRadius.zero, + inputTextDecoration: InputDecoration( + filled: true, + fillColor: scale.primaryScale.elementBackground, + isDense: true, + contentPadding: const EdgeInsets.fromLTRB(8, 12, 8, 12), + border: const OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.all(Radius.circular(16))), + ), + inputContainerDecoration: + BoxDecoration(color: scale.primaryScale.border), + inputPadding: const EdgeInsets.all(9), + inputTextColor: scale.primaryScale.appText, + attachmentButtonIcon: const Icon(Icons.attach_file), + sentMessageBodyTextStyle: TextStyle( + color: scale.primaryScale.foregroundText, + decorationColor: Colors.red, + fontSize: 16, + fontWeight: FontWeight.w500, + height: 1.5, + ), + sentEmojiMessageTextStyle: const TextStyle( + color: Colors.white, + fontSize: 64, + ), + receivedMessageBodyTextStyle: TextStyle( + color: scale.primaryScale.foregroundText, + fontSize: 16, + fontWeight: FontWeight.w500, + height: 1.5, + ), + receivedEmojiMessageTextStyle: const TextStyle( + color: Colors.white, + fontSize: 64, + )); ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) { final textTheme = (brightness == Brightness.light) ? Typography.blackCupertino : Typography.whiteCupertino; final radix = _radixScheme(brightness, themeColor); - final colorScheme = _radixColorScheme(brightness, radix); final scaleScheme = radix.toScale(); + final colorScheme = _scaleToColorScheme(brightness, scaleScheme); final themeData = ThemeData.from( colorScheme: colorScheme, textTheme: textTheme, useMaterial3: true); diff --git a/lib/theme/models/scale_color.dart b/lib/theme/models/scale_color.dart index 1b2f112..8a14acc 100644 --- a/lib/theme/models/scale_color.dart +++ b/lib/theme/models/scale_color.dart @@ -13,7 +13,8 @@ class ScaleColor { required this.background, required this.hoverBackground, required this.subtleText, - required this.text, + required this.appText, + required this.foregroundText, }); Color appBackground; @@ -27,21 +28,24 @@ class ScaleColor { Color background; Color hoverBackground; Color subtleText; - Color text; + Color appText; + Color foregroundText; - 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 copyWith({ + Color? appBackground, + Color? subtleBackground, + Color? elementBackground, + Color? hoverElementBackground, + Color? activeElementBackground, + Color? subtleBorder, + Color? border, + Color? hoverBorder, + Color? background, + Color? hoverBackground, + Color? subtleText, + Color? appText, + Color? foregroundText, + }) => ScaleColor( appBackground: appBackground ?? this.appBackground, subtleBackground: subtleBackground ?? this.subtleBackground, @@ -56,7 +60,8 @@ class ScaleColor { background: background ?? this.background, hoverBackground: hoverBackground ?? this.hoverBackground, subtleText: subtleText ?? this.subtleText, - text: text ?? this.text, + appText: appText ?? this.appText, + foregroundText: foregroundText ?? this.foregroundText, ); // ignore: prefer_constructors_over_static_methods @@ -86,6 +91,8 @@ class ScaleColor { 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), + appText: Color.lerp(a.appText, b.appText, t) ?? const Color(0x00000000), + foregroundText: Color.lerp(a.foregroundText, b.foregroundText, t) ?? + const Color(0x00000000), ); } diff --git a/lib/tools/enter_password.dart b/lib/tools/enter_password.dart index 3bce52d..ca1bd73 100644 --- a/lib/tools/enter_password.dart +++ b/lib/tools/enter_password.dart @@ -94,7 +94,7 @@ class _EnterPasswordDialogState extends State { _passwordVisible ? Icons.visibility : Icons.visibility_off, - color: scale.primaryScale.text, + color: scale.primaryScale.appText, ), onPressed: () { setState(() { diff --git a/lib/tools/enter_pin.dart b/lib/tools/enter_pin.dart index 9961430..7166126 100644 --- a/lib/tools/enter_pin.dart +++ b/lib/tools/enter_pin.dart @@ -58,7 +58,7 @@ class _EnterPinDialogState extends State { final defaultPinTheme = PinTheme( width: 56, height: 60, - textStyle: TextStyle(fontSize: 22, color: scale.primaryScale.text), + textStyle: TextStyle(fontSize: 22, color: scale.primaryScale.appText), decoration: BoxDecoration( color: fillColor, borderRadius: BorderRadius.circular(8), diff --git a/lib/tools/state_logger.dart b/lib/tools/state_logger.dart index 60c274e..150309c 100644 --- a/lib/tools/state_logger.dart +++ b/lib/tools/state_logger.dart @@ -4,7 +4,8 @@ import 'loggy.dart'; const Map _blocChangeLogLevels = { 'ConnectionStateCubit': LogLevel.off, - 'ActiveConversationMessagesBlocMapCubit': LogLevel.off + 'ActiveConversationMessagesBlocMapCubit': LogLevel.off, + 'DHTShortArrayCubit': LogLevel.off, }; const Map _blocCreateCloseLogLevels = {}; const Map _blocErrorLogLevels = {}; diff --git a/lib/tools/widget_helpers.dart b/lib/tools/widget_helpers.dart index 61485a7..0e9928e 100644 --- a/lib/tools/widget_helpers.dart +++ b/lib/tools/widget_helpers.dart @@ -132,17 +132,20 @@ void showInfoToast(BuildContext context, String message) { ).show(context); } -Widget styledTitleContainer( - {required BuildContext context, - required String title, - required Widget child}) { +Widget styledTitleContainer({ + required BuildContext context, + required String title, + required Widget child, + Color? borderColor, + Color? backgroundColor, +}) { final theme = Theme.of(context); final scale = theme.extension()!; final textTheme = theme.textTheme; return DecoratedBox( decoration: ShapeDecoration( - color: scale.primaryScale.border, + color: borderColor ?? scale.primaryScale.border, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), )), @@ -154,7 +157,8 @@ Widget styledTitleContainer( ).paddingLTRB(8, 8, 8, 8), DecoratedBox( decoration: ShapeDecoration( - color: scale.primaryScale.subtleBackground, + color: + backgroundColor ?? scale.primaryScale.subtleBackground, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), )), diff --git a/lib/veilid_processor/views/developer.dart b/lib/veilid_processor/views/developer.dart index 143da47..978ab8f 100644 --- a/lib/veilid_processor/views/developer.dart +++ b/lib/veilid_processor/views/developer.dart @@ -13,6 +13,7 @@ import 'package:quickalert/quickalert.dart'; import 'package:veilid_support/veilid_support.dart'; import 'package:xterm/xterm.dart'; +import '../../layout/layout.dart'; import '../../theme/theme.dart'; import '../../tools/tools.dart'; @@ -127,15 +128,16 @@ class _DeveloperPageState extends State { // }); return Scaffold( - appBar: AppBar( + appBar: DefaultAppBar( + title: Text(translate('developer.title')), leading: IconButton( - icon: Icon(Icons.arrow_back, color: scale.primaryScale.text), + icon: Icon(Icons.arrow_back, color: scale.primaryScale.appText), onPressed: () => GoRouterHelper(context).pop(), ), actions: [ IconButton( icon: const Icon(Icons.copy), - color: scale.primaryScale.text, + color: scale.primaryScale.appText, disabledColor: scale.grayScale.subtleText, onPressed: _terminalController.selection == null ? null @@ -144,14 +146,14 @@ class _DeveloperPageState extends State { }), IconButton( icon: const Icon(Icons.clear_all), - color: scale.primaryScale.text, + color: scale.primaryScale.appText, disabledColor: scale.grayScale.subtleText, onPressed: () async { await QuickAlert.show( context: context, type: QuickAlertType.confirm, title: translate('developer.are_you_sure_clear'), - textColor: scale.primaryScale.text, + textColor: scale.primaryScale.appText, confirmBtnColor: scale.primaryScale.elementBackground, backgroundColor: scale.primaryScale.subtleBackground, headerBackgroundColor: scale.primaryScale.background, @@ -181,7 +183,7 @@ class _DeveloperPageState extends State { height: 40, render: ResultRender.icon, textStyle: textTheme.labelMedium! - .copyWith(color: scale.primaryScale.text), + .copyWith(color: scale.primaryScale.appText), padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), openBoxDecoration: BoxDecoration( color: scale.primaryScale.activeElementBackground), @@ -201,9 +203,9 @@ class _DeveloperPageState extends State { align: DropdownTriangleAlign.right), dropdownItemOptions: DropdownItemOptions( selectedTextStyle: textTheme.labelMedium! - .copyWith(color: scale.primaryScale.text), + .copyWith(color: scale.primaryScale.appText), textStyle: textTheme.labelMedium! - .copyWith(color: scale.primaryScale.text), + .copyWith(color: scale.primaryScale.appText), selectedBoxDecoration: BoxDecoration( color: scale.primaryScale.activeElementBackground), mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -212,10 +214,6 @@ class _DeveloperPageState extends State { dropdownList: _logLevelDropdownItems, ) ], - title: Text(translate('developer.title'), - style: - textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.bold)), - centerTitle: true, ), body: SafeArea( child: Column(children: [ diff --git a/lib/veilid_processor/views/signal_strength_meter.dart b/lib/veilid_processor/views/signal_strength_meter.dart index 1b94f78..eaf5022 100644 --- a/lib/veilid_processor/views/signal_strength_meter.dart +++ b/lib/veilid_processor/views/signal_strength_meter.dart @@ -33,27 +33,27 @@ class SignalStrengthMeterWidget extends StatelessWidget { switch (connectionState.attachment.state) { case AttachmentState.detached: iconWidget = Icon(Icons.signal_cellular_nodata, - size: iconSize, color: scale.grayScale.text); + size: iconSize, color: scale.grayScale.appText); return; case AttachmentState.detaching: iconWidget = Icon(Icons.signal_cellular_off, - size: iconSize, color: scale.grayScale.text); + size: iconSize, color: scale.grayScale.appText); return; case AttachmentState.attaching: value = 0; - color = scale.primaryScale.text; + color = scale.primaryScale.appText; case AttachmentState.attachedWeak: value = 1; - color = scale.primaryScale.text; + color = scale.primaryScale.appText; case AttachmentState.attachedStrong: value = 2; - color = scale.primaryScale.text; + color = scale.primaryScale.appText; case AttachmentState.attachedGood: value = 3; - color = scale.primaryScale.text; + color = scale.primaryScale.appText; case AttachmentState.fullyAttached: value = 4; - color = scale.primaryScale.text; + color = scale.primaryScale.appText; case AttachmentState.overAttached: value = 4; color = scale.secondaryScale.subtleText; diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 97ddd73..7130dd5 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -424,7 +424,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = XP5LBLT7M7; @@ -562,7 +562,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = XP5LBLT7M7; @@ -594,7 +594,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = XP5LBLT7M7; diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index cff5a4b..c55503e 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -6,11 +6,15 @@ com.apple.security.cs.allow-jit - com.apple.security.network.server + com.apple.security.files.user-selected.read-write com.apple.security.network.client - com.apple.security.files.user-selected.read-write + com.apple.security.network.server + keychain-access-groups + + $(AppIdentifierPrefix)com.veilid.veilidchat + diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements index f822844..d5ed4c2 100644 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -4,11 +4,15 @@ com.apple.security.app-sandbox - com.apple.security.network.server + com.apple.security.files.user-selected.read-write com.apple.security.network.client - com.apple.security.files.user-selected.read-write + com.apple.security.network.server + keychain-access-groups + + $(AppIdentifierPrefix)com.veilid.veilidchat + diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index f4835ef..00f3cdd 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -723,7 +723,7 @@ packages: path: "../../../veilid/veilid-flutter" relative: true source: path - version: "0.3.0" + version: "0.3.1" vm_service: dependency: transitive description: From 23ec185324d7464bbaa0e3db9471c0b89a5595a7 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 10 Apr 2024 16:13:08 -0400 Subject: [PATCH 083/270] ui cleanup --- assets/i18n/en.json | 7 +- .../new_account_page/new_account_page.dart | 2 + lib/app.dart | 62 ++++++++++--- lib/chat/views/new_chat_bottom_sheet.dart | 71 ++++++++++++++ lib/chat/views/views.dart | 1 + .../chat_single_contact_list_widget.dart | 81 +++++++--------- .../views/create_invitation_dialog.dart | 2 +- .../views/new_contact_bottom_sheet.dart | 72 ++++++++++++++ .../new_contact_invitation_bottom_sheet.dart | 66 ------------- .../views/paste_invitation_dialog.dart | 1 - lib/contact_invitation/views/views.dart | 2 +- lib/contacts/views/contact_item_widget.dart | 7 +- lib/contacts/views/contact_list_widget.dart | 73 ++++++--------- .../home_account_ready_chat.dart | 7 +- .../main_pager/account_page.dart | 3 - .../main_pager/chats_page.dart | 3 - .../main_pager/main_pager.dart | 17 +--- lib/layout/home/home_shell.dart | 13 +-- lib/settings/settings_page.dart | 8 +- lib/theme/models/radix_generator.dart | 93 ++++++++++++++++--- lib/theme/views/brightness_preferences.dart | 2 +- lib/theme/views/color_preferences.dart | 3 +- lib/tick.dart | 13 +-- lib/tools/widget_helpers.dart | 39 +++++++- lib/veilid_processor/views/developer.dart | 12 +++ packages/veilid_support/lib/src/config.dart | 3 +- 26 files changed, 419 insertions(+), 244 deletions(-) create mode 100644 lib/chat/views/new_chat_bottom_sheet.dart create mode 100644 lib/contact_invitation/views/new_contact_bottom_sheet.dart delete mode 100644 lib/contact_invitation/views/new_contact_invitation_bottom_sheet.dart diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 2551515..ed96192 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -58,12 +58,15 @@ "account_page": { "contact_invitations": "Contact Invitations" }, - "accounts_menu": { - "invite_contact": "Invite Contact", + "add_contact_sheet": { + "new_contact": "New Contact", "create_invite": "Create Invitation", "scan_invite": "Scan Invitation", "paste_invite": "Paste Invitation" }, + "add_chat_sheet": { + "new_chat": "New Chat" + }, "create_invitation_dialog": { "title": "Create Contact Invitation", "connect_with_me": "Connect with me on VeilidChat!", 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 f81f30d..53ba671 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 @@ -113,7 +113,9 @@ class NewAccountPageState extends State { body: _newAccountForm( context, onSubmit: (formKey) async { + // dismiss the keyboard by unfocusing the textfield FocusScope.of(context).unfocus(); + try { final name = _formKey.currentState!.fields[formFieldName]!.value as String; diff --git a/lib/app.dart b/lib/app.dart index 9dcab2f..e0c08c5 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,6 +1,7 @@ import 'package:animated_theme_switcher/animated_theme_switcher.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_localizations/flutter_localizations.dart'; import 'package:flutter_translate/flutter_translate.dart'; @@ -12,9 +13,15 @@ import 'init.dart'; import 'layout/splash.dart'; import 'router/router.dart'; import 'settings/settings.dart'; +import 'theme/models/theme_preference.dart'; import 'tick.dart'; +import 'tools/loggy.dart'; import 'veilid_processor/veilid_processor.dart'; +class ReloadThemeIntent extends Intent { + const ReloadThemeIntent(); +} + class VeilidChatApp extends StatelessWidget { const VeilidChatApp({ required this.initialThemeData, @@ -25,6 +32,28 @@ class VeilidChatApp extends StatelessWidget { final ThemeData initialThemeData; + void _reloadTheme(BuildContext context) { + log.info('Reloading theme'); + final theme = + PreferencesRepository.instance.value.themePreferences.themeData(); + ThemeSwitcher.of(context).changeTheme(theme: theme); + } + + Widget _buildShortcuts( + {required BuildContext context, + required Widget Function(BuildContext) builder}) => + ThemeSwitcher( + builder: (context) => Shortcuts( + shortcuts: { + LogicalKeySet( + LogicalKeyboardKey.alt, LogicalKeyboardKey.keyR): + const ReloadThemeIntent(), + }, + child: Actions(actions: >{ + ReloadThemeIntent: CallbackAction( + onInvoke: (intent) => _reloadTheme(context)), + }, child: Focus(autofocus: true, child: builder(context))))); + @override Widget build(BuildContext context) => FutureProvider( initialData: null, @@ -68,21 +97,24 @@ class VeilidChatApp extends StatelessWidget { ) ], child: BackgroundTicker( - builder: (context) => MaterialApp.router( - debugShowCheckedModeBanner: false, - routerConfig: context.watch().router(), - title: translate('app.title'), - theme: theme, - localizationsDelegates: [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - FormBuilderLocalizations.delegate, - localizationDelegate - ], - supportedLocales: - localizationDelegate.supportedLocales, - locale: localizationDelegate.currentLocale, - )), + child: _buildShortcuts( + context: context, + builder: (context) => MaterialApp.router( + debugShowCheckedModeBanner: false, + routerConfig: + context.watch().router(), + title: translate('app.title'), + theme: theme, + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + FormBuilderLocalizations.delegate, + localizationDelegate + ], + supportedLocales: + localizationDelegate.supportedLocales, + locale: localizationDelegate.currentLocale, + ))), )), ); }); diff --git a/lib/chat/views/new_chat_bottom_sheet.dart b/lib/chat/views/new_chat_bottom_sheet.dart new file mode 100644 index 0000000..e900b6c --- /dev/null +++ b/lib/chat/views/new_chat_bottom_sheet.dart @@ -0,0 +1,71 @@ +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 '../../tools/tools.dart'; + +Widget newChatBottomSheetBuilder( + BuildContext sheetContext, BuildContext context) { + final theme = Theme.of(sheetContext); + final scale = theme.extension()!; + + return KeyboardListener( + focusNode: FocusNode(), + onKeyEvent: (ke) { + if (ke.logicalKey == LogicalKeyboardKey.escape) { + Navigator.pop(sheetContext); + } + }, + child: styledBottomSheet( + context: context, + title: translate('add_chat_sheet.new_chat'), + child: SizedBox( + height: 160, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( + 'Group and custom chat functionality is not available yet') + // Column(children: [ + // IconButton( + // onPressed: () async { + // Navigator.pop(sheetContext); + // await CreateInvitationDialog.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(sheetContext); + // await ScanInvitationDialog.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(sheetContext); + // await PasteInvitationDialog.show(context); + // }, + // iconSize: 64, + // icon: const Icon(Icons.paste), + // color: scale.primaryScale.background), + // Text( + // translate('accounts_menu.paste_invite'), + // ) + // ]) + ]).paddingAll(16)))); +} diff --git a/lib/chat/views/views.dart b/lib/chat/views/views.dart index 6230e65..1999862 100644 --- a/lib/chat/views/views.dart +++ b/lib/chat/views/views.dart @@ -1,3 +1,4 @@ export 'chat_component.dart'; export 'empty_chat_widget.dart'; +export 'new_chat_bottom_sheet.dart'; export 'no_conversation_widget.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 c689e22..0e212dd 100644 --- a/lib/chat_list/views/chat_single_contact_list_widget.dart +++ b/lib/chat_list/views/chat_single_contact_list_widget.dart @@ -7,7 +7,6 @@ import 'package:searchable_listview/searchable_listview.dart'; import '../../contacts/contacts.dart'; import '../../proto/proto.dart' as proto; -import '../../theme/theme.dart'; import '../../tools/tools.dart'; import '../chat_list.dart'; @@ -17,10 +16,6 @@ class ChatSingleContactListWidget extends StatelessWidget { @override // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { - final theme = Theme.of(context); - //final textTheme = theme.textTheme; - final scale = theme.extension()!; - final contactListV = context.watch().state; return contactListV.builder((context, contactList) { @@ -29,55 +24,49 @@ class ChatSingleContactListWidget extends StatelessWidget { valueMapper: (c) => c); final chatListV = context.watch().state; - return chatListV.builder((context, chatList) => SizedBox.expand( + 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( - initialList: chatList.toList(), - builder: (l, i, c) { + child: (chatList.isEmpty) + ? const EmptyChatListWidget() + : SearchableList( + initialList: chatList.toList(), + builder: (l, i, c) { + final contact = + contactMap[c.remoteConversationRecordKey]; + if (contact == null) { + return const Text('...'); + } + return ChatSingleContactItemWidget( + contact: contact, + disabled: contactListV.busy); + }, + filter: (value) { + final lowerValue = value.toLowerCase(); + return chatList.where((c) { final contact = contactMap[c.remoteConversationRecordKey]; if (contact == null) { - return const Text('...'); + return false; } - return ChatSingleContactItemWidget( - contact: contact, - disabled: contactListV.busy); - }, - filter: (value) { - final lowerValue = value.toLowerCase(); - return chatList.where((c) { - final contact = - contactMap[c.remoteConversationRecordKey]; - 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.elementBackground, - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: scale.primaryScale.hoverBorder, - ), - borderRadius: BorderRadius.circular(8), - ), - ), - ).paddingAll(8)))) - .paddingLTRB(8, 0, 8, 8)); + return contact.editedProfile.name + .toLowerCase() + .contains(lowerValue) || + contact.editedProfile.pronouns + .toLowerCase() + .contains(lowerValue); + }).toList(); + }, + spaceBetweenSearchAndList: 4, + inputDecoration: InputDecoration( + labelText: translate('chat_list.search'), + ), + ), + ).paddingAll(8)))) + .paddingLTRB(8, 0, 8, 8); }); } } diff --git a/lib/contact_invitation/views/create_invitation_dialog.dart b/lib/contact_invitation/views/create_invitation_dialog.dart index be814a0..8365744 100644 --- a/lib/contact_invitation/views/create_invitation_dialog.dart +++ b/lib/contact_invitation/views/create_invitation_dialog.dart @@ -182,7 +182,7 @@ class CreateInvitationDialogState extends State { LengthLimitingTextInputFormatter(128), ], decoration: InputDecoration( - border: const OutlineInputBorder(), + //border: const OutlineInputBorder(), hintText: translate('create_invitation_dialog.enter_message_hint'), labelText: translate('create_invitation_dialog.message')), diff --git a/lib/contact_invitation/views/new_contact_bottom_sheet.dart b/lib/contact_invitation/views/new_contact_bottom_sheet.dart new file mode 100644 index 0000000..32bd725 --- /dev/null +++ b/lib/contact_invitation/views/new_contact_bottom_sheet.dart @@ -0,0 +1,72 @@ +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 '../../tools/tools.dart'; +import 'create_invitation_dialog.dart'; +import 'paste_invitation_dialog.dart'; +import 'scan_invitation_dialog.dart'; + +Widget newContactBottomSheetBuilder( + BuildContext sheetContext, BuildContext context) { + final theme = Theme.of(sheetContext); + final scale = theme.extension()!; + + return KeyboardListener( + focusNode: FocusNode(), + onKeyEvent: (ke) { + if (ke.logicalKey == LogicalKeyboardKey.escape) { + Navigator.pop(sheetContext); + } + }, + child: styledBottomSheet( + context: context, + title: translate('add_contact_sheet.new_contact'), + child: SizedBox( + height: 160, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column(children: [ + IconButton( + onPressed: () async { + Navigator.pop(sheetContext); + await CreateInvitationDialog.show(context); + }, + iconSize: 64, + icon: const Icon(Icons.contact_page), + color: scale.primaryScale.background), + Text( + translate('add_contact_sheet.create_invite'), + ) + ]), + Column(children: [ + IconButton( + onPressed: () async { + Navigator.pop(sheetContext); + await ScanInvitationDialog.show(context); + }, + iconSize: 64, + icon: const Icon(Icons.qr_code_scanner), + color: scale.primaryScale.background), + Text( + translate('add_contact_sheet.scan_invite'), + ) + ]), + Column(children: [ + IconButton( + onPressed: () async { + Navigator.pop(sheetContext); + await PasteInvitationDialog.show(context); + }, + iconSize: 64, + icon: const Icon(Icons.paste), + color: scale.primaryScale.background), + Text( + translate('add_contact_sheet.paste_invite'), + ) + ]) + ]).paddingAll(16)))); +} diff --git a/lib/contact_invitation/views/new_contact_invitation_bottom_sheet.dart b/lib/contact_invitation/views/new_contact_invitation_bottom_sheet.dart deleted file mode 100644 index 8228245..0000000 --- a/lib/contact_invitation/views/new_contact_invitation_bottom_sheet.dart +++ /dev/null @@ -1,66 +0,0 @@ -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_invitation_dialog.dart'; -import 'scan_invitation_dialog.dart'; -import 'create_invitation_dialog.dart'; - -Widget newContactInvitationBottomSheetBuilder( - BuildContext sheetContext, BuildContext context) { - final theme = Theme.of(sheetContext); - final textTheme = theme.textTheme; - final scale = theme.extension()!; - - return KeyboardListener( - focusNode: FocusNode(), - onKeyEvent: (ke) { - if (ke.logicalKey == LogicalKeyboardKey.escape) { - Navigator.pop(sheetContext); - } - }, - 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(sheetContext); - await CreateInvitationDialog.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(sheetContext); - await ScanInvitationDialog.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(sheetContext); - await PasteInvitationDialog.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_invitation_dialog.dart b/lib/contact_invitation/views/paste_invitation_dialog.dart index 3e19c1c..ead492b 100644 --- a/lib/contact_invitation/views/paste_invitation_dialog.dart +++ b/lib/contact_invitation/views/paste_invitation_dialog.dart @@ -125,7 +125,6 @@ class PasteInvitationDialogState extends State { maxLines: null, controller: _pasteTextController, decoration: const InputDecoration( - border: OutlineInputBorder(), hintText: '--- BEGIN VEILIDCHAT CONTACT INVITE ----\n' 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n' '---- END VEILIDCHAT CONTACT INVITE -----\n', diff --git a/lib/contact_invitation/views/views.dart b/lib/contact_invitation/views/views.dart index 3a7b8ec..726f0b9 100644 --- a/lib/contact_invitation/views/views.dart +++ b/lib/contact_invitation/views/views.dart @@ -3,6 +3,6 @@ export 'contact_invitation_item_widget.dart'; export 'contact_invitation_list_widget.dart'; export 'create_invitation_dialog.dart'; export 'invitation_dialog.dart'; -export 'new_contact_invitation_bottom_sheet.dart'; +export 'new_contact_bottom_sheet.dart'; export 'paste_invitation_dialog.dart'; export 'scan_invitation_dialog.dart'; diff --git a/lib/contacts/views/contact_item_widget.dart b/lib/contacts/views/contact_item_widget.dart index 1306ef3..9016212 100644 --- a/lib/contacts/views/contact_item_widget.dart +++ b/lib/contacts/views/contact_item_widget.dart @@ -29,14 +29,15 @@ class ContactItemWidget extends StatelessWidget { final remoteConversationKey = contact.remoteConversationRecordKey.toVeilid(); - const selected = - false; // xxx: eventually when we have selectable contacts: activeContactCubit.state == remoteConversationRecordKey; + const selected = false; // xxx: eventually when we have selectable contacts: + // activeContactCubit.state == remoteConversationRecordKey; return Container( margin: const EdgeInsets.fromLTRB(0, 4, 0, 0), clipBehavior: Clip.antiAlias, decoration: ShapeDecoration( color: selected + // ignore: dead_code ? scale.primaryScale.activeElementBackground : scale.primaryScale.hoverElementBackground, shape: RoundedRectangleBorder( @@ -102,9 +103,11 @@ class ContactItemWidget extends StatelessWidget { ? Text(contact.editedProfile.pronouns) : null, iconColor: selected + // ignore: dead_code ? scale.primaryScale.appText : scale.primaryScale.subtleText, textColor: selected + // ignore: dead_code ? scale.primaryScale.appText : scale.primaryScale.subtleText, selectedColor: scale.primaryScale.appText, diff --git a/lib/contacts/views/contact_list_widget.dart b/lib/contacts/views/contact_list_widget.dart index f5b775b..7fc0a08 100644 --- a/lib/contacts/views/contact_list_widget.dart +++ b/lib/contacts/views/contact_list_widget.dart @@ -6,7 +6,6 @@ import 'package:flutter_translate/flutter_translate.dart'; import 'package:searchable_listview/searchable_listview.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'; @@ -26,47 +25,33 @@ class ContactListWidget extends StatelessWidget { } @override - 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('contact_list.title'), - child: SizedBox.expand( - child: (contactList.isEmpty) - ? const EmptyContactListWidget() - : SearchableList( - initialList: contactList.toList(), - builder: (l, i, c) => - ContactItemWidget(contact: c, disabled: disabled), - filter: (value) { - final lowerValue = value.toLowerCase(); - return contactList - .where((element) => - element.editedProfile.name - .toLowerCase() - .contains(lowerValue) || - element.editedProfile.pronouns - .toLowerCase() - .contains(lowerValue)) - .toList(); - }, - spaceBetweenSearchAndList: 4, - inputDecoration: InputDecoration( - labelText: translate('contact_list.search'), - contentPadding: const EdgeInsets.all(2), - fillColor: scale.primaryScale.appText, - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: scale.primaryScale.hoverBorder, - ), - borderRadius: BorderRadius.circular(8), - ), - ), - ).paddingAll(8), - ))).paddingLTRB(8, 0, 8, 8); - } + Widget build(BuildContext context) => SizedBox.expand( + child: styledTitleContainer( + context: context, + title: translate('contact_list.title'), + child: SizedBox.expand( + child: (contactList.isEmpty) + ? const EmptyContactListWidget() + : SearchableList( + initialList: contactList.toList(), + builder: (l, i, c) => + ContactItemWidget(contact: c, disabled: disabled), + filter: (value) { + final lowerValue = value.toLowerCase(); + return contactList + .where((element) => + element.editedProfile.name + .toLowerCase() + .contains(lowerValue) || + element.editedProfile.pronouns + .toLowerCase() + .contains(lowerValue)) + .toList(); + }, + spaceBetweenSearchAndList: 4, + inputDecoration: InputDecoration( + labelText: translate('contact_list.search'), + ), + ).paddingAll(8), + ))).paddingLTRB(8, 0, 8, 8); } 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 ffeaa05..621f9e8 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 @@ -12,8 +12,6 @@ class HomeAccountReadyChat extends StatefulWidget { } class HomeAccountReadyChatState extends State { - final _unfocusNode = FocusNode(); - @override void initState() { super.initState(); @@ -26,7 +24,6 @@ class HomeAccountReadyChatState extends State { @override void dispose() { - _unfocusNode.dispose(); super.dispose(); } @@ -42,8 +39,6 @@ class HomeAccountReadyChatState extends State { @override Widget build(BuildContext context) => SafeArea( - child: GestureDetector( - onTap: () => FocusScope.of(context).requestFocus(_unfocusNode), child: buildChatComponent(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 b97ebe1..3d0cfad 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 @@ -18,8 +18,6 @@ class AccountPage extends StatefulWidget { } class AccountPageState extends State { - final _unfocusNode = FocusNode(); - @override void initState() { super.initState(); @@ -27,7 +25,6 @@ class AccountPageState extends State { @override void dispose() { - _unfocusNode.dispose(); super.dispose(); } 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 1c7e7fe..bdea8e3 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 @@ -11,8 +11,6 @@ class ChatsPage extends StatefulWidget { } class ChatsPageState extends State { - final _unfocusNode = FocusNode(); - @override void initState() { super.initState(); @@ -20,7 +18,6 @@ class ChatsPageState extends State { @override void dispose() { - _unfocusNode.dispose(); super.dispose(); } 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 0e121a4..8c03eda 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 @@ -5,9 +5,9 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_animate/flutter_animate.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 '../../../../chat/chat.dart'; import '../../../../contact_invitation/contact_invitation.dart'; import '../../../../theme/theme.dart'; import '../../../../tools/tools.dart'; @@ -28,8 +28,6 @@ class MainPager extends StatefulWidget { class MainPagerState extends State with TickerProviderStateMixin { ////////////////////////////////////////////////////////////////// - final _unfocusNode = FocusNode(); - var _currentPage = 0; final pageController = PreloadPageController(); @@ -56,7 +54,6 @@ class MainPagerState extends State with TickerProviderStateMixin { @override void dispose() { - _unfocusNode.dispose(); pageController.dispose(); super.dispose(); } @@ -127,21 +124,13 @@ class MainPagerState extends State with TickerProviderStateMixin { }); } - 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 sheetContext, BuildContext context) { if (_currentPage == 0) { // New contact invitation - return newContactInvitationBottomSheetBuilder(sheetContext, context); + return newContactBottomSheetBuilder(sheetContext, context); } else if (_currentPage == 1) { // New chat - return _onNewChatBottomSheetBuilder(sheetContext, context); + return newChatBottomSheetBuilder(sheetContext, context); } else { // Unknown error return debugPage('unknown page'); diff --git a/lib/layout/home/home_shell.dart b/lib/layout/home/home_shell.dart index bd1949c..8851730 100644 --- a/lib/layout/home/home_shell.dart +++ b/lib/layout/home/home_shell.dart @@ -19,8 +19,6 @@ class HomeShell extends StatefulWidget { } class HomeShellState extends State { - final _unfocusNode = FocusNode(); - @override void initState() { super.initState(); @@ -28,7 +26,6 @@ class HomeShellState extends State { @override void dispose() { - _unfocusNode.dispose(); super.dispose(); } @@ -69,11 +66,9 @@ class HomeShellState extends State { // 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: buildWithLogin(context)))); + child: DecoratedBox( + decoration: BoxDecoration( + color: scale.primaryScale.activeElementBackground), + child: buildWithLogin(context))); } } diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart index c84e7d7..5eb89fb 100644 --- a/lib/settings/settings_page.dart +++ b/lib/settings/settings_page.dart @@ -53,11 +53,11 @@ class SettingsPageState extends State { child: ListView( children: [ buildSettingsPageColorPreferences( - onChanged: () => setState(() {})), + context: context, onChanged: () => setState(() {})), buildSettingsPageBrightnessPreferences( - onChanged: () => setState(() {})), - ], + context: context, onChanged: () => setState(() {})), + ].map((x) => x.paddingLTRB(0, 0, 0, 8)).toList(), ), - ).paddingSymmetric(horizontal: 24, vertical: 8), + ).paddingSymmetric(horizontal: 24, vertical: 16), ))); } diff --git a/lib/theme/models/radix_generator.dart b/lib/theme/models/radix_generator.dart index efc3bdb..415b628 100644 --- a/lib/theme/models/radix_generator.dart +++ b/lib/theme/models/radix_generator.dart @@ -1,7 +1,10 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_chat_ui/flutter_chat_ui.dart'; import 'package:radix_colors/radix_colors.dart'; +import '../../tools/tools.dart'; import 'scale_color.dart'; import 'scale_scheme.dart'; @@ -571,26 +574,27 @@ ColorScheme _scaleToColorScheme(Brightness brightness, ScaleScheme scale) => Colors.red, // scale.primaryScale.hoverElementBackground, onPrimaryContainer: Colors.green, //scale.primaryScale.subtleText, secondary: scale.secondaryScale.background, - onSecondary: scale.secondaryScale.appText, + onSecondary: scale.secondaryScale.foregroundText, secondaryContainer: scale.secondaryScale.hoverElementBackground, onSecondaryContainer: scale.secondaryScale.subtleText, tertiary: scale.tertiaryScale.background, - onTertiary: scale.tertiaryScale.appText, + onTertiary: scale.tertiaryScale.foregroundText, tertiaryContainer: scale.tertiaryScale.hoverElementBackground, onTertiaryContainer: scale.tertiaryScale.subtleText, error: scale.errorScale.background, - onError: scale.errorScale.appText, + onError: scale.errorScale.foregroundText, errorContainer: scale.errorScale.hoverElementBackground, onErrorContainer: scale.errorScale.subtleText, background: scale.grayScale.appBackground, // reviewed onBackground: scale.grayScale.appText, // reviewed - surface: scale.primaryScale.activeElementBackground, // reviewed - onSurface: scale.primaryScale.subtleText, // reviewed + surface: scale.primaryScale.background, // reviewed + onSurface: scale.primaryScale.foregroundText, // reviewed surfaceVariant: scale.primaryScale.elementBackground, - onSurfaceVariant: scale.primaryScale.subtleText, // ?? reviewed a little + onSurfaceVariant: + scale.primaryScale.foregroundText, // ?? reviewed a little outline: scale.primaryScale.border, outlineVariant: scale.primaryScale.subtleBorder, - shadow: RadixColors.dark.gray.step1, + shadow: const Color(0xFF000000), scrim: scale.primaryScale.background, inverseSurface: scale.primaryScale.subtleText, onInverseSurface: scale.primaryScale.subtleBackground, @@ -612,7 +616,7 @@ ChatTheme makeChatTheme(ScaleScheme scale, TextTheme textTheme) => contentPadding: const EdgeInsets.fromLTRB(8, 12, 8, 12), border: const OutlineInputBorder( borderSide: BorderSide.none, - borderRadius: BorderRadius.all(Radius.circular(16))), + borderRadius: BorderRadius.all(Radius.circular(8))), ), inputContainerDecoration: BoxDecoration(color: scale.primaryScale.border), @@ -641,10 +645,39 @@ ChatTheme makeChatTheme(ScaleScheme scale, TextTheme textTheme) => fontSize: 64, )); +TextTheme _makeTextTheme(Brightness brightness) { + late final TextTheme textTheme; + if (Platform.isIOS) { + textTheme = (brightness == Brightness.light) + ? Typography.blackCupertino + : Typography.whiteCupertino; + } else if (Platform.isMacOS) { + textTheme = (brightness == Brightness.light) + ? Typography.blackRedwoodCity + : Typography.whiteRedwoodCity; + } else if (Platform.isAndroid || Platform.isFuchsia) { + textTheme = (brightness == Brightness.light) + ? Typography.blackMountainView + : Typography.whiteMountainView; + } else if (Platform.isLinux) { + textTheme = (brightness == Brightness.light) + ? Typography.blackHelsinki + : Typography.whiteHelsinki; + } else if (Platform.isWindows) { + textTheme = (brightness == Brightness.light) + ? Typography.blackRedmond + : Typography.whiteRedmond; + } else { + log.warning('unknown platform'); + textTheme = (brightness == Brightness.light) + ? Typography.blackHelsinki + : Typography.whiteHelsinki; + } + return textTheme; +} + ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) { - final textTheme = (brightness == Brightness.light) - ? Typography.blackCupertino - : Typography.whiteCupertino; + final textTheme = _makeTextTheme(brightness); final radix = _radixScheme(brightness, themeColor); final scaleScheme = radix.toScale(); final colorScheme = _scaleToColorScheme(brightness, scaleScheme); @@ -655,8 +688,42 @@ ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) { bottomSheetTheme: themeData.bottomSheetTheme.copyWith( elevation: 0, modalElevation: 0, - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16)))), + canvasColor: scaleScheme.primaryScale.subtleBackground, + chipTheme: themeData.chipTheme.copyWith( + backgroundColor: scaleScheme.primaryScale.elementBackground, + selectedColor: scaleScheme.primaryScale.activeElementBackground, + surfaceTintColor: scaleScheme.primaryScale.hoverElementBackground, + checkmarkColor: scaleScheme.primaryScale.background, + side: BorderSide(color: scaleScheme.primaryScale.border)), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: scaleScheme.primaryScale.elementBackground, + foregroundColor: scaleScheme.primaryScale.appText, + disabledBackgroundColor: scaleScheme.grayScale.elementBackground, + disabledForegroundColor: scaleScheme.grayScale.appText, + shape: RoundedRectangleBorder( + side: BorderSide(color: scaleScheme.primaryScale.border), + borderRadius: BorderRadius.circular(8))), + ), + focusColor: scaleScheme.primaryScale.activeElementBackground, + hoverColor: scaleScheme.primaryScale.hoverElementBackground, + inputDecorationTheme: themeData.inputDecorationTheme.copyWith( + border: OutlineInputBorder( + borderSide: BorderSide(color: scaleScheme.primaryScale.border), + borderRadius: BorderRadius.circular(8)), + contentPadding: const EdgeInsets.all(8), + labelStyle: TextStyle( + color: scaleScheme.primaryScale.subtleText.withAlpha(127)), + floatingLabelStyle: + TextStyle(color: scaleScheme.primaryScale.subtleText), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: scaleScheme.primaryScale.hoverBorder, width: 2), + borderRadius: BorderRadius.circular(8))), extensions: >[ scaleScheme, ]); diff --git a/lib/theme/views/brightness_preferences.dart b/lib/theme/views/brightness_preferences.dart index 0c7ab10..2f3f410 100644 --- a/lib/theme/views/brightness_preferences.dart +++ b/lib/theme/views/brightness_preferences.dart @@ -22,7 +22,7 @@ List> _getBrightnessDropdownItems() { } Widget buildSettingsPageBrightnessPreferences( - {required void Function() onChanged}) { + {required BuildContext context, required void Function() onChanged}) { final preferencesRepository = PreferencesRepository.instance; final themePreferences = preferencesRepository.value.themePreferences; return ThemeSwitcher.withTheme( diff --git a/lib/theme/views/color_preferences.dart b/lib/theme/views/color_preferences.dart index a364e00..4a5219d 100644 --- a/lib/theme/views/color_preferences.dart +++ b/lib/theme/views/color_preferences.dart @@ -30,7 +30,8 @@ List> _getThemeDropdownItems() { .toList(); } -Widget buildSettingsPageColorPreferences({required void Function() onChanged}) { +Widget buildSettingsPageColorPreferences( + {required BuildContext context, required void Function() onChanged}) { final preferencesRepository = PreferencesRepository.instance; final themePreferences = preferencesRepository.value.themePreferences; return ThemeSwitcher.withTheme( diff --git a/lib/tick.dart b/lib/tick.dart index 279de11..197a1d6 100644 --- a/lib/tick.dart +++ b/lib/tick.dart @@ -1,24 +1,17 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:veilid_support/veilid_support.dart'; import 'veilid_processor/veilid_processor.dart'; class BackgroundTicker extends StatefulWidget { - const BackgroundTicker({required this.builder, super.key}); + const BackgroundTicker({required this.child, super.key}); - final Widget Function(BuildContext) builder; + final Widget child; @override BackgroundTickerState createState() => BackgroundTickerState(); - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(ObjectFlagProperty.has( - 'builder', builder)); - } } class BackgroundTickerState extends State { @@ -48,7 +41,7 @@ class BackgroundTickerState extends State { @override // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { - return widget.builder(context); + return widget.child; } Future _onTick() async { diff --git a/lib/tools/widget_helpers.dart b/lib/tools/widget_helpers.dart index 0e9928e..3d8be63 100644 --- a/lib/tools/widget_helpers.dart +++ b/lib/tools/widget_helpers.dart @@ -154,7 +154,7 @@ Widget styledTitleContainer({ title, style: textTheme.titleMedium! .copyWith(color: scale.primaryScale.subtleText), - ).paddingLTRB(8, 8, 8, 8), + ).paddingLTRB(8, 8, 8, 4), DecoratedBox( decoration: ShapeDecoration( color: @@ -168,6 +168,43 @@ Widget styledTitleContainer({ ])); } +Widget styledBottomSheet({ + required BuildContext context, + required String title, + required Widget child, + Color? borderColor, + Color? backgroundColor, +}) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final textTheme = theme.textTheme; + + return DecoratedBox( + decoration: ShapeDecoration( + color: borderColor ?? scale.primaryScale.border, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16)))), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Text( + title, + style: textTheme.titleMedium! + .copyWith(color: scale.primaryScale.subtleText), + ).paddingLTRB(8, 8, 8, 4), + DecoratedBox( + decoration: ShapeDecoration( + color: + backgroundColor ?? scale.primaryScale.subtleBackground, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16)))), + child: child) + .paddingLTRB(4, 4, 4, 0) + ])); +} + bool get isPlatformDark => WidgetsBinding.instance.platformDispatcher.platformBrightness == Brightness.dark; diff --git a/lib/veilid_processor/views/developer.dart b/lib/veilid_processor/views/developer.dart index 978ab8f..4e70ff9 100644 --- a/lib/veilid_processor/views/developer.dart +++ b/lib/veilid_processor/views/developer.dart @@ -80,6 +80,18 @@ class _DeveloperPageState extends State { return; } + if (debugCommand.startsWith('change_log_ignore ')) { + final args = debugCommand.split(' '); + if (args.length < 3) { + _debugOut('Incorrect number of arguments'); + return; + } + final layer = args[1]; + final changes = args[2].split(','); + Veilid.instance.changeLogIgnore(layer, changes); + return; + } + if (debugCommand == 'ellet') { setState(() { _showEllet = !_showEllet; diff --git a/packages/veilid_support/lib/src/config.dart b/packages/veilid_support/lib/src/config.dart index c13a8b8..889a8fd 100644 --- a/packages/veilid_support/lib/src/config.dart +++ b/packages/veilid_support/lib/src/config.dart @@ -1,6 +1,7 @@ -import 'package:veilid/veilid.dart'; import 'dart:io' show Platform; +import 'package:veilid/veilid.dart'; + Map getDefaultVeilidPlatformConfig( bool isWeb, String appName) { final ignoreLogTargetsStr = From 4f0243596496377398eca18b291a20b8fdf483be Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 12 Apr 2024 20:55:05 -0400 Subject: [PATCH 084/270] unify handling of themes accessible theming/high contrast support --- assets/i18n/en.json | 4 +- .../new_account_page/new_account_page.dart | 1 + lib/account_manager/views/profile_widget.dart | 7 +- lib/chat/views/chat_component.dart | 7 +- lib/chat/views/empty_chat_widget.dart | 55 +++--- lib/chat/views/new_chat_bottom_sheet.dart | 46 +---- .../chat_single_contact_item_widget.dart | 102 +++-------- .../chat_single_contact_list_widget.dart | 7 +- .../views/contact_invitation_display.dart | 1 + .../views/contact_invitation_item_widget.dart | 135 +++++--------- .../views/create_invitation_dialog.dart | 1 + .../views/invitation_dialog.dart | 1 + .../views/new_contact_bottom_sheet.dart | 7 +- .../views/scan_invitation_dialog.dart | 6 +- lib/contacts/views/contact_item_widget.dart | 132 +++++--------- lib/contacts/views/contact_list_widget.dart | 67 +++---- .../home_account_ready_main.dart | 6 +- .../home_account_ready_shell.dart | 2 +- .../main_pager/account_page.dart | 3 +- .../main_pager/main_pager.dart | 31 +--- lib/layout/home/home_no_active.dart | 2 +- lib/theme/models/chat_theme.dart | 54 ++++++ lib/theme/models/contrast_generator.dart | 83 +++++++++ lib/theme/models/models.dart | 2 + lib/theme/models/radix_generator.dart | 156 ++++------------- lib/theme/models/scale_color.dart | 75 +++++--- .../models/scale_input_decorator_theme.dart | 165 ++++++++++++++++++ lib/theme/models/scale_scheme.dart | 95 +++++++++- lib/theme/models/slider_tile.dart | 151 ++++++++++++++++ lib/theme/models/theme_preference.dart | 5 +- .../views}/scanner_error_widget.dart | 0 lib/{tools => theme/views}/styled_dialog.dart | 7 +- lib/theme/views/views.dart | 3 + .../views}/widget_helpers.dart | 48 +++-- lib/tools/enter_password.dart | 18 +- lib/tools/enter_pin.dart | 12 +- lib/tools/tools.dart | 3 - lib/veilid_processor/views/developer.dart | 56 ++++-- .../views/signal_strength_meter.dart | 20 +-- .../dht_short_array_cubit.dart | 2 +- .../dht_short_array/dht_short_array_head.dart | 2 + 41 files changed, 958 insertions(+), 622 deletions(-) create mode 100644 lib/theme/models/chat_theme.dart create mode 100644 lib/theme/models/contrast_generator.dart create mode 100644 lib/theme/models/scale_input_decorator_theme.dart create mode 100644 lib/theme/models/slider_tile.dart rename lib/{tools => theme/views}/scanner_error_widget.dart (100%) rename lib/{tools => theme/views}/styled_dialog.dart (90%) rename lib/{tools => theme/views}/widget_helpers.dart (85%) diff --git a/assets/i18n/en.json b/assets/i18n/en.json index ed96192..2a1e4ac 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -6,7 +6,6 @@ "settings_tooltip": "Settings" }, "pager": { - "account": "Account", "chats": "Chats", "contacts": "Contacts" }, @@ -67,6 +66,9 @@ "add_chat_sheet": { "new_chat": "New Chat" }, + "chat": { + "say_something": "Say Something" + }, "create_invitation_dialog": { "title": "Create Contact Invitation", "connect_with_me": "Connect with me on VeilidChat!", 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 53ba671..38664ba 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 @@ -7,6 +7,7 @@ import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:go_router/go_router.dart'; import '../../../layout/default_app_bar.dart'; +import '../../../theme/theme.dart'; import '../../../tools/tools.dart'; import '../../../veilid_processor/veilid_processor.dart'; import '../../account_manager.dart'; diff --git a/lib/account_manager/views/profile_widget.dart b/lib/account_manager/views/profile_widget.dart index 5d85014..ecb7c3d 100644 --- a/lib/account_manager/views/profile_widget.dart +++ b/lib/account_manager/views/profile_widget.dart @@ -31,11 +31,14 @@ class ProfileWidget extends StatelessWidget { child: Column(children: [ Text( _profile.name, - style: textTheme.headlineSmall, + style: textTheme.headlineSmall! + .copyWith(color: scale.primaryScale.borderText), textAlign: TextAlign.left, ).paddingAll(4), if (_profile.pronouns.isNotEmpty) - Text(_profile.pronouns, style: textTheme.bodyMedium) + Text(_profile.pronouns, + style: textTheme.bodyMedium! + .copyWith(color: scale.primaryScale.borderText)) .paddingLTRB(4, 0, 4, 4), ]), ); diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 32210a0..35a2987 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -13,7 +13,6 @@ import '../../chat_list/chat_list.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 StatelessWidget { @@ -173,11 +172,13 @@ class ChatComponent extends StatelessWidget { 16, 0, 16, 0), child: Text(_remoteUser.firstName!, textAlign: TextAlign.start, - style: textTheme.titleMedium), + style: textTheme.titleMedium!.copyWith( + color: scale.primaryScale.borderText)), )), const Spacer(), IconButton( - icon: const Icon(Icons.close), + icon: Icon(Icons.close, + color: scale.primaryScale.borderText), onPressed: () async { context.read().setActiveChat(null); }).paddingLTRB(16, 0, 16, 0) diff --git a/lib/chat/views/empty_chat_widget.dart b/lib/chat/views/empty_chat_widget.dart index a9072cd..c975722 100644 --- a/lib/chat/views/empty_chat_widget.dart +++ b/lib/chat/views/empty_chat_widget.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_translate/flutter_translate.dart'; + +import '../../theme/theme.dart'; class EmptyChatWidget extends StatelessWidget { const EmptyChatWidget({super.key}); @@ -7,28 +10,32 @@ class EmptyChatWidget extends StatelessWidget { // 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, - ), - ), - ], - ), - ); + ) { + final theme = Theme.of(context); + final scale = theme.extension()!; + + 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: scale.primaryScale.subtleBorder, + size: 48, + ), + Text( + translate('chat.say_something'), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: scale.primaryScale.subtleBorder, + ), + ), + ], + ), + ); + } } diff --git a/lib/chat/views/new_chat_bottom_sheet.dart b/lib/chat/views/new_chat_bottom_sheet.dart index e900b6c..646a3ec 100644 --- a/lib/chat/views/new_chat_bottom_sheet.dart +++ b/lib/chat/views/new_chat_bottom_sheet.dart @@ -4,12 +4,11 @@ import 'package:flutter/services.dart'; import 'package:flutter_translate/flutter_translate.dart'; import '../../theme/theme.dart'; -import '../../tools/tools.dart'; Widget newChatBottomSheetBuilder( BuildContext sheetContext, BuildContext context) { - final theme = Theme.of(sheetContext); - final scale = theme.extension()!; + //final theme = Theme.of(sheetContext); + //final scale = theme.extension()!; return KeyboardListener( focusNode: FocusNode(), @@ -23,49 +22,10 @@ Widget newChatBottomSheetBuilder( title: translate('add_chat_sheet.new_chat'), child: SizedBox( height: 160, - child: Row( + child: const Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Text( 'Group and custom chat functionality is not available yet') - // Column(children: [ - // IconButton( - // onPressed: () async { - // Navigator.pop(sheetContext); - // await CreateInvitationDialog.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(sheetContext); - // await ScanInvitationDialog.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(sheetContext); - // await PasteInvitationDialog.show(context); - // }, - // iconSize: 64, - // icon: const Icon(Icons.paste), - // color: scale.primaryScale.background), - // Text( - // translate('accounts_menu.paste_invite'), - // ) - // ]) ]).paddingAll(16)))); } 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 241791c..75501b4 100644 --- a/lib/chat_list/views/chat_single_contact_item_widget.dart +++ b/lib/chat_list/views/chat_single_contact_item_widget.dart @@ -1,8 +1,6 @@ import 'package:async_tools/async_tools.dart'; -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; @@ -25,85 +23,35 @@ class ChatSingleContactItemWidget extends StatelessWidget { Widget build( BuildContext context, ) { - final theme = Theme.of(context); - //final textTheme = theme.textTheme; - final scale = theme.extension()!; - final activeChatCubit = context.watch(); final remoteConversationRecordKey = _contact.remoteConversationRecordKey.toVeilid(); final selected = activeChatCubit.state == remoteConversationRecordKey; - return Container( - margin: const EdgeInsets.fromLTRB(0, 4, 0, 0), - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - color: selected - ? scale.primaryScale.activeElementBackground - : scale.primaryScale.hoverElementBackground, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - )), - child: Slidable( - key: ObjectKey(_contact), - endActionPane: ActionPane( - motion: const DrawerMotion(), - children: [ - SlidableAction( - onPressed: _disabled - ? null - : (context) async { - final chatListCubit = context.read(); - await chatListCubit.deleteChat( - remoteConversationRecordKey: - remoteConversationRecordKey); - }, - backgroundColor: scale.tertiaryScale.background, - foregroundColor: scale.tertiaryScale.foregroundText, - icon: Icons.delete, - label: translate('button.delete'), - padding: const EdgeInsets.all(2)), - // SlidableAction( - // onPressed: (context) => (), - // backgroundColor: scale.secondaryScale.background, - // foregroundColor: scale.secondaryScale.text, - // icon: Icons.edit, - // label: 'Edit', - // ), - ], - ), - - // The child of the Slidable is what the user sees when the - // component is not dragged. - child: ListTile( - onTap: _disabled - ? null - : () { - singleFuture(activeChatCubit, () async { - activeChatCubit - .setActiveChat(remoteConversationRecordKey); - }); - }, - title: Text(_contact.editedProfile.name), - - /// xxx show last message here - subtitle: (_contact.editedProfile.pronouns.isNotEmpty) - ? Text(_contact.editedProfile.pronouns) - : null, - iconColor: selected - ? scale.primaryScale.appText - : scale.primaryScale.subtleText, - textColor: selected - ? scale.primaryScale.appText - : scale.primaryScale.subtleText, - selectedColor: scale.primaryScale.appText, - //Text(Timestamp.fromInt64(contactInvitationRecord.expiration) / ), - leading: const Icon(Icons.chat)))); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('contact', _contact)); + return SliderTile( + key: ObjectKey(_contact), + disabled: _disabled, + selected: selected, + tileScale: ScaleKind.secondary, + title: _contact.editedProfile.name, + subtitle: _contact.editedProfile.pronouns, + icon: Icons.chat, + onTap: () { + singleFuture(activeChatCubit, () async { + activeChatCubit.setActiveChat(remoteConversationRecordKey); + }); + }, + endActions: [ + SliderTileAction( + icon: Icons.delete, + label: translate('button.delete'), + actionScale: ScaleKind.tertiary, + onPressed: (context) async { + final chatListCubit = context.read(); + await chatListCubit.deleteChat( + remoteConversationRecordKey: remoteConversationRecordKey); + }) + ], + ); } } 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 0e212dd..c1f54d8 100644 --- a/lib/chat_list/views/chat_single_contact_list_widget.dart +++ b/lib/chat_list/views/chat_single_contact_list_widget.dart @@ -7,7 +7,7 @@ import 'package:searchable_listview/searchable_listview.dart'; import '../../contacts/contacts.dart'; import '../../proto/proto.dart' as proto; -import '../../tools/tools.dart'; +import '../../theme/theme.dart'; import '../chat_list.dart'; class ChatSingleContactListWidget extends StatelessWidget { @@ -41,8 +41,9 @@ class ChatSingleContactListWidget extends StatelessWidget { return const Text('...'); } return ChatSingleContactItemWidget( - contact: contact, - disabled: contactListV.busy); + contact: contact, + disabled: contactListV.busy) + .paddingLTRB(0, 4, 0, 0); }, filter: (value) { final lowerValue = value.toLowerCase(); diff --git a/lib/contact_invitation/views/contact_invitation_display.dart b/lib/contact_invitation/views/contact_invitation_display.dart index 020d510..374a309 100644 --- a/lib/contact_invitation/views/contact_invitation_display.dart +++ b/lib/contact_invitation/views/contact_invitation_display.dart @@ -10,6 +10,7 @@ import 'package:flutter_translate/flutter_translate.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:veilid_support/veilid_support.dart'; +import '../../theme/theme.dart'; import '../../tools/tools.dart'; import '../contact_invitation.dart'; diff --git a/lib/contact_invitation/views/contact_invitation_item_widget.dart b/lib/contact_invitation/views/contact_invitation_item_widget.dart index fd00c0a..fcf021f 100644 --- a/lib/contact_invitation/views/contact_invitation_item_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_item_widget.dart @@ -1,7 +1,6 @@ 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'; @@ -28,99 +27,51 @@ class ContactInvitationItemWidget extends StatelessWidget { @override // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { - final theme = Theme.of(context); - //final textTheme = theme.textTheme; - final scale = theme.extension()!; + // final remoteConversationKey = + // contact.remoteConversationRecordKey.toVeilid(); - return Container( - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - color: scale.tertiaryScale.subtleBorder, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - )), - child: Slidable( - // Specify a key if the Slidable is dismissible. - key: ObjectKey(contactInvitationRecord), - endActionPane: ActionPane( - // A motion is a widget used to control how the pane animates. - motion: const DrawerMotion(), + const selected = + false; // xxx: eventually when we have selectable invitations: + // activeContactCubit.state == remoteConversationRecordKey; - // A pane can dismiss the Slidable. - //dismissible: DismissiblePane(onDismissed: () {}), + final tileDisabled = + disabled || context.watch().isBusy; - // All actions are defined in the children parameter. - children: [ - // A SlidableAction can have an icon and/or a label. - SlidableAction( - 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.appText, - icon: Icons.delete, - label: translate('button.delete'), - padding: const EdgeInsets.all(2)), - ], - ), - - // startActionPane: ActionPane( - // motion: const DrawerMotion(), - // children: [ - // SlidableAction( - // // An action can be bigger than the others. - // flex: 2, - // onPressed: (context) => (), - // backgroundColor: Color(0xFF7BC043), - // foregroundColor: Colors.white, - // icon: Icons.archive, - // label: 'Archive', - // ), - // SlidableAction( - // onPressed: (context) => (), - // backgroundColor: Color(0xFF0392CF), - // foregroundColor: Colors.white, - // icon: Icons.save, - // label: 'Save', - // ), - // ], - // ), - - // The child of the Slidable is what the user sees when the - // component is not dragged. - child: ListTile( - //title: Text(translate('contact_list.invitation')), - onTap: disabled - ? null - : () async { - if (!context.mounted) { - return; - } - await ContactInvitationDisplayDialog.show( - context: context, - message: contactInvitationRecord.message, - create: (context) => InvitationGeneratorCubit.value( - Uint8List.fromList( - contactInvitationRecord.invitation))); - }, - title: Text( - contactInvitationRecord.message.isEmpty - ? translate('contact_list.invitation') - : contactInvitationRecord.message, - softWrap: true, - ), - iconColor: scale.tertiaryScale.background, - textColor: scale.tertiaryScale.appText, - //Text(Timestamp.fromInt64(contactInvitationRecord.expiration) / ), - leading: const Icon(Icons.person_add)))); + return SliderTile( + key: ObjectKey(contactInvitationRecord), + disabled: tileDisabled, + selected: selected, + tileScale: ScaleKind.primary, + title: contactInvitationRecord.message.isEmpty + ? translate('contact_list.invitation') + : contactInvitationRecord.message, + icon: Icons.person_add, + onTap: () async { + if (!context.mounted) { + return; + } + await ContactInvitationDisplayDialog.show( + context: context, + message: contactInvitationRecord.message, + create: (context) => InvitationGeneratorCubit.value( + Uint8List.fromList(contactInvitationRecord.invitation))); + }, + endActions: [ + SliderTileAction( + icon: Icons.delete, + label: translate('button.delete'), + actionScale: ScaleKind.tertiary, + onPressed: (context) async { + final contactInvitationListCubit = + context.read(); + await contactInvitationListCubit.deleteInvitation( + accepted: false, + contactRequestInboxRecordKey: contactInvitationRecord + .contactRequestInbox.recordKey + .toVeilid()); + }, + ) + ], + ); } } diff --git a/lib/contact_invitation/views/create_invitation_dialog.dart b/lib/contact_invitation/views/create_invitation_dialog.dart index 8365744..ace71d5 100644 --- a/lib/contact_invitation/views/create_invitation_dialog.dart +++ b/lib/contact_invitation/views/create_invitation_dialog.dart @@ -10,6 +10,7 @@ import 'package:flutter_translate/flutter_translate.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; +import '../../theme/theme.dart'; import '../../tools/tools.dart'; import '../contact_invitation.dart'; diff --git a/lib/contact_invitation/views/invitation_dialog.dart b/lib/contact_invitation/views/invitation_dialog.dart index 4d304ab..60d8784 100644 --- a/lib/contact_invitation/views/invitation_dialog.dart +++ b/lib/contact_invitation/views/invitation_dialog.dart @@ -9,6 +9,7 @@ import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../contacts/contacts.dart'; +import '../../theme/theme.dart'; import '../../tools/tools.dart'; import '../contact_invitation.dart'; diff --git a/lib/contact_invitation/views/new_contact_bottom_sheet.dart b/lib/contact_invitation/views/new_contact_bottom_sheet.dart index 32bd725..a79a07f 100644 --- a/lib/contact_invitation/views/new_contact_bottom_sheet.dart +++ b/lib/contact_invitation/views/new_contact_bottom_sheet.dart @@ -4,7 +4,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_translate/flutter_translate.dart'; import '../../theme/theme.dart'; -import '../../tools/tools.dart'; import 'create_invitation_dialog.dart'; import 'paste_invitation_dialog.dart'; import 'scan_invitation_dialog.dart'; @@ -37,7 +36,7 @@ Widget newContactBottomSheetBuilder( }, iconSize: 64, icon: const Icon(Icons.contact_page), - color: scale.primaryScale.background), + color: scale.primaryScale.hoverBorder), Text( translate('add_contact_sheet.create_invite'), ) @@ -50,7 +49,7 @@ Widget newContactBottomSheetBuilder( }, iconSize: 64, icon: const Icon(Icons.qr_code_scanner), - color: scale.primaryScale.background), + color: scale.primaryScale.hoverBorder), Text( translate('add_contact_sheet.scan_invite'), ) @@ -63,7 +62,7 @@ Widget newContactBottomSheetBuilder( }, iconSize: 64, icon: const Icon(Icons.paste), - color: scale.primaryScale.background), + color: scale.primaryScale.hoverBorder), Text( translate('add_contact_sheet.paste_invite'), ) diff --git a/lib/contact_invitation/views/scan_invitation_dialog.dart b/lib/contact_invitation/views/scan_invitation_dialog.dart index 03f6101..44bb32e 100644 --- a/lib/contact_invitation/views/scan_invitation_dialog.dart +++ b/lib/contact_invitation/views/scan_invitation_dialog.dart @@ -211,7 +211,7 @@ class ScanInvitationDialogState extends State { scale.grayScale.subtleBackground); case TorchState.on: return Icon(Icons.flash_on, - color: scale.primaryScale.background); + color: scale.primaryScale.primary); } }, ), @@ -258,8 +258,8 @@ class ScanInvitationDialogState extends State { alignment: Alignment.topRight, child: IconButton( color: Colors.white, - icon: Icon(Icons.close, - color: scale.grayScale.background), + icon: + Icon(Icons.close, color: scale.grayScale.primary), iconSize: 32, onPressed: () => { SchedulerBinding.instance diff --git a/lib/contacts/views/contact_item_widget.dart b/lib/contacts/views/contact_item_widget.dart index 9016212..dfe9e6e 100644 --- a/lib/contacts/views/contact_item_widget.dart +++ b/lib/contacts/views/contact_item_widget.dart @@ -2,7 +2,6 @@ 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 '../../layout/layout.dart'; @@ -17,106 +16,65 @@ class ContactItemWidget extends StatelessWidget { final proto.Contact contact; final bool disabled; + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('contact', contact)) + ..add(DiagnosticsProperty('disabled', disabled)); + } + @override // ignore: prefer_expression_function_bodies Widget build( BuildContext context, ) { - final theme = Theme.of(context); - //final textTheme = theme.textTheme; - final scale = theme.extension()!; - final remoteConversationKey = contact.remoteConversationRecordKey.toVeilid(); const selected = false; // xxx: eventually when we have selectable contacts: // activeContactCubit.state == remoteConversationRecordKey; - return Container( - margin: const EdgeInsets.fromLTRB(0, 4, 0, 0), - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - color: selected - // ignore: dead_code - ? scale.primaryScale.activeElementBackground - : scale.primaryScale.hoverElementBackground, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - )), - child: Slidable( - key: ObjectKey(contact), - endActionPane: ActionPane( - motion: const DrawerMotion(), - children: [ - SlidableAction( - onPressed: disabled || context.watch().isBusy - ? null - : (context) async { - final contactListCubit = - context.read(); - final chatListCubit = context.read(); + final tileDisabled = disabled || context.watch().isBusy; - // Remove any chats for this contact - await chatListCubit.deleteChat( - remoteConversationRecordKey: - remoteConversationKey); + return SliderTile( + key: ObjectKey(contact), + disabled: tileDisabled, + selected: selected, + tileScale: ScaleKind.primary, + title: contact.editedProfile.name, + subtitle: contact.editedProfile.pronouns, + icon: Icons.person, + onTap: () async { + // Start a chat + final chatListCubit = context.read(); - // Delete the contact itself - await contactListCubit.deleteContact( - contact: contact); - }, - backgroundColor: scale.tertiaryScale.background, - foregroundColor: scale.tertiaryScale.appText, - icon: Icons.delete, - label: translate('button.delete'), - padding: const EdgeInsets.all(2)), - // SlidableAction( - // onPressed: (context) => (), - // backgroundColor: scale.secondaryScale.background, - // foregroundColor: scale.secondaryScale.text, - // icon: Icons.edit, - // label: 'Edit', - // ), - ], - ), + 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); + } + }, + endActions: [ + SliderTileAction( + icon: Icons.delete, + label: translate('button.delete'), + actionScale: ScaleKind.tertiary, + onPressed: (context) async { + final contactListCubit = context.read(); + final chatListCubit = context.read(); - // The child of the Slidable is what the user sees when the - // component is not dragged. - child: ListTile( - onTap: disabled || context.watch().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) - : null, - iconColor: selected - // ignore: dead_code - ? scale.primaryScale.appText - : scale.primaryScale.subtleText, - textColor: selected - // ignore: dead_code - ? scale.primaryScale.appText - : scale.primaryScale.subtleText, - selectedColor: scale.primaryScale.appText, - leading: const Icon(Icons.person)))); - } + // Remove any chats for this contact + await chatListCubit.deleteChat( + remoteConversationRecordKey: remoteConversationKey); - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('contact', contact)); + // Delete the contact itself + await contactListCubit.deleteContact(contact: contact); + }) + ], + ); } } diff --git a/lib/contacts/views/contact_list_widget.dart b/lib/contacts/views/contact_list_widget.dart index 7fc0a08..4c83a92 100644 --- a/lib/contacts/views/contact_list_widget.dart +++ b/lib/contacts/views/contact_list_widget.dart @@ -6,7 +6,7 @@ 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 '../../theme/theme.dart'; import 'contact_item_widget.dart'; import 'empty_contact_list_widget.dart'; @@ -25,33 +25,40 @@ class ContactListWidget extends StatelessWidget { } @override - Widget build(BuildContext context) => SizedBox.expand( - child: styledTitleContainer( - context: context, - title: translate('contact_list.title'), - child: SizedBox.expand( - child: (contactList.isEmpty) - ? const EmptyContactListWidget() - : SearchableList( - initialList: contactList.toList(), - builder: (l, i, c) => - ContactItemWidget(contact: c, disabled: disabled), - filter: (value) { - final lowerValue = value.toLowerCase(); - return contactList - .where((element) => - element.editedProfile.name - .toLowerCase() - .contains(lowerValue) || - element.editedProfile.pronouns - .toLowerCase() - .contains(lowerValue)) - .toList(); - }, - spaceBetweenSearchAndList: 4, - inputDecoration: InputDecoration( - labelText: translate('contact_list.search'), - ), - ).paddingAll(8), - ))).paddingLTRB(8, 0, 8, 8); + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + + return SizedBox.expand( + child: styledTitleContainer( + context: context, + title: translate('contact_list.title'), + child: SizedBox.expand( + child: (contactList.isEmpty) + ? const EmptyContactListWidget() + : SearchableList( + initialList: contactList.toList(), + builder: (l, i, c) => + ContactItemWidget(contact: c, disabled: disabled) + .paddingLTRB(0, 4, 0, 0), + filter: (value) { + final lowerValue = value.toLowerCase(); + return contactList + .where((element) => + element.editedProfile.name + .toLowerCase() + .contains(lowerValue) || + element.editedProfile.pronouns + .toLowerCase() + .contains(lowerValue)) + .toList(); + }, + spaceBetweenSearchAndList: 4, + defaultSuffixIconColor: scale.primaryScale.border, + inputDecoration: InputDecoration( + labelText: translate('contact_list.search'), + ), + ).paddingAll(8), + ))).paddingLTRB(8, 0, 8, 8); + } } 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 fd5790f..e6ed99e 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 @@ -37,11 +37,11 @@ class _HomeAccountReadyMainState extends State { Row(children: [ IconButton( icon: const Icon(Icons.settings), - color: scale.secondaryScale.appText, + color: scale.secondaryScale.borderText, constraints: const BoxConstraints.expand(height: 64, width: 64), style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all(scale.secondaryScale.border), + backgroundColor: MaterialStateProperty.all( + scale.primaryScale.hoverBorder), shape: MaterialStateProperty.all( const RoundedRectangleBorder( borderRadius: 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 c508568..4ff44d2 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 @@ -11,7 +11,7 @@ import '../../../chat_list/chat_list.dart'; import '../../../contact_invitation/contact_invitation.dart'; import '../../../contacts/contacts.dart'; import '../../../router/router.dart'; -import '../../../tools/tools.dart'; +import '../../../theme/theme.dart'; class HomeAccountReadyShell extends StatefulWidget { factory HomeAccountReadyShell( 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 3d0cfad..304d534 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 @@ -61,8 +61,9 @@ class AccountPageState extends State { translate('account_page.contact_invitations'), textAlign: TextAlign.center, style: textTheme.titleMedium! - .copyWith(color: scale.primaryScale.subtleText), + .copyWith(color: scale.primaryScale.borderText), ), + iconColor: scale.primaryScale.borderText, initiallyExpanded: true, children: [ ContactInvitationListWidget( 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 8c03eda..cdd6ac5 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 @@ -10,7 +10,6 @@ import 'package:stylish_bottom_bar/stylish_bottom_bar.dart'; import '../../../../chat/chat.dart'; import '../../../../contact_invitation/contact_invitation.dart'; import '../../../../theme/theme.dart'; -import '../../../../tools/tools.dart'; import 'account_page.dart'; import 'bottom_sheet_action_button.dart'; import 'chats_page.dart'; @@ -41,7 +40,7 @@ class MainPagerState extends State with TickerProviderStateMixin { Icons.add_comment_sharp, ]; final _bottomLabelList = [ - translate('pager.account'), + translate('pager.contacts'), translate('pager.chats'), ]; @@ -82,12 +81,11 @@ class MainPagerState extends State with TickerProviderStateMixin { final scale = theme.extension()!; return BottomBarItem( title: Text(_bottomLabelList[index]), - icon: Icon(_selectedIconList[index], color: scale.primaryScale.appText), + icon: + Icon(_selectedIconList[index], color: scale.primaryScale.borderText), selectedIcon: - Icon(_selectedIconList[index], color: scale.primaryScale.appText), - backgroundColor: scale.primaryScale.appText, - //unSelectedColor: theme.colorScheme.primaryContainer, - //selectedColor: theme.colorScheme.primary, + Icon(_selectedIconList[index], color: scale.primaryScale.borderText), + backgroundColor: scale.primaryScale.borderText, //badge: const Text('9+'), //showBadge: true, ); @@ -169,21 +167,10 @@ class MainPagerState extends State with TickerProviderStateMixin { // ), bottomNavigationBar: StylishBottomBar( backgroundColor: scale.primaryScale.hoverBorder, - // gradient: LinearGradient( - // begin: Alignment.topCenter, - // end: Alignment.bottomCenter, - // colors: [ - // theme.colorScheme.primary, - // theme.colorScheme.primaryContainer, - // ]), - //borderRadius: BorderRadius.all(Radius.circular(16)), option: AnimatedBarOptions( - // iconSize: 32, - //barAnimation: BarAnimation.fade, - iconStyle: IconStyle.animated, inkEffect: true, - inkColor: scale.primaryScale.hoverBackground, - //opacity: 0.3, + inkColor: scale.primaryScale.hoverPrimary, + opacity: 0.3, ), items: _buildBottomBarItems(), hasNotch: true, @@ -198,11 +185,11 @@ class MainPagerState extends State with TickerProviderStateMixin { floatingActionButton: BottomSheetActionButton( shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(14))), - foregroundColor: scale.secondaryScale.appText, + foregroundColor: scale.secondaryScale.borderText, backgroundColor: scale.secondaryScale.hoverBorder, builder: (context) => Icon( _fabIconList[_currentPage], - color: scale.secondaryScale.appText, + color: scale.secondaryScale.borderText, ), bottomSheetBuilder: (sheetContext) => _bottomSheetBuilder(sheetContext, context)), diff --git a/lib/layout/home/home_no_active.dart b/lib/layout/home/home_no_active.dart index e61fe0e..b2671dc 100644 --- a/lib/layout/home/home_no_active.dart +++ b/lib/layout/home/home_no_active.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../tools/tools.dart'; +import '../../theme/theme.dart'; class HomeNoActive extends StatefulWidget { const HomeNoActive({super.key}); diff --git a/lib/theme/models/chat_theme.dart b/lib/theme/models/chat_theme.dart new file mode 100644 index 0000000..b6ef7ba --- /dev/null +++ b/lib/theme/models/chat_theme.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_chat_ui/flutter_chat_ui.dart'; + +import 'scale_scheme.dart'; + +ChatTheme makeChatTheme(ScaleScheme scale, TextTheme textTheme) => + DefaultChatTheme( + primaryColor: scale.primaryScale.calloutBackground, + secondaryColor: scale.secondaryScale.calloutBackground, + backgroundColor: scale.grayScale.appBackground, + sendButtonIcon: Image.asset( + 'assets/icon-send.png', + color: scale.primaryScale.borderText, + package: 'flutter_chat_ui', + ), + inputBackgroundColor: Colors.blue, + inputBorderRadius: BorderRadius.zero, + inputTextDecoration: InputDecoration( + filled: true, + fillColor: scale.primaryScale.elementBackground, + isDense: true, + contentPadding: const EdgeInsets.fromLTRB(8, 12, 8, 12), + border: const OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.all(Radius.circular(8))), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.all(Radius.circular(8))), + ), + inputContainerDecoration: + BoxDecoration(color: scale.primaryScale.border), + inputPadding: const EdgeInsets.all(9), + inputTextColor: scale.primaryScale.appText, + attachmentButtonIcon: const Icon(Icons.attach_file), + sentMessageBodyTextStyle: TextStyle( + color: scale.primaryScale.calloutText, + fontSize: 16, + fontWeight: FontWeight.w500, + height: 1.5, + ), + sentEmojiMessageTextStyle: const TextStyle( + color: Colors.white, + fontSize: 64, + ), + receivedMessageBodyTextStyle: TextStyle( + color: scale.secondaryScale.calloutText, + fontSize: 16, + fontWeight: FontWeight.w500, + height: 1.5, + ), + receivedEmojiMessageTextStyle: const TextStyle( + color: Colors.white, + fontSize: 64, + )); diff --git a/lib/theme/models/contrast_generator.dart b/lib/theme/models/contrast_generator.dart new file mode 100644 index 0000000..52b32c9 --- /dev/null +++ b/lib/theme/models/contrast_generator.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; + +import 'radix_generator.dart'; +import 'scale_color.dart'; +import 'scale_input_decorator_theme.dart'; +import 'scale_scheme.dart'; + +ScaleScheme _contrastScale(Brightness brightness) { + final back = brightness == Brightness.light ? Colors.white : Colors.black; + final front = brightness == Brightness.light ? Colors.black : Colors.white; + + final primaryScale = ScaleColor( + appBackground: back, + subtleBackground: back, + elementBackground: back, + hoverElementBackground: back, + activeElementBackground: back, + subtleBorder: front, + border: front, + hoverBorder: front, + primary: back, + hoverPrimary: back, + subtleText: front, + appText: front, + primaryText: front, + borderText: back, + dialogBorder: front, + calloutBackground: front, + calloutText: back, + ); + + return ScaleScheme( + primaryScale: primaryScale, + primaryAlphaScale: primaryScale, + secondaryScale: primaryScale, + tertiaryScale: primaryScale, + grayScale: primaryScale, + errorScale: primaryScale); +} + +ThemeData contrastGenerator(Brightness brightness) { + final textTheme = makeRadixTextTheme(brightness); + final scaleScheme = _contrastScale(brightness); + final colorScheme = scaleScheme.toColorScheme(brightness); + final scaleConfig = ScaleConfig(useVisualIndicators: true); + + final themeData = ThemeData.from( + colorScheme: colorScheme, textTheme: textTheme, useMaterial3: true); + return themeData.copyWith( + bottomSheetTheme: themeData.bottomSheetTheme.copyWith( + elevation: 0, + modalElevation: 0, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16)))), + canvasColor: scaleScheme.primaryScale.subtleBackground, + chipTheme: themeData.chipTheme.copyWith( + backgroundColor: scaleScheme.primaryScale.elementBackground, + selectedColor: scaleScheme.primaryScale.activeElementBackground, + surfaceTintColor: scaleScheme.primaryScale.hoverElementBackground, + checkmarkColor: scaleScheme.primaryScale.border, + side: BorderSide(color: scaleScheme.primaryScale.border)), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: scaleScheme.primaryScale.elementBackground, + foregroundColor: scaleScheme.primaryScale.appText, + disabledBackgroundColor: scaleScheme.grayScale.elementBackground, + disabledForegroundColor: scaleScheme.grayScale.appText, + shape: RoundedRectangleBorder( + side: BorderSide(color: scaleScheme.primaryScale.border), + borderRadius: BorderRadius.circular(8))), + ), + textSelectionTheme: TextSelectionThemeData( + cursorColor: scaleScheme.primaryScale.appText, + selectionColor: scaleScheme.primaryScale.appText.withAlpha(0x7F), + selectionHandleColor: scaleScheme.primaryScale.appText), + inputDecorationTheme: ScaleInputDecoratorTheme(scaleScheme, textTheme), + extensions: >[ + scaleScheme, + scaleConfig, + ]); +} diff --git a/lib/theme/models/models.dart b/lib/theme/models/models.dart index c22e8ab..e0ba490 100644 --- a/lib/theme/models/models.dart +++ b/lib/theme/models/models.dart @@ -1,4 +1,6 @@ +export 'chat_theme.dart'; export 'radix_generator.dart'; export 'scale_color.dart'; export 'scale_scheme.dart'; +export 'slider_tile.dart'; export 'theme_preference.dart'; diff --git a/lib/theme/models/radix_generator.dart b/lib/theme/models/radix_generator.dart index 415b628..b1f510a 100644 --- a/lib/theme/models/radix_generator.dart +++ b/lib/theme/models/radix_generator.dart @@ -1,16 +1,16 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:flutter_chat_ui/flutter_chat_ui.dart'; import 'package:radix_colors/radix_colors.dart'; import '../../tools/tools.dart'; import 'scale_color.dart'; +import 'scale_input_decorator_theme.dart'; import 'scale_scheme.dart'; enum RadixThemeColor { - scarlet, // tomato + red + violet - babydoll, // crimson + purple + pink + scarlet, // red + violet + tomato + babydoll, // crimson + pink + purple vapor, // pink + cyan + plum gold, // yellow + amber + orange garden, // grass + orange + brown @@ -19,7 +19,7 @@ enum RadixThemeColor { lapis, // blue + indigo + mint eggplant, // violet + purple + indigo lime, // lime + yellow + orange - grim, // mauve + slate + sage + grim, // grey + purple + brown } enum _RadixBaseColor { @@ -282,11 +282,15 @@ extension ToScaleColor on RadixColor { subtleBorder: step6, border: step7, hoverBorder: step8, - background: step9, - hoverBackground: step10, + primary: step9, + hoverPrimary: step10, subtleText: step11, appText: step12, - foregroundText: scaleExtra.foregroundText, + primaryText: scaleExtra.foregroundText, + borderText: step12, + dialogBorder: step9, + calloutBackground: step9, + calloutText: scaleExtra.foregroundText, ); } @@ -338,28 +342,27 @@ class RadixScheme { RadixScheme _radixScheme(Brightness brightness, RadixThemeColor themeColor) { late RadixScheme radixScheme; switch (themeColor) { - // tomato + red + violet + // red + violet + tomato case RadixThemeColor.scarlet: radixScheme = RadixScheme( - primaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.tomato), + primaryScale: _radixColorSteps(brightness, false, _RadixBaseColor.red), primaryExtra: RadixScaleExtra(foregroundText: Colors.white), primaryAlphaScale: - _radixColorSteps(brightness, true, _RadixBaseColor.tomato), + _radixColorSteps(brightness, true, _RadixBaseColor.red), primaryAlphaExtra: RadixScaleExtra(foregroundText: Colors.white), secondaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.red), + _radixColorSteps(brightness, false, _RadixBaseColor.violet), secondaryExtra: RadixScaleExtra(foregroundText: Colors.white), tertiaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.violet), + _radixColorSteps(brightness, false, _RadixBaseColor.tomato), tertiaryExtra: RadixScaleExtra(foregroundText: Colors.white), - grayScale: _radixGraySteps(brightness, false, _RadixBaseColor.tomato), + grayScale: _radixGraySteps(brightness, false, _RadixBaseColor.red), grayExtra: RadixScaleExtra(foregroundText: Colors.white), errorScale: _radixColorSteps(brightness, false, _RadixBaseColor.yellow), errorExtra: RadixScaleExtra(foregroundText: Colors.black), ); - // crimson + purple + pink + // crimson + pink + purple case RadixThemeColor.babydoll: radixScheme = RadixScheme( primaryScale: @@ -369,10 +372,10 @@ RadixScheme _radixScheme(Brightness brightness, RadixThemeColor themeColor) { _radixColorSteps(brightness, true, _RadixBaseColor.crimson), primaryAlphaExtra: RadixScaleExtra(foregroundText: Colors.white), secondaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.purple), + _radixColorSteps(brightness, false, _RadixBaseColor.pink), secondaryExtra: RadixScaleExtra(foregroundText: Colors.white), tertiaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.pink), + _radixColorSteps(brightness, false, _RadixBaseColor.purple), tertiaryExtra: RadixScaleExtra(foregroundText: Colors.white), grayScale: _radixGraySteps(brightness, false, _RadixBaseColor.crimson), @@ -546,13 +549,13 @@ RadixScheme _radixScheme(Brightness brightness, RadixThemeColor themeColor) { _radixGraySteps(brightness, false, _RadixBaseColor.tomato), primaryExtra: RadixScaleExtra(foregroundText: Colors.white), primaryAlphaScale: - _radixColorSteps(brightness, true, _RadixBaseColor.tomato), + _radixGraySteps(brightness, true, _RadixBaseColor.tomato), primaryAlphaExtra: RadixScaleExtra(foregroundText: Colors.white), secondaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.indigo), + _radixColorSteps(brightness, false, _RadixBaseColor.purple), secondaryExtra: RadixScaleExtra(foregroundText: Colors.white), tertiaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.teal), + _radixColorSteps(brightness, false, _RadixBaseColor.brown), tertiaryExtra: RadixScaleExtra(foregroundText: Colors.white), grayScale: brightness == Brightness.dark ? RadixColors.dark.gray @@ -565,87 +568,7 @@ RadixScheme _radixScheme(Brightness brightness, RadixThemeColor themeColor) { return radixScheme; } -ColorScheme _scaleToColorScheme(Brightness brightness, ScaleScheme scale) => - ColorScheme( - brightness: brightness, - primary: scale.primaryScale.background, // reviewed - onPrimary: scale.primaryScale.foregroundText, // reviewed - primaryContainer: - Colors.red, // scale.primaryScale.hoverElementBackground, - onPrimaryContainer: Colors.green, //scale.primaryScale.subtleText, - secondary: scale.secondaryScale.background, - onSecondary: scale.secondaryScale.foregroundText, - secondaryContainer: scale.secondaryScale.hoverElementBackground, - onSecondaryContainer: scale.secondaryScale.subtleText, - tertiary: scale.tertiaryScale.background, - onTertiary: scale.tertiaryScale.foregroundText, - tertiaryContainer: scale.tertiaryScale.hoverElementBackground, - onTertiaryContainer: scale.tertiaryScale.subtleText, - error: scale.errorScale.background, - onError: scale.errorScale.foregroundText, - errorContainer: scale.errorScale.hoverElementBackground, - onErrorContainer: scale.errorScale.subtleText, - background: scale.grayScale.appBackground, // reviewed - onBackground: scale.grayScale.appText, // reviewed - surface: scale.primaryScale.background, // reviewed - onSurface: scale.primaryScale.foregroundText, // reviewed - surfaceVariant: scale.primaryScale.elementBackground, - onSurfaceVariant: - scale.primaryScale.foregroundText, // ?? reviewed a little - outline: scale.primaryScale.border, - outlineVariant: scale.primaryScale.subtleBorder, - shadow: const Color(0xFF000000), - scrim: scale.primaryScale.background, - inverseSurface: scale.primaryScale.subtleText, - onInverseSurface: scale.primaryScale.subtleBackground, - inversePrimary: scale.primaryScale.hoverBackground, - surfaceTint: scale.primaryAlphaScale.hoverElementBackground, - ); - -ChatTheme makeChatTheme(ScaleScheme scale, TextTheme textTheme) => - DefaultChatTheme( - primaryColor: scale.primaryScale.background, - secondaryColor: scale.secondaryScale.background, - backgroundColor: scale.grayScale.appBackground, - inputBackgroundColor: Colors.blue, - inputBorderRadius: BorderRadius.zero, - inputTextDecoration: InputDecoration( - filled: true, - fillColor: scale.primaryScale.elementBackground, - isDense: true, - contentPadding: const EdgeInsets.fromLTRB(8, 12, 8, 12), - border: const OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.all(Radius.circular(8))), - ), - inputContainerDecoration: - BoxDecoration(color: scale.primaryScale.border), - inputPadding: const EdgeInsets.all(9), - inputTextColor: scale.primaryScale.appText, - attachmentButtonIcon: const Icon(Icons.attach_file), - sentMessageBodyTextStyle: TextStyle( - color: scale.primaryScale.foregroundText, - decorationColor: Colors.red, - fontSize: 16, - fontWeight: FontWeight.w500, - height: 1.5, - ), - sentEmojiMessageTextStyle: const TextStyle( - color: Colors.white, - fontSize: 64, - ), - receivedMessageBodyTextStyle: TextStyle( - color: scale.primaryScale.foregroundText, - fontSize: 16, - fontWeight: FontWeight.w500, - height: 1.5, - ), - receivedEmojiMessageTextStyle: const TextStyle( - color: Colors.white, - fontSize: 64, - )); - -TextTheme _makeTextTheme(Brightness brightness) { +TextTheme makeRadixTextTheme(Brightness brightness) { late final TextTheme textTheme; if (Platform.isIOS) { textTheme = (brightness == Brightness.light) @@ -677,10 +600,11 @@ TextTheme _makeTextTheme(Brightness brightness) { } ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) { - final textTheme = _makeTextTheme(brightness); + final textTheme = makeRadixTextTheme(brightness); final radix = _radixScheme(brightness, themeColor); final scaleScheme = radix.toScale(); - final colorScheme = _scaleToColorScheme(brightness, scaleScheme); + final colorScheme = scaleScheme.toColorScheme(brightness); + final scaleConfig = ScaleConfig(useVisualIndicators: false); final themeData = ThemeData.from( colorScheme: colorScheme, textTheme: textTheme, useMaterial3: true); @@ -697,34 +621,18 @@ ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) { backgroundColor: scaleScheme.primaryScale.elementBackground, selectedColor: scaleScheme.primaryScale.activeElementBackground, surfaceTintColor: scaleScheme.primaryScale.hoverElementBackground, - checkmarkColor: scaleScheme.primaryScale.background, + checkmarkColor: scaleScheme.primaryScale.primary, side: BorderSide(color: scaleScheme.primaryScale.border)), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( backgroundColor: scaleScheme.primaryScale.elementBackground, - foregroundColor: scaleScheme.primaryScale.appText, + foregroundColor: scaleScheme.primaryScale.primary, disabledBackgroundColor: scaleScheme.grayScale.elementBackground, - disabledForegroundColor: scaleScheme.grayScale.appText, + disabledForegroundColor: scaleScheme.grayScale.primary, shape: RoundedRectangleBorder( side: BorderSide(color: scaleScheme.primaryScale.border), borderRadius: BorderRadius.circular(8))), ), - focusColor: scaleScheme.primaryScale.activeElementBackground, - hoverColor: scaleScheme.primaryScale.hoverElementBackground, - inputDecorationTheme: themeData.inputDecorationTheme.copyWith( - border: OutlineInputBorder( - borderSide: BorderSide(color: scaleScheme.primaryScale.border), - borderRadius: BorderRadius.circular(8)), - contentPadding: const EdgeInsets.all(8), - labelStyle: TextStyle( - color: scaleScheme.primaryScale.subtleText.withAlpha(127)), - floatingLabelStyle: - TextStyle(color: scaleScheme.primaryScale.subtleText), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: scaleScheme.primaryScale.hoverBorder, width: 2), - borderRadius: BorderRadius.circular(8))), - extensions: >[ - scaleScheme, - ]); + inputDecorationTheme: ScaleInputDecoratorTheme(scaleScheme, textTheme), + extensions: >[scaleScheme, scaleConfig]); } diff --git a/lib/theme/models/scale_color.dart b/lib/theme/models/scale_color.dart index 8a14acc..244f6a3 100644 --- a/lib/theme/models/scale_color.dart +++ b/lib/theme/models/scale_color.dart @@ -10,11 +10,15 @@ class ScaleColor { required this.subtleBorder, required this.border, required this.hoverBorder, - required this.background, - required this.hoverBackground, + required this.primary, + required this.hoverPrimary, required this.subtleText, required this.appText, - required this.foregroundText, + required this.primaryText, + required this.borderText, + required this.dialogBorder, + required this.calloutBackground, + required this.calloutText, }); Color appBackground; @@ -25,11 +29,15 @@ class ScaleColor { Color subtleBorder; Color border; Color hoverBorder; - Color background; - Color hoverBackground; + Color primary; + Color hoverPrimary; Color subtleText; Color appText; - Color foregroundText; + Color primaryText; + Color borderText; + Color dialogBorder; + Color calloutBackground; + Color calloutText; ScaleColor copyWith({ Color? appBackground, @@ -45,24 +53,31 @@ class ScaleColor { Color? subtleText, Color? appText, Color? foregroundText, + Color? borderText, + Color? dialogBorder, + Color? calloutBackground, + Color? calloutText, }) => 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, - appText: appText ?? this.appText, - foregroundText: foregroundText ?? this.foregroundText, - ); + 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, + primary: background ?? this.primary, + hoverPrimary: hoverBackground ?? this.hoverPrimary, + subtleText: subtleText ?? this.subtleText, + appText: appText ?? this.appText, + primaryText: foregroundText ?? this.primaryText, + borderText: borderText ?? this.borderText, + dialogBorder: dialogBorder ?? this.dialogBorder, + calloutBackground: calloutBackground ?? this.calloutBackground, + calloutText: calloutText ?? this.calloutText); // ignore: prefer_constructors_over_static_methods static ScaleColor lerp(ScaleColor a, ScaleColor b, double t) => ScaleColor( @@ -85,14 +100,22 @@ class ScaleColor { 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) ?? + primary: Color.lerp(a.primary, b.primary, t) ?? const Color(0x00000000), + hoverPrimary: Color.lerp(a.hoverPrimary, b.hoverPrimary, t) ?? const Color(0x00000000), subtleText: Color.lerp(a.subtleText, b.subtleText, t) ?? const Color(0x00000000), appText: Color.lerp(a.appText, b.appText, t) ?? const Color(0x00000000), - foregroundText: Color.lerp(a.foregroundText, b.foregroundText, t) ?? + primaryText: Color.lerp(a.primaryText, b.primaryText, t) ?? + const Color(0x00000000), + borderText: Color.lerp(a.borderText, b.borderText, t) ?? + const Color(0x00000000), + dialogBorder: Color.lerp(a.dialogBorder, b.dialogBorder, t) ?? + const Color(0x00000000), + calloutBackground: + Color.lerp(a.calloutBackground, b.calloutBackground, t) ?? + const Color(0x00000000), + calloutText: Color.lerp(a.calloutText, b.calloutText, t) ?? const Color(0x00000000), ); } diff --git a/lib/theme/models/scale_input_decorator_theme.dart b/lib/theme/models/scale_input_decorator_theme.dart new file mode 100644 index 0000000..f6865cd --- /dev/null +++ b/lib/theme/models/scale_input_decorator_theme.dart @@ -0,0 +1,165 @@ +import 'package:flutter/material.dart'; + +import 'scale_scheme.dart'; + +class ScaleInputDecoratorTheme extends InputDecorationTheme { + ScaleInputDecoratorTheme(this._scaleScheme, this._textTheme) + : super( + border: OutlineInputBorder( + borderSide: BorderSide(color: _scaleScheme.primaryScale.border), + borderRadius: BorderRadius.circular(8)), + contentPadding: const EdgeInsets.all(8), + labelStyle: TextStyle( + color: _scaleScheme.primaryScale.subtleText.withAlpha(127)), + floatingLabelStyle: + TextStyle(color: _scaleScheme.primaryScale.subtleText), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: _scaleScheme.primaryScale.hoverBorder, width: 2), + borderRadius: BorderRadius.circular(8))); + + final ScaleScheme _scaleScheme; + final TextTheme _textTheme; + + @override + TextStyle? get hintStyle => MaterialStateTextStyle.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return TextStyle(color: _scaleScheme.grayScale.border); + } + return TextStyle(color: _scaleScheme.primaryScale.border); + }); + + @override + Color? get fillColor => MaterialStateColor.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return _scaleScheme.grayScale.primary.withOpacity(0.04); + } + return _scaleScheme.primaryScale.primary.withOpacity(0.04); + }); + + @override + BorderSide? get activeIndicatorBorder => + MaterialStateBorderSide.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return BorderSide( + color: _scaleScheme.grayScale.border.withAlpha(0x7F)); + } + if (states.contains(MaterialState.error)) { + if (states.contains(MaterialState.hovered)) { + return BorderSide(color: _scaleScheme.errorScale.hoverBorder); + } + if (states.contains(MaterialState.focused)) { + return BorderSide(color: _scaleScheme.errorScale.border, width: 2); + } + return BorderSide(color: _scaleScheme.errorScale.subtleBorder); + } + if (states.contains(MaterialState.hovered)) { + return BorderSide(color: _scaleScheme.secondaryScale.hoverBorder); + } + if (states.contains(MaterialState.focused)) { + return BorderSide( + color: _scaleScheme.secondaryScale.border, width: 2); + } + return BorderSide(color: _scaleScheme.secondaryScale.subtleBorder); + }); + + @override + BorderSide? get outlineBorder => + MaterialStateBorderSide.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return BorderSide( + color: _scaleScheme.grayScale.border.withAlpha(0x7F)); + } + if (states.contains(MaterialState.error)) { + if (states.contains(MaterialState.hovered)) { + return BorderSide(color: _scaleScheme.errorScale.hoverBorder); + } + if (states.contains(MaterialState.focused)) { + return BorderSide(color: _scaleScheme.errorScale.border, width: 2); + } + return BorderSide(color: _scaleScheme.errorScale.subtleBorder); + } + if (states.contains(MaterialState.hovered)) { + return BorderSide(color: _scaleScheme.primaryScale.hoverBorder); + } + if (states.contains(MaterialState.focused)) { + return BorderSide(color: _scaleScheme.primaryScale.border, width: 2); + } + return BorderSide(color: _scaleScheme.primaryScale.subtleBorder); + }); + + @override + Color? get iconColor => _scaleScheme.primaryScale.primary; + + @override + Color? get prefixIconColor => MaterialStateColor.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return _scaleScheme.primaryScale.primary.withAlpha(0x3F); + } + if (states.contains(MaterialState.error)) { + return _scaleScheme.errorScale.primary; + } + return _scaleScheme.primaryScale.primary; + }); + + @override + Color? get suffixIconColor => MaterialStateColor.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return _scaleScheme.primaryScale.primary.withAlpha(0x3F); + } + if (states.contains(MaterialState.error)) { + return _scaleScheme.errorScale.primary; + } + return _scaleScheme.primaryScale.primary; + }); + + @override + TextStyle? get labelStyle => MaterialStateTextStyle.resolveWith((states) { + final textStyle = _textTheme.bodyLarge ?? const TextStyle(); + if (states.contains(MaterialState.disabled)) { + return textStyle.copyWith( + color: _scaleScheme.grayScale.border.withAlpha(0x7F)); + } + if (states.contains(MaterialState.error)) { + if (states.contains(MaterialState.hovered)) { + return textStyle.copyWith( + color: _scaleScheme.errorScale.hoverBorder); + } + if (states.contains(MaterialState.focused)) { + return textStyle.copyWith( + color: _scaleScheme.errorScale.hoverBorder); + } + return textStyle.copyWith( + color: _scaleScheme.errorScale.subtleBorder); + } + if (states.contains(MaterialState.hovered)) { + return textStyle.copyWith( + color: _scaleScheme.primaryScale.hoverBorder); + } + if (states.contains(MaterialState.focused)) { + return textStyle.copyWith( + color: _scaleScheme.primaryScale.hoverBorder); + } + return textStyle.copyWith(color: _scaleScheme.primaryScale.border); + }); + + @override + TextStyle? get floatingLabelStyle => labelStyle; + + @override + TextStyle? get helperStyle => MaterialStateTextStyle.resolveWith((states) { + final textStyle = _textTheme.bodySmall ?? const TextStyle(); + if (states.contains(MaterialState.disabled)) { + return textStyle.copyWith( + color: _scaleScheme.grayScale.border.withAlpha(0x7F)); + } + return textStyle.copyWith( + color: _scaleScheme.secondaryScale.border.withAlpha(0x7F)); + }); + + @override + TextStyle? get errorStyle => MaterialStateTextStyle.resolveWith((states) { + final textStyle = _textTheme.bodySmall ?? const TextStyle(); + return textStyle.copyWith(color: _scaleScheme.errorScale.primary); + }); +} diff --git a/lib/theme/models/scale_scheme.dart b/lib/theme/models/scale_scheme.dart index 74e51bc..990fe1e 100644 --- a/lib/theme/models/scale_scheme.dart +++ b/lib/theme/models/scale_scheme.dart @@ -2,14 +2,17 @@ import 'package:flutter/material.dart'; import 'scale_color.dart'; +enum ScaleKind { primary, primaryAlpha, secondary, tertiary, gray, error } + class ScaleScheme extends ThemeExtension { - ScaleScheme( - {required this.primaryScale, - required this.primaryAlphaScale, - required this.secondaryScale, - required this.tertiaryScale, - required this.grayScale, - required this.errorScale}); + 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; @@ -18,6 +21,23 @@ class ScaleScheme extends ThemeExtension { final ScaleColor grayScale; final ScaleColor errorScale; + ScaleColor scale(ScaleKind kind) { + switch (kind) { + case ScaleKind.primary: + return primaryScale; + case ScaleKind.primaryAlpha: + return primaryAlphaScale; + case ScaleKind.secondary: + return secondaryScale; + case ScaleKind.tertiary: + return tertiaryScale; + case ScaleKind.gray: + return grayScale; + case ScaleKind.error: + return errorScale; + } + } + @override ScaleScheme copyWith( {ScaleColor? primaryScale, @@ -50,4 +70,65 @@ class ScaleScheme extends ThemeExtension { errorScale: ScaleColor.lerp(errorScale, other.errorScale, t), ); } + + ColorScheme toColorScheme(Brightness brightness) => ColorScheme( + brightness: brightness, + primary: primaryScale.primary, // reviewed + onPrimary: primaryScale.primaryText, // reviewed + // primaryContainer: primaryScale.hoverElementBackground, + // onPrimaryContainer: primaryScale.subtleText, + secondary: secondaryScale.primary, + onSecondary: secondaryScale.primaryText, + // secondaryContainer: secondaryScale.hoverElementBackground, + // onSecondaryContainer: secondaryScale.subtleText, + tertiary: tertiaryScale.primary, + onTertiary: tertiaryScale.primaryText, + // tertiaryContainer: tertiaryScale.hoverElementBackground, + // onTertiaryContainer: tertiaryScale.subtleText, + error: errorScale.primary, + onError: errorScale.primaryText, + // errorContainer: errorScale.hoverElementBackground, + // onErrorContainer: errorScale.subtleText, + background: grayScale.appBackground, // reviewed + onBackground: grayScale.appText, // reviewed + surface: primaryScale.primary, // reviewed + onSurface: primaryScale.primaryText, // reviewed + surfaceVariant: secondaryScale.primary, + onSurfaceVariant: secondaryScale.primaryText, // ?? reviewed a little + outline: primaryScale.border, + outlineVariant: secondaryScale.border, + shadow: const Color(0xFF000000), + //scrim: primaryScale.background, + // inverseSurface: primaryScale.subtleText, + // onInverseSurface: primaryScale.subtleBackground, + // inversePrimary: primaryScale.hoverBackground, + // surfaceTint: primaryAlphaScale.hoverElementBackground, + ); +} + +class ScaleConfig extends ThemeExtension { + ScaleConfig({ + required this.useVisualIndicators, + }); + + final bool useVisualIndicators; + + @override + ScaleConfig copyWith({ + bool? useVisualIndicators, + }) => + ScaleConfig( + useVisualIndicators: useVisualIndicators ?? this.useVisualIndicators, + ); + + @override + ScaleConfig lerp(ScaleConfig? other, double t) { + if (other is! ScaleConfig) { + return this; + } + return ScaleConfig( + useVisualIndicators: + t < .5 ? useVisualIndicators : other.useVisualIndicators, + ); + } } diff --git a/lib/theme/models/slider_tile.dart b/lib/theme/models/slider_tile.dart new file mode 100644 index 0000000..251581e --- /dev/null +++ b/lib/theme/models/slider_tile.dart @@ -0,0 +1,151 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; + +import '../theme.dart'; + +class SliderTileAction { + const SliderTileAction({ + required this.actionScale, + required this.onPressed, + this.key, + this.icon, + this.label, + }); + + final Key? key; + final ScaleKind actionScale; + final String? label; + final IconData? icon; + final SlidableActionCallback? onPressed; +} + +class SliderTile extends StatelessWidget { + const SliderTile( + {required this.disabled, + required this.selected, + required this.tileScale, + required this.title, + this.subtitle = '', + this.endActions = const [], + this.startActions = const [], + this.onTap, + this.icon, + super.key}); + + final bool disabled; + final bool selected; + final ScaleKind tileScale; + final List endActions; + final List startActions; + final GestureTapCallback? onTap; + final IconData? icon; + final String title; + final String subtitle; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('disabled', disabled)) + ..add(DiagnosticsProperty('selected', selected)) + ..add(DiagnosticsProperty('tileScale', tileScale)) + ..add(IterableProperty('endActions', endActions)) + ..add(IterableProperty('startActions', startActions)) + ..add(ObjectFlagProperty.has('onTap', onTap)) + ..add(DiagnosticsProperty('icon', icon)) + ..add(StringProperty('title', title)) + ..add(StringProperty('subtitle', subtitle)); + } + + @override + // ignore: prefer_expression_function_bodies + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final tileColor = scale.scale(!disabled ? tileScale : ScaleKind.gray); + final scalecfg = theme.extension()!; + + final borderColor = selected ? tileColor.hoverBorder : tileColor.border; + final backgroundColor = scalecfg.useVisualIndicators && !selected + ? tileColor.borderText + : borderColor; + final textColor = scalecfg.useVisualIndicators && !selected + ? borderColor + : tileColor.borderText; + + return Container( + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: backgroundColor, + shape: RoundedRectangleBorder( + side: scalecfg.useVisualIndicators + ? BorderSide(width: 2, color: borderColor, strokeAlign: 0) + : BorderSide.none, + borderRadius: BorderRadius.circular(8), + )), + child: Slidable( + // Specify a key if the Slidable is dismissible. + key: key, + endActionPane: endActions.isEmpty + ? null + : ActionPane( + motion: const DrawerMotion(), + children: endActions + .map( + (a) => SlidableAction( + onPressed: disabled ? null : a.onPressed, + backgroundColor: scalecfg.useVisualIndicators + ? (selected + ? tileColor.borderText + : tileColor.border) + : scale.scale(a.actionScale).primary, + foregroundColor: scalecfg.useVisualIndicators + ? (selected + ? tileColor.border + : tileColor.borderText) + : scale.scale(a.actionScale).primaryText, + icon: a.icon, + label: a.label, + padding: const EdgeInsets.all(2)), + ) + .toList()), + startActionPane: startActions.isEmpty + ? null + : ActionPane( + motion: const DrawerMotion(), + children: startActions + .map( + (a) => SlidableAction( + onPressed: disabled ? null : a.onPressed, + backgroundColor: scalecfg.useVisualIndicators + ? (selected + ? tileColor.borderText + : tileColor.border) + : scale.scale(a.actionScale).primary, + foregroundColor: scalecfg.useVisualIndicators + ? (selected + ? tileColor.border + : tileColor.borderText) + : scale.scale(a.actionScale).primaryText, + icon: a.icon, + label: a.label, + padding: const EdgeInsets.all(2)), + ) + .toList()), + child: Padding( + padding: scalecfg.useVisualIndicators + ? EdgeInsets.zero + : const EdgeInsets.fromLTRB(0, 2, 0, 2), + child: ListTile( + onTap: onTap, + title: Text( + title, + softWrap: true, + ), + subtitle: subtitle.isNotEmpty ? Text(subtitle) : null, + iconColor: textColor, + textColor: textColor, + leading: icon == null ? null : Icon(icon))))); + } +} diff --git a/lib/theme/models/theme_preference.dart b/lib/theme/models/theme_preference.dart index 74c90d8..334bbba 100644 --- a/lib/theme/models/theme_preference.dart +++ b/lib/theme/models/theme_preference.dart @@ -2,7 +2,8 @@ import 'package:change_case/change_case.dart'; import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import '../../tools/tools.dart'; +import '../views/widget_helpers.dart'; +import 'contrast_generator.dart'; import 'radix_generator.dart'; part 'theme_preference.freezed.dart'; @@ -83,7 +84,7 @@ extension ThemePreferencesExt on ThemePreferences { // Special cases case ColorPreference.contrast: // xxx do contrastGenerator - themeData = radixGenerator(brightness, RadixThemeColor.grim); + themeData = contrastGenerator(brightness); // Generate from Radix case ColorPreference.scarlet: themeData = radixGenerator(brightness, RadixThemeColor.scarlet); diff --git a/lib/tools/scanner_error_widget.dart b/lib/theme/views/scanner_error_widget.dart similarity index 100% rename from lib/tools/scanner_error_widget.dart rename to lib/theme/views/scanner_error_widget.dart diff --git a/lib/tools/styled_dialog.dart b/lib/theme/views/styled_dialog.dart similarity index 90% rename from lib/tools/styled_dialog.dart rename to lib/theme/views/styled_dialog.dart index 25c3c0a..0fd079c 100644 --- a/lib/tools/styled_dialog.dart +++ b/lib/theme/views/styled_dialog.dart @@ -2,7 +2,7 @@ import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import '../theme/theme.dart'; +import '../theme.dart'; class StyledDialog extends StatelessWidget { const StyledDialog({required this.title, required this.child, super.key}); @@ -19,10 +19,11 @@ class StyledDialog extends StatelessWidget { borderRadius: BorderRadius.all(Radius.circular(16)), ), contentPadding: const EdgeInsets.all(4), - backgroundColor: scale.primaryScale.border, + backgroundColor: scale.primaryScale.dialogBorder, title: Text( title, - style: textTheme.titleMedium, + style: textTheme.titleMedium! + .copyWith(color: scale.primaryScale.borderText), textAlign: TextAlign.center, ), titlePadding: const EdgeInsets.fromLTRB(4, 4, 4, 4), diff --git a/lib/theme/views/views.dart b/lib/theme/views/views.dart index 85d06c4..6f6d7ac 100644 --- a/lib/theme/views/views.dart +++ b/lib/theme/views/views.dart @@ -1,2 +1,5 @@ export 'brightness_preferences.dart'; export 'color_preferences.dart'; +export 'scanner_error_widget.dart'; +export 'styled_dialog.dart'; +export 'widget_helpers.dart'; diff --git a/lib/tools/widget_helpers.dart b/lib/theme/views/widget_helpers.dart similarity index 85% rename from lib/tools/widget_helpers.dart rename to lib/theme/views/widget_helpers.dart index 3d8be63..7cef561 100644 --- a/lib/tools/widget_helpers.dart +++ b/lib/theme/views/widget_helpers.dart @@ -9,7 +9,7 @@ import 'package:flutter_translate/flutter_translate.dart'; import 'package:motion_toast/motion_toast.dart'; import 'package:quickalert/quickalert.dart'; -import '../theme/theme.dart'; +import '../theme.dart'; extension BorderExt on Widget { DecoratedBox debugBorder() => DecoratedBox( @@ -35,19 +35,22 @@ Widget buildProgressIndicator() => Builder(builder: (context) { final theme = Theme.of(context); final scale = theme.extension()!; return SpinKitFoldingCube( - color: scale.tertiaryScale.background, + color: scale.tertiaryScale.primary, size: 80, ); }); -Widget waitingPage({String? text}) => Builder( - builder: (context) => ColoredBox( - color: Theme.of(context).scaffoldBackgroundColor, - child: Center( - child: Column(children: [ - buildProgressIndicator().expanded(), - if (text != null) Text(text) - ])))); +Widget waitingPage({String? text}) => Builder(builder: (context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + return ColoredBox( + color: scale.tertiaryScale.primaryText, + child: Center( + child: Column(children: [ + buildProgressIndicator().expanded(), + if (text != null) Text(text) + ]))); + }); Widget debugPage(String text) => Builder( builder: (context) => ColoredBox( @@ -132,12 +135,30 @@ void showInfoToast(BuildContext context, String message) { ).show(context); } +// Widget insetBorder( +// {required BuildContext context, +// required bool enabled, +// required Color color, +// required Widget child}) { +// if (!enabled) { +// return child; +// } + +// return Stack({ +// children: [] { +// DecoratedBox(decoration: BoxDecoration() +// child, +// } +// }) +// } + Widget styledTitleContainer({ required BuildContext context, required String title, required Widget child, Color? borderColor, Color? backgroundColor, + Color? titleColor, }) { final theme = Theme.of(context); final scale = theme.extension()!; @@ -153,7 +174,7 @@ Widget styledTitleContainer({ Text( title, style: textTheme.titleMedium! - .copyWith(color: scale.primaryScale.subtleText), + .copyWith(color: titleColor ?? scale.primaryScale.borderText), ).paddingLTRB(8, 8, 8, 4), DecoratedBox( decoration: ShapeDecoration( @@ -174,6 +195,7 @@ Widget styledBottomSheet({ required Widget child, Color? borderColor, Color? backgroundColor, + Color? titleColor, }) { final theme = Theme.of(context); final scale = theme.extension()!; @@ -181,7 +203,7 @@ Widget styledBottomSheet({ return DecoratedBox( decoration: ShapeDecoration( - color: borderColor ?? scale.primaryScale.border, + color: borderColor ?? scale.primaryScale.dialogBorder, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular(16), @@ -190,7 +212,7 @@ Widget styledBottomSheet({ Text( title, style: textTheme.titleMedium! - .copyWith(color: scale.primaryScale.subtleText), + .copyWith(color: titleColor ?? scale.primaryScale.borderText), ).paddingLTRB(8, 8, 8, 4), DecoratedBox( decoration: ShapeDecoration( diff --git a/lib/tools/enter_password.dart b/lib/tools/enter_password.dart index ca1bd73..2240278 100644 --- a/lib/tools/enter_password.dart +++ b/lib/tools/enter_password.dart @@ -52,27 +52,23 @@ class _EnterPasswordDialogState extends State { final theme = Theme.of(context); final scale = theme.extension()!; - return Dialog( - backgroundColor: scale.grayScale.subtleBackground, + return StyledDialog( + title: widget.matchPass == null + ? translate('enter_password_dialog.enter_password') + : translate('enter_password_dialog.reenter_password'), child: Form( key: formKey, child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - widget.matchPass == null - ? translate('enter_password_dialog.enter_password') - : translate('enter_password_dialog.reenter_password'), - style: theme.textTheme.titleLarge, - ).paddingAll(16), TextField( controller: passwordController, focusNode: focusNode, autofocus: true, enableSuggestions: false, - obscureText: - !_passwordVisible, //This will obscure text dynamically + obscureText: !_passwordVisible, + obscuringCharacter: '*', inputFormatters: [ FilteringTextInputFormatter.singleLineFormatter ], @@ -87,7 +83,7 @@ class _EnterPasswordDialogState extends State { ? null : Icon(Icons.check_circle, color: passwordController.text == widget.matchPass - ? scale.primaryScale.background + ? scale.primaryScale.primary : scale.grayScale.subtleBackground), suffixIcon: IconButton( icon: Icon( diff --git a/lib/tools/enter_pin.dart b/lib/tools/enter_pin.dart index 7166126..d0a21ec 100644 --- a/lib/tools/enter_pin.dart +++ b/lib/tools/enter_pin.dart @@ -67,20 +67,16 @@ class _EnterPinDialogState extends State { ); /// Optionally you can use form to validate the Pinput - return Dialog( - backgroundColor: scale.grayScale.subtleBackground, + return StyledDialog( + title: !widget.reenter + ? translate('enter_pin_dialog.enter_pin') + : translate('enter_pin_dialog.reenter_pin'), child: Form( key: formKey, child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - !widget.reenter - ? translate('enter_pin_dialog.enter_pin') - : translate('enter_pin_dialog.reenter_pin'), - style: theme.textTheme.titleLarge, - ).paddingAll(16), Directionality( // Specify direction if desired textDirection: TextDirection.ltr, diff --git a/lib/tools/tools.dart b/lib/tools/tools.dart index a5cefcf..c556f98 100644 --- a/lib/tools/tools.dart +++ b/lib/tools/tools.dart @@ -5,10 +5,7 @@ export 'loggy.dart'; export 'phono_byte.dart'; export 'pop_control.dart'; export 'responsive.dart'; -export 'scanner_error_widget.dart'; export 'shared_preferences.dart'; export 'state_logger.dart'; export 'stream_listenable.dart'; -export 'styled_dialog.dart'; -export 'widget_helpers.dart'; export 'window_control.dart'; diff --git a/lib/veilid_processor/views/developer.dart b/lib/veilid_processor/views/developer.dart index 4e70ff9..8ca539a 100644 --- a/lib/veilid_processor/views/developer.dart +++ b/lib/veilid_processor/views/developer.dart @@ -140,17 +140,18 @@ class _DeveloperPageState extends State { // }); return Scaffold( + backgroundColor: scale.primaryScale.primary, appBar: DefaultAppBar( title: Text(translate('developer.title')), leading: IconButton( - icon: Icon(Icons.arrow_back, color: scale.primaryScale.appText), + icon: Icon(Icons.arrow_back, color: scale.primaryScale.primaryText), onPressed: () => GoRouterHelper(context).pop(), ), actions: [ IconButton( icon: const Icon(Icons.copy), - color: scale.primaryScale.appText, - disabledColor: scale.grayScale.subtleText, + color: scale.primaryScale.primaryText, + disabledColor: scale.primaryScale.primaryText.withAlpha(0x3F), onPressed: _terminalController.selection == null ? null : () async { @@ -158,17 +159,22 @@ class _DeveloperPageState extends State { }), IconButton( icon: const Icon(Icons.clear_all), - color: scale.primaryScale.appText, - disabledColor: scale.grayScale.subtleText, + color: scale.primaryScale.primaryText, + disabledColor: scale.primaryScale.primaryText.withAlpha(0x3F), onPressed: () async { await QuickAlert.show( context: context, type: QuickAlertType.confirm, title: translate('developer.are_you_sure_clear'), - textColor: scale.primaryScale.appText, - confirmBtnColor: scale.primaryScale.elementBackground, - backgroundColor: scale.primaryScale.subtleBackground, - headerBackgroundColor: scale.primaryScale.background, + titleColor: scale.primaryScale.appText, + textColor: scale.primaryScale.subtleText, + confirmBtnColor: scale.primaryScale.primary, + cancelBtnTextStyle: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 18, + color: scale.primaryScale.appText), + backgroundColor: scale.primaryScale.appBackground, + headerBackgroundColor: scale.primaryScale.primary, confirmBtnText: translate('button.ok'), cancelBtnText: translate('button.cancel'), onConfirmBtnTap: () async { @@ -194,13 +200,23 @@ class _DeveloperPageState extends State { width: 64, height: 40, render: ResultRender.icon, + icon: SizedBox( + width: 10, + height: 10, + child: CustomPaint( + painter: DropdownArrowPainter( + color: scale.primaryScale.primaryText))), textStyle: textTheme.labelMedium! - .copyWith(color: scale.primaryScale.appText), + .copyWith(color: scale.primaryScale.primaryText), padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), openBoxDecoration: BoxDecoration( - color: scale.primaryScale.activeElementBackground), - boxDecoration: - BoxDecoration(color: scale.primaryScale.elementBackground), + color: scale.primaryScale.border, + borderRadius: BorderRadius.circular(8), + ), + boxDecoration: BoxDecoration( + color: scale.primaryScale.hoverBorder, + borderRadius: BorderRadius.circular(8), + ), ), dropdownOptions: DropdownOptions( width: 160, @@ -224,7 +240,7 @@ class _DeveloperPageState extends State { padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), selectedPadding: const EdgeInsets.fromLTRB(8, 4, 8, 4)), dropdownList: _logLevelDropdownItems, - ) + ).paddingLTRB(0, 0, 8, 0) ], ), body: SafeArea( @@ -245,13 +261,19 @@ class _DeveloperPageState extends State { decoration: InputDecoration( filled: true, contentPadding: const EdgeInsets.fromLTRB(8, 2, 8, 2), - border: OutlineInputBorder( + enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: scale.primaryScale.border)), + borderSide: BorderSide.none), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), fillColor: scale.primaryScale.subtleBackground, hintText: translate('developer.command'), suffixIcon: IconButton( - icon: const Icon(Icons.send), + icon: Icon(Icons.send, + color: _debugCommandController.text.isEmpty + ? scale.primaryScale.primary.withAlpha(0x3F) + : scale.primaryScale.primary), onPressed: _debugCommandController.text.isEmpty ? null : () async { diff --git a/lib/veilid_processor/views/signal_strength_meter.dart b/lib/veilid_processor/views/signal_strength_meter.dart index eaf5022..4691e87 100644 --- a/lib/veilid_processor/views/signal_strength_meter.dart +++ b/lib/veilid_processor/views/signal_strength_meter.dart @@ -33,32 +33,32 @@ class SignalStrengthMeterWidget extends StatelessWidget { switch (connectionState.attachment.state) { case AttachmentState.detached: iconWidget = Icon(Icons.signal_cellular_nodata, - size: iconSize, color: scale.grayScale.appText); + size: iconSize, color: scale.primaryScale.primaryText); return; case AttachmentState.detaching: iconWidget = Icon(Icons.signal_cellular_off, - size: iconSize, color: scale.grayScale.appText); + size: iconSize, color: scale.primaryScale.primaryText); return; case AttachmentState.attaching: value = 0; - color = scale.primaryScale.appText; + color = scale.primaryScale.primaryText; case AttachmentState.attachedWeak: value = 1; - color = scale.primaryScale.appText; + color = scale.primaryScale.primaryText; case AttachmentState.attachedStrong: value = 2; - color = scale.primaryScale.appText; + color = scale.primaryScale.primaryText; case AttachmentState.attachedGood: value = 3; - color = scale.primaryScale.appText; + color = scale.primaryScale.primaryText; case AttachmentState.fullyAttached: value = 4; - color = scale.primaryScale.appText; + color = scale.primaryScale.primaryText; case AttachmentState.overAttached: value = 4; - color = scale.secondaryScale.subtleText; + color = scale.primaryScale.primaryText; } - inactiveColor = scale.grayScale.subtleText; + inactiveColor = scale.primaryScale.primaryText; iconWidget = SignalStrengthIndicator.bars( value: value, @@ -66,7 +66,7 @@ class SignalStrengthMeterWidget extends StatelessWidget { inactiveColor: inactiveColor, size: iconSize, barCount: 4, - spacing: 1); + spacing: 2); }, loading: () => {iconWidget = const Icon(Icons.warning)}, error: (e, st) => { 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 cd5ef23..52214ba 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 @@ -69,7 +69,7 @@ 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>>( + _sspUpdate.busyUpdate>( busy, (emit) async => _refreshInner(emit)); } 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 01390ed..2fd1a60 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 @@ -470,6 +470,8 @@ class _DHTShortArrayHead { _subscription = null; } + // Called when the shortarray changes online and we find out from a watch + // but not when we make a change locally Future _onHeadValueChanged( DHTRecord record, Uint8List? data, List subkeys) async { // If head record subkey zero changes, then the layout From 809f6d69bf41d6a106ba2d90fd2ce5c05f0cb84a Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 17 Apr 2024 21:31:26 -0400 Subject: [PATCH 085/270] better message status support --- .../account_repository.dart | 27 +- lib/chat/chat.dart | 1 + .../cubits/single_contact_messages_cubit.dart | 330 ++++++++++++++---- lib/chat/models/message_state.dart | 34 ++ lib/chat/models/message_state.freezed.dart | 229 ++++++++++++ lib/chat/models/message_state.g.dart | 25 ++ lib/chat/models/models.dart | 1 + lib/chat/views/chat_component.dart | 49 +-- .../active_conversations_bloc_map_cubit.dart | 6 +- ...ve_single_contact_chat_bloc_map_cubit.dart | 15 +- lib/chat_list/cubits/chat_list_cubit.dart | 11 +- .../chat_single_contact_list_widget.dart | 8 +- .../cubits/contact_invitation_list_cubit.dart | 14 +- .../waiting_invitations_bloc_map_cubit.dart | 7 +- lib/contacts/cubits/conversation_cubit.dart | 8 +- .../main_pager/account_page.dart | 7 +- lib/proto/veilidchat.proto | 3 - lib/router/cubit/router_cubit.dart | 11 +- lib/router/cubit/router_state.dart | 11 - packages/bloc_tools/lib/src/future_cubit.dart | 20 +- .../src/dht_record/dht_record.dart | 2 +- .../src/dht_record/dht_record_pool.dart | 14 +- .../src/dht_short_array/dht_short_array.dart | 6 +- .../dht_short_array_cubit.dart | 31 +- .../dht_short_array/dht_short_array_read.dart | 37 ++ .../dht_short_array_write.dart | 28 +- .../lib/src/async_table_db_backed_cubit.dart | 49 ++- packages/veilid_support/lib/src/identity.dart | 5 +- .../lib/src/persistent_queue_cubit.dart | 194 ++++++++++ packages/veilid_support/lib/src/table_db.dart | 110 +++++- .../veilid_support/lib/veilid_support.dart | 1 + 31 files changed, 1046 insertions(+), 248 deletions(-) create mode 100644 lib/chat/models/message_state.dart create mode 100644 lib/chat/models/message_state.freezed.dart create mode 100644 lib/chat/models/message_state.g.dart create mode 100644 lib/chat/models/models.dart delete mode 100644 lib/router/cubit/router_state.dart create mode 100644 packages/veilid_support/lib/src/persistent_queue_cubit.dart diff --git a/lib/account_manager/repository/account_repository/account_repository.dart b/lib/account_manager/repository/account_repository/account_repository.dart index 056fd5f..777c79e 100644 --- a/lib/account_manager/repository/account_repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository/account_repository.dart @@ -26,7 +26,8 @@ class AccountRepository { ? IList.fromJson( obj, genericFromJson(LocalAccount.fromJson)) : IList(), - valueToJson: (val) => val.toJson((la) => la.toJson())); + valueToJson: (val) => val?.toJson((la) => la.toJson()), + makeInitialValue: IList.empty); static TableDBValue> _initUserLogins() => TableDBValue( tableName: 'local_account_manager', @@ -34,13 +35,15 @@ class AccountRepository { valueFromJson: (obj) => obj != null ? IList.fromJson(obj, genericFromJson(UserLogin.fromJson)) : IList(), - valueToJson: (val) => val.toJson((la) => la.toJson())); + valueToJson: (val) => val?.toJson((la) => la.toJson()), + makeInitialValue: IList.empty); static TableDBValue _initActiveAccount() => TableDBValue( tableName: 'local_account_manager', tableKeyName: 'active_local_account', valueFromJson: (obj) => obj == null ? null : TypedKey.fromJson(obj), - valueToJson: (val) => val?.toJson()); + valueToJson: (val) => val?.toJson(), + makeInitialValue: () => null); ////////////////////////////////////////////////////////////// /// Fields @@ -62,7 +65,9 @@ class AccountRepository { } Future close() async { - // ??? + await _localAccounts.close(); + await _userLogins.close(); + await _activeLocalAccount.close(); } ////////////////////////////////////////////////////////////// @@ -72,18 +77,18 @@ class AccountRepository { ////////////////////////////////////////////////////////////// /// Selectors - IList getLocalAccounts() => _localAccounts.requireValue; - TypedKey? getActiveLocalAccount() => _activeLocalAccount.requireValue; - IList getUserLogins() => _userLogins.requireValue; + IList getLocalAccounts() => _localAccounts.value; + TypedKey? getActiveLocalAccount() => _activeLocalAccount.value; + IList getUserLogins() => _userLogins.value; UserLogin? getActiveUserLogin() { - final activeLocalAccount = _activeLocalAccount.requireValue; + final activeLocalAccount = _activeLocalAccount.value; return activeLocalAccount == null ? null : fetchUserLogin(activeLocalAccount); } LocalAccount? fetchLocalAccount(TypedKey accountMasterRecordKey) { - final localAccounts = _localAccounts.requireValue; + final localAccounts = _localAccounts.value; final idx = localAccounts.indexWhere( (e) => e.identityMaster.masterRecordKey == accountMasterRecordKey); if (idx == -1) { @@ -93,7 +98,7 @@ class AccountRepository { } UserLogin? fetchUserLogin(TypedKey accountMasterRecordKey) { - final userLogins = _userLogins.requireValue; + final userLogins = _userLogins.value; final idx = userLogins .indexWhere((e) => e.accountMasterRecordKey == accountMasterRecordKey); if (idx == -1) { @@ -295,7 +300,7 @@ class AccountRepository { if (accountMasterRecordKey != null) { // Assert the specified record key can be found, will throw if not - final _ = _userLogins.requireValue.firstWhere( + final _ = _userLogins.value.firstWhere( (ul) => ul.accountMasterRecordKey == accountMasterRecordKey); } await _activeLocalAccount.set(accountMasterRecordKey); diff --git a/lib/chat/chat.dart b/lib/chat/chat.dart index 6acdd43..08ae2e7 100644 --- a/lib/chat/chat.dart +++ b/lib/chat/chat.dart @@ -1,2 +1,3 @@ export 'cubits/cubits.dart'; +export 'models/models.dart'; export 'views/views.dart'; diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 1a2a604..85eb9d6 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -1,20 +1,49 @@ 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:fixnum/fixnum.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; +import '../models/models.dart'; -class _SingleContactMessageQueueEntry { - _SingleContactMessageQueueEntry({this.remoteMessages}); - IList? remoteMessages; +class RenderStateElement { + RenderStateElement( + {required this.message, + required this.isLocal, + this.reconciled = false, + this.reconciledOffline = false, + this.sent = false, + this.sentOffline = false}); + + MessageSendState? get sendState { + if (!isLocal) { + return null; + } + if (reconciled && sent) { + if (!reconciledOffline && !sentOffline) { + return MessageSendState.delivered; + } + return MessageSendState.sent; + } + if (sent && !sentOffline) { + return MessageSendState.sent; + } + return MessageSendState.sending; + } + + proto.Message message; + bool isLocal; + bool reconciled; + bool reconciledOffline; + bool sent; + bool sentOffline; } -typedef SingleContactMessagesState = AsyncValue>; +typedef SingleContactMessagesState = AsyncValue>; // Cubit that processes single-contact chats // Builds the reconciled chat record from the local and remote conversation keys @@ -34,7 +63,14 @@ class SingleContactMessagesCubit extends Cubit { _remoteConversationRecordKey = remoteConversationRecordKey, _remoteMessagesRecordKey = remoteMessagesRecordKey, _reconciledChatRecord = reconciledChatRecord, - _messagesUpdateQueue = StreamController(), + _unreconciledMessagesQueue = PersistentQueueCubit( + table: 'SingleContactUnreconciledMessages', + key: remoteConversationRecordKey.toString(), + fromBuffer: proto.Message.fromBuffer), + _sendingMessagesQueue = PersistentQueueCubit( + table: 'SingleContactSendingMessages', + key: remoteConversationRecordKey.toString(), + fromBuffer: proto.Message.fromBuffer), super(const AsyncValue.loading()) { // Async Init _initWait.add(_init); @@ -44,13 +80,14 @@ class SingleContactMessagesCubit extends Cubit { Future close() async { await _initWait(); - await _messagesUpdateQueue.close(); - await _localSubscription?.cancel(); - await _remoteSubscription?.cancel(); - await _reconciledChatSubscription?.cancel(); - await _localMessagesCubit?.close(); - await _remoteMessagesCubit?.close(); - await _reconciledChatMessagesCubit?.close(); + await _unreconciledMessagesQueue.close(); + await _sendingMessagesQueue.close(); + await _sentSubscription?.cancel(); + await _rcvdSubscription?.cancel(); + await _reconciledSubscription?.cancel(); + await _sentMessagesCubit?.close(); + await _rcvdMessagesCubit?.close(); + await _reconciledMessagesCubit?.close(); await super.close(); } @@ -60,95 +97,137 @@ class SingleContactMessagesCubit extends Cubit { await _initMessagesCrypto(); // Reconciled messages key - await _initReconciledChatMessages(); + await _initReconciledMessagesCubit(); // Local messages key - await _initLocalMessages(); + await _initSentMessagesCubit(); // Remote messages key - await _initRemoteMessages(); + await _initRcvdMessagesCubit(); - // Messages listener + // Unreconciled messages processing queue listener Future.delayed(Duration.zero, () async { - await for (final entry in _messagesUpdateQueue.stream) { - await _updateMessagesStateAsync(entry); + await for (final entry in _unreconciledMessagesQueue.stream) { + final data = entry.asData; + if (data != null && data.value.isNotEmpty) { + // Process data using recoverable processing mechanism + await _unreconciledMessagesQueue.process((messages) async { + await _processUnreconciledMessages(data.value); + }); + } + } + }); + + // Sending messages processing queue listener + Future.delayed(Duration.zero, () async { + await for (final entry in _sendingMessagesQueue.stream) { + final data = entry.asData; + if (data != null && data.value.isNotEmpty) { + // Process data using recoverable processing mechanism + await _sendingMessagesQueue.process((messages) async { + await _processSendingMessages(data.value); + }); + } } }); } // Make crypto - Future _initMessagesCrypto() async { _messagesCrypto = await _activeAccountInfo .makeConversationCrypto(_remoteIdentityPublicKey); } // Open local messages key - Future _initLocalMessages() async { + Future _initSentMessagesCubit() async { final writer = _activeAccountInfo.conversationWriter; - _localMessagesCubit = DHTShortArrayCubit( + _sentMessagesCubit = DHTShortArrayCubit( open: () async => DHTShortArray.openWrite( _localMessagesRecordKey, writer, debugName: - 'SingleContactMessagesCubit::_initLocalMessages::LocalMessages', + 'SingleContactMessagesCubit::_initSentMessagesCubit::SentMessages', parent: _localConversationRecordKey, crypto: _messagesCrypto), decodeElement: proto.Message.fromBuffer); + _sentSubscription = + _sentMessagesCubit!.stream.listen(_updateSentMessagesState); + _updateSentMessagesState(_sentMessagesCubit!.state); } // Open remote messages key - Future _initRemoteMessages() async { - _remoteMessagesCubit = DHTShortArrayCubit( + Future _initRcvdMessagesCubit() async { + _rcvdMessagesCubit = DHTShortArrayCubit( open: () async => DHTShortArray.openRead(_remoteMessagesRecordKey, - debugName: 'SingleContactMessagesCubit::_initRemoteMessages::' - 'RemoteMessages', + debugName: 'SingleContactMessagesCubit::_initRcvdMessagesCubit::' + 'RcvdMessages', parent: _remoteConversationRecordKey, crypto: _messagesCrypto), decodeElement: proto.Message.fromBuffer); - _remoteSubscription = - _remoteMessagesCubit!.stream.listen(_updateRemoteMessagesState); - _updateRemoteMessagesState(_remoteMessagesCubit!.state); + _rcvdSubscription = + _rcvdMessagesCubit!.stream.listen(_updateRcvdMessagesState); + _updateRcvdMessagesState(_rcvdMessagesCubit!.state); } // Open reconciled chat record key - Future _initReconciledChatMessages() async { + Future _initReconciledMessagesCubit() async { final accountRecordKey = _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - _reconciledChatMessagesCubit = DHTShortArrayCubit( + _reconciledMessagesCubit = DHTShortArrayCubit( open: () async => DHTShortArray.openOwned(_reconciledChatRecord, - debugName: - 'SingleContactMessagesCubit::_initReconciledChatMessages::' - 'ReconciledChat', + debugName: 'SingleContactMessagesCubit::_initReconciledMessages::' + 'ReconciledMessages', parent: accountRecordKey), decodeElement: proto.Message.fromBuffer); - _reconciledChatSubscription = - _reconciledChatMessagesCubit!.stream.listen(_updateReconciledChatState); - _updateReconciledChatState(_reconciledChatMessagesCubit!.state); + _reconciledSubscription = + _reconciledMessagesCubit!.stream.listen(_updateReconciledMessagesState); + _updateReconciledMessagesState(_reconciledMessagesCubit!.state); } // Called when the remote messages list gets a change - void _updateRemoteMessagesState( - BlocBusyState>> avmessages) { + void _updateRcvdMessagesState( + DHTShortArrayBusyState avmessages) { final remoteMessages = avmessages.state.asData?.value; if (remoteMessages == null) { return; } + // Add remote messages updates to queue to process asynchronously - _messagesUpdateQueue - .add(_SingleContactMessageQueueEntry(remoteMessages: remoteMessages)); + // Ignore offline state because remote messages are always fully delivered + // This may happen once per client but should be idempotent + _unreconciledMessagesQueue + .addAllSync(remoteMessages.map((x) => x.value).toIList()); + + // Update the view + _renderState(); + } + + // Called when the send messages list gets a change + // This will re-render when messages are sent from another machine + void _updateSentMessagesState( + DHTShortArrayBusyState avmessages) { + final remoteMessages = avmessages.state.asData?.value; + if (remoteMessages == null) { + return; + } + // Don't reconcile, the sending machine will have already added + // to the reconciliation queue on that machine + + // Update the view + _renderState(); } // 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); + // This can happen when multiple clients for the same identity are + // reading and reconciling the same remote chat + void _updateReconciledMessagesState( + DHTShortArrayBusyState avmessages) { + // Update the view + _renderState(); } - Future _mergeMessagesInner( + Future _reconcileMessagesInner( {required DHTShortArrayWrite reconciledMessagesWriter, required IList messages}) async { // Ensure remoteMessages is sorted by timestamp @@ -209,29 +288,129 @@ class SingleContactMessagesCubit extends Cubit { } } - Future _updateMessagesStateAsync( - _SingleContactMessageQueueEntry entry) async { - final reconciledChatMessagesCubit = _reconciledChatMessagesCubit!; - - // Merge remote and local messages into the reconciled chat log - await reconciledChatMessagesCubit + // Async process to reconcile messages sent or received in the background + Future _processUnreconciledMessages( + IList messages) async { + await _reconciledMessagesCubit! .operateWrite((reconciledMessagesWriter) async { - if (entry.remoteMessages != null) { - await _mergeMessagesInner( - reconciledMessagesWriter: reconciledMessagesWriter, - messages: entry.remoteMessages!); - } + await _reconcileMessagesInner( + reconciledMessagesWriter: reconciledMessagesWriter, + messages: messages); }); } - Future addMessage({required proto.Message message}) async { - await _initWait(); + // Async process to send messages in the background + Future _processSendingMessages(IList messages) async { + for (final message in messages) { + await _sentMessagesCubit!.operateWriteEventual( + (writer) => writer.tryAddItem(message.writeToBuffer())); + } + } - await _reconciledChatMessagesCubit!.operateWrite((writer) => - _mergeMessagesInner( - reconciledMessagesWriter: writer, messages: [message].toIList())); - await _localMessagesCubit! - .operateWrite((writer) => writer.tryAddItem(message.writeToBuffer())); + // Produce a state for this cubit from the input cubits and queues + void _renderState() { + // Get all reconciled messages + final reconciledMessages = + _reconciledMessagesCubit?.state.state.asData?.value; + // Get all sent messages + final sentMessages = _sentMessagesCubit?.state.state.asData?.value; + // Get all items in the unreconciled queue + final unreconciledMessages = _unreconciledMessagesQueue.state.asData?.value; + // Get all items in the unsent queue + final sendingMessages = _sendingMessagesQueue.state.asData?.value; + + // If we aren't ready to render a state, say we're loading + if (reconciledMessages == null || + sentMessages == null || + unreconciledMessages == null || + sendingMessages == null) { + emit(const AsyncLoading()); + return; + } + + // Generate state for each message + final sentMessagesMap = + IMap>.fromValues( + keyMapper: (x) => x.value.timestamp, + values: sentMessages, + ); + final reconciledMessagesMap = + IMap>.fromValues( + keyMapper: (x) => x.value.timestamp, + values: reconciledMessages, + ); + final sendingMessagesMap = IMap.fromValues( + keyMapper: (x) => x.timestamp, + values: sendingMessages, + ); + final unreconciledMessagesMap = IMap.fromValues( + keyMapper: (x) => x.timestamp, + values: unreconciledMessages, + ); + + final renderedElements = {}; + + for (final m in reconciledMessagesMap.entries) { + renderedElements[m.key] = RenderStateElement( + message: m.value.value, + isLocal: m.value.value.author.toVeilid() != _remoteIdentityPublicKey, + reconciled: true, + reconciledOffline: m.value.isOffline); + } + for (final m in sentMessagesMap.entries) { + renderedElements.putIfAbsent( + m.key, + () => RenderStateElement( + message: m.value.value, + isLocal: true, + )) + ..sent = true + ..sentOffline = m.value.isOffline; + } + for (final m in unreconciledMessagesMap.entries) { + renderedElements + .putIfAbsent( + m.key, + () => RenderStateElement( + message: m.value, + isLocal: + m.value.author.toVeilid() != _remoteIdentityPublicKey, + )) + .reconciled = false; + } + for (final m in sendingMessagesMap.entries) { + renderedElements + .putIfAbsent( + m.key, + () => RenderStateElement( + message: m.value, + isLocal: true, + )) + .sent = false; + } + + // Render the state + final messageKeys = renderedElements.entries + .toIList() + .sort((x, y) => x.key.compareTo(y.key)); + final renderedState = messageKeys + .map((x) => MessageState( + author: x.value.message.author.toVeilid(), + timestamp: Timestamp.fromInt64(x.key), + text: x.value.message.text, + sendState: x.value.sendState)) + .toIList(); + + // Emit the rendered state + emit(AsyncValue.data(renderedState)); + } + + void addMessage({required proto.Message message}) { + _unreconciledMessagesQueue.addSync(message); + _sendingMessagesQueue.addSync(message); + + // Update the view + _renderState(); } final WaitSet _initWait = WaitSet(); @@ -245,16 +424,15 @@ class SingleContactMessagesCubit extends Cubit { late final DHTRecordCrypto _messagesCrypto; - DHTShortArrayCubit? _localMessagesCubit; - DHTShortArrayCubit? _remoteMessagesCubit; - DHTShortArrayCubit? _reconciledChatMessagesCubit; + DHTShortArrayCubit? _sentMessagesCubit; + DHTShortArrayCubit? _rcvdMessagesCubit; + DHTShortArrayCubit? _reconciledMessagesCubit; - final StreamController<_SingleContactMessageQueueEntry> _messagesUpdateQueue; + final PersistentQueueCubit _unreconciledMessagesQueue; + final PersistentQueueCubit _sendingMessagesQueue; - StreamSubscription>>>? - _localSubscription; - StreamSubscription>>>? - _remoteSubscription; - StreamSubscription>>>? - _reconciledChatSubscription; + StreamSubscription>? _sentSubscription; + StreamSubscription>? _rcvdSubscription; + StreamSubscription>? + _reconciledSubscription; } diff --git a/lib/chat/models/message_state.dart b/lib/chat/models/message_state.dart new file mode 100644 index 0000000..c4c6ca5 --- /dev/null +++ b/lib/chat/models/message_state.dart @@ -0,0 +1,34 @@ +import 'package:change_case/change_case.dart'; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:veilid_support/veilid_support.dart'; + +part 'message_state.freezed.dart'; +part 'message_state.g.dart'; + +// Whether or not a message has been fully sent +enum MessageSendState { + // message is still being sent + sending, + // message issued has not reached the network + sent, + // message was sent and has reached the network + delivered; + + factory MessageSendState.fromJson(dynamic j) => + MessageSendState.values.byName((j as String).toCamelCase()); + String toJson() => name.toPascalCase(); +} + +@freezed +class MessageState with _$MessageState { + const factory MessageState({ + required TypedKey author, + required Timestamp timestamp, + required String text, + required MessageSendState? sendState, + }) = _MessageState; + + factory MessageState.fromJson(dynamic json) => + _$MessageStateFromJson(json as Map); +} diff --git a/lib/chat/models/message_state.freezed.dart b/lib/chat/models/message_state.freezed.dart new file mode 100644 index 0000000..3d76551 --- /dev/null +++ b/lib/chat/models/message_state.freezed.dart @@ -0,0 +1,229 @@ +// 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 'message_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#adding-getters-and-methods-to-our-models'); + +MessageState _$MessageStateFromJson(Map json) { + return _MessageState.fromJson(json); +} + +/// @nodoc +mixin _$MessageState { + Typed get author => throw _privateConstructorUsedError; + Timestamp get timestamp => throw _privateConstructorUsedError; + String get text => throw _privateConstructorUsedError; + MessageSendState? get sendState => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $MessageStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $MessageStateCopyWith<$Res> { + factory $MessageStateCopyWith( + MessageState value, $Res Function(MessageState) then) = + _$MessageStateCopyWithImpl<$Res, MessageState>; + @useResult + $Res call( + {Typed author, + Timestamp timestamp, + String text, + MessageSendState? sendState}); +} + +/// @nodoc +class _$MessageStateCopyWithImpl<$Res, $Val extends MessageState> + implements $MessageStateCopyWith<$Res> { + _$MessageStateCopyWithImpl(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? author = null, + Object? timestamp = null, + Object? text = null, + Object? sendState = freezed, + }) { + return _then(_value.copyWith( + author: null == author + ? _value.author + : author // ignore: cast_nullable_to_non_nullable + as Typed, + timestamp: null == timestamp + ? _value.timestamp + : timestamp // ignore: cast_nullable_to_non_nullable + as Timestamp, + text: null == text + ? _value.text + : text // ignore: cast_nullable_to_non_nullable + as String, + sendState: freezed == sendState + ? _value.sendState + : sendState // ignore: cast_nullable_to_non_nullable + as MessageSendState?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$MessageStateImplCopyWith<$Res> + implements $MessageStateCopyWith<$Res> { + factory _$$MessageStateImplCopyWith( + _$MessageStateImpl value, $Res Function(_$MessageStateImpl) then) = + __$$MessageStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {Typed author, + Timestamp timestamp, + String text, + MessageSendState? sendState}); +} + +/// @nodoc +class __$$MessageStateImplCopyWithImpl<$Res> + extends _$MessageStateCopyWithImpl<$Res, _$MessageStateImpl> + implements _$$MessageStateImplCopyWith<$Res> { + __$$MessageStateImplCopyWithImpl( + _$MessageStateImpl _value, $Res Function(_$MessageStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? author = null, + Object? timestamp = null, + Object? text = null, + Object? sendState = freezed, + }) { + return _then(_$MessageStateImpl( + author: null == author + ? _value.author + : author // ignore: cast_nullable_to_non_nullable + as Typed, + timestamp: null == timestamp + ? _value.timestamp + : timestamp // ignore: cast_nullable_to_non_nullable + as Timestamp, + text: null == text + ? _value.text + : text // ignore: cast_nullable_to_non_nullable + as String, + sendState: freezed == sendState + ? _value.sendState + : sendState // ignore: cast_nullable_to_non_nullable + as MessageSendState?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { + const _$MessageStateImpl( + {required this.author, + required this.timestamp, + required this.text, + required this.sendState}); + + factory _$MessageStateImpl.fromJson(Map json) => + _$$MessageStateImplFromJson(json); + + @override + final Typed author; + @override + final Timestamp timestamp; + @override + final String text; + @override + final MessageSendState? sendState; + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'MessageState(author: $author, timestamp: $timestamp, text: $text, sendState: $sendState)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'MessageState')) + ..add(DiagnosticsProperty('author', author)) + ..add(DiagnosticsProperty('timestamp', timestamp)) + ..add(DiagnosticsProperty('text', text)) + ..add(DiagnosticsProperty('sendState', sendState)); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$MessageStateImpl && + (identical(other.author, author) || other.author == author) && + (identical(other.timestamp, timestamp) || + other.timestamp == timestamp) && + (identical(other.text, text) || other.text == text) && + (identical(other.sendState, sendState) || + other.sendState == sendState)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, author, timestamp, text, sendState); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$MessageStateImplCopyWith<_$MessageStateImpl> get copyWith => + __$$MessageStateImplCopyWithImpl<_$MessageStateImpl>(this, _$identity); + + @override + Map toJson() { + return _$$MessageStateImplToJson( + this, + ); + } +} + +abstract class _MessageState implements MessageState { + const factory _MessageState( + {required final Typed author, + required final Timestamp timestamp, + required final String text, + required final MessageSendState? sendState}) = _$MessageStateImpl; + + factory _MessageState.fromJson(Map json) = + _$MessageStateImpl.fromJson; + + @override + Typed get author; + @override + Timestamp get timestamp; + @override + String get text; + @override + MessageSendState? get sendState; + @override + @JsonKey(ignore: true) + _$$MessageStateImplCopyWith<_$MessageStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/chat/models/message_state.g.dart b/lib/chat/models/message_state.g.dart new file mode 100644 index 0000000..5324b93 --- /dev/null +++ b/lib/chat/models/message_state.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'message_state.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$MessageStateImpl _$$MessageStateImplFromJson(Map json) => + _$MessageStateImpl( + author: Typed.fromJson(json['author']), + timestamp: Timestamp.fromJson(json['timestamp']), + text: json['text'] as String, + sendState: json['send_state'] == null + ? null + : MessageSendState.fromJson(json['send_state']), + ); + +Map _$$MessageStateImplToJson(_$MessageStateImpl instance) => + { + 'author': instance.author.toJson(), + 'timestamp': instance.timestamp.toJson(), + 'text': instance.text, + 'send_state': instance.sendState?.toJson(), + }; diff --git a/lib/chat/models/models.dart b/lib/chat/models/models.dart new file mode 100644 index 0000000..2d92e01 --- /dev/null +++ b/lib/chat/models/models.dart @@ -0,0 +1 @@ +export 'message_state.dart'; diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 35a2987..d0d3f39 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:async_tools/async_tools.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/material.dart'; @@ -99,38 +97,52 @@ class ChatComponent extends StatelessWidget { ///////////////////////////////////////////////////////////////////// - types.Message messageToChatMessage(proto.Message message) { - final isLocal = message.author == _localUserIdentityKey.toProto(); + types.Message messageToChatMessage(MessageState message) { + final isLocal = message.author == _localUserIdentityKey; + + types.Status? status; + if (message.sendState != null) { + assert(isLocal, 'send state should only be on sent messages'); + switch (message.sendState!) { + case MessageSendState.sending: + status = types.Status.sending; + case MessageSendState.sent: + status = types.Status.sent; + case MessageSendState.delivered: + status = types.Status.delivered; + } + } final textMessage = types.TextMessage( - author: isLocal ? _localUser : _remoteUser, - createdAt: (message.timestamp ~/ 1000).toInt(), - id: message.timestamp.toString(), - text: message.text, - ); + author: isLocal ? _localUser : _remoteUser, + createdAt: (message.timestamp.value ~/ BigInt.from(1000)).toInt(), + id: message.timestamp.toString(), + text: message.text, + showStatus: status != null, + status: status); return textMessage; } - Future _addMessage(proto.Message message) async { + void _addMessage(proto.Message message) { if (message.text.isEmpty) { return; } - await _messagesCubit.addMessage(message: message); + _messagesCubit.addMessage(message: message); } - Future _handleSendPressed(types.PartialText message) async { + void _handleSendPressed(types.PartialText message) { final protoMessage = proto.Message() ..author = _localUserIdentityKey.toProto() ..timestamp = Veilid.instance.now().toInt64() ..text = message.text; //..signature = signature; - await _addMessage(protoMessage); + _addMessage(protoMessage); } - Future _handleAttachmentPressed() async { - // - } + // void _handleAttachmentPressed() async { + // // + // } @override Widget build(BuildContext context) { @@ -195,10 +207,7 @@ class ChatComponent extends StatelessWidget { //onAttachmentPressed: _handleAttachmentPressed, //onMessageTap: _handleMessageTap, //onPreviewDataFetched: _handlePreviewDataFetched, - onSendPressed: (message) { - singleFuture( - this, () async => _handleSendPressed(message)); - }, + onSendPressed: _handleSendPressed, //showUserAvatars: false, //showUserNames: true, user: _localUser, 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 b79191b..ff903ca 100644 --- a/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart +++ b/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart @@ -85,14 +85,14 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit c.remoteConversationRecordKey.toVeilid() == key); + final contactIndex = contactList.indexWhere( + (c) => c.value.remoteConversationRecordKey.toVeilid() == key); if (contactIndex == -1) { await addState(key, AsyncValue.error('Contact not found')); return; } final contact = contactList[contactIndex]; - await _addConversation(contact: contact); + await _addConversation(contact: contact.value); } //// 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 index 7ebbdb7..19ad150 100644 --- 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 @@ -2,7 +2,6 @@ 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'; @@ -16,7 +15,7 @@ import 'chat_list_cubit.dart'; // Wraps a MessagesCubit to stream the latest messages to the state // Automatically follows the state of a ActiveConversationsBlocMapCubit. class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit>, SingleContactMessagesCubit> + SingleContactMessagesState, SingleContactMessagesCubit> with StateMapFollower> { @@ -61,14 +60,14 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit c.remoteConversationRecordKey.toVeilid() == key); + final contactIndex = contactList.indexWhere( + (c) => c.value.remoteConversationRecordKey.toVeilid() == key); if (contactIndex == -1) { await addState( key, AsyncValue.error('Contact not found for conversation')); return; } - final contact = contactList[contactIndex]; + final contact = contactList[contactIndex].value; // Get the chat object for this single contact chat final chatList = _chatListCubit.state.state.asData?.value; @@ -76,13 +75,13 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit c.remoteConversationRecordKey.toVeilid() == key); + final chatIndex = chatList.indexWhere( + (c) => c.value.remoteConversationRecordKey.toVeilid() == key); if (contactIndex == -1) { await addState(key, AsyncValue.error('Chat not found for conversation')); return; } - final chat = chatList[chatIndex]; + final chat = chatList[chatIndex].value; await value.when( data: (state) => _addConversationMessages( diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index 990c0d1..4a0818a 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -1,6 +1,5 @@ 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'; @@ -14,7 +13,7 @@ import '../../tools/tools.dart'; ////////////////////////////////////////////////// // Mutable state for per-account chat list -typedef ChatListCubitState = BlocBusyState>>; +typedef ChatListCubitState = DHTShortArrayBusyState; class ChatListCubit extends DHTShortArrayCubit with StateMapFollowable { @@ -119,8 +118,8 @@ class ChatListCubit extends DHTShortArrayCubit // chat record now if (success && deletedItem != null) { try { - await DHTRecordPool.instance - .delete(deletedItem.reconciledChatRecord.toVeilid().recordKey); + await DHTRecordPool.instance.deleteRecord( + deletedItem.reconciledChatRecord.toVeilid().recordKey); } on Exception catch (e) { log.debug('error removing reconciled chat record: $e', e); } @@ -135,8 +134,8 @@ class ChatListCubit extends DHTShortArrayCubit return IMap(); } return IMap.fromIterable(stateValue, - keyMapper: (e) => e.remoteConversationRecordKey.toVeilid(), - valueMapper: (e) => e); + keyMapper: (e) => e.value.remoteConversationRecordKey.toVeilid(), + valueMapper: (e) => e.value); } final ActiveChatCubit activeChatCubit; 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 c1f54d8..a671011 100644 --- a/lib/chat_list/views/chat_single_contact_list_widget.dart +++ b/lib/chat_list/views/chat_single_contact_list_widget.dart @@ -20,8 +20,8 @@ class ChatSingleContactListWidget extends StatelessWidget { return contactListV.builder((context, contactList) { final contactMap = IMap.fromIterable(contactList, - keyMapper: (c) => c.remoteConversationRecordKey, - valueMapper: (c) => c); + keyMapper: (c) => c.value.remoteConversationRecordKey, + valueMapper: (c) => c.value); final chatListV = context.watch().state; return chatListV @@ -33,7 +33,7 @@ class ChatSingleContactListWidget extends StatelessWidget { child: (chatList.isEmpty) ? const EmptyChatListWidget() : SearchableList( - initialList: chatList.toList(), + initialList: chatList.map((x) => x.value).toList(), builder: (l, i, c) { final contact = contactMap[c.remoteConversationRecordKey]; @@ -47,7 +47,7 @@ class ChatSingleContactListWidget extends StatelessWidget { }, filter: (value) { final lowerValue = value.toLowerCase(); - return chatList.where((c) { + return chatList.map((x) => x.value).where((c) { final contact = contactMap[c.remoteConversationRecordKey]; if (contact == null) { diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index 03df901..a24005a 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -1,6 +1,5 @@ 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:fixnum/fixnum.dart'; @@ -27,7 +26,7 @@ typedef GetEncryptionKeyCallback = Future Function( ////////////////////////////////////////////////// typedef ContactInvitiationListState - = BlocBusyState>>; + = DHTShortArrayBusyState; ////////////////////////////////////////////////// // Mutable state for per-account contact invitations @@ -208,13 +207,14 @@ class ContactInvitationListCubit await contactRequestInbox.tryWriteBytes(Uint8List(0)); }); try { - await pool.delete(contactRequestInbox.recordKey); + await pool.deleteRecord(contactRequestInbox.recordKey); } on Exception catch (e) { log.debug('error removing contact request inbox: $e', e); } if (!accepted) { try { - await pool.delete(deletedItem.localConversationRecordKey.toVeilid()); + await pool + .deleteRecord(deletedItem.localConversationRecordKey.toVeilid()); } on Exception catch (e) { log.debug('error removing local conversation record: $e', e); } @@ -246,7 +246,7 @@ class ContactInvitationListCubit // If we're chatting to ourselves, // we are validating an invitation we have created final isSelf = state.state.asData!.value.indexWhere((cir) => - cir.contactRequestInbox.recordKey.toVeilid() == + cir.value.contactRequestInbox.recordKey.toVeilid() == contactRequestInboxKey) != -1; @@ -315,8 +315,8 @@ class ContactInvitationListCubit return IMap(); } return IMap.fromIterable(stateValue, - keyMapper: (e) => e.contactRequestInbox.recordKey.toVeilid(), - valueMapper: (e) => e); + keyMapper: (e) => e.value.contactRequestInbox.recordKey.toVeilid(), + valueMapper: (e) => e.value); } // 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 bf81b4e..7c06bf7 100644 --- a/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart @@ -1,6 +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:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; @@ -16,10 +15,8 @@ typedef WaitingInvitationsBlocMapState class WaitingInvitationsBlocMapCubit extends BlocMapCubit, WaitingInvitationCubit> with - StateMapFollower< - BlocBusyState>>, - TypedKey, - proto.ContactInvitationRecord> { + StateMapFollower, + TypedKey, proto.ContactInvitationRecord> { WaitingInvitationsBlocMapCubit( {required this.activeAccountInfo, required this.account}); diff --git a/lib/contacts/cubits/conversation_cubit.dart b/lib/contacts/cubits/conversation_cubit.dart index 7b2dde2..7e23a99 100644 --- a/lib/contacts/cubits/conversation_cubit.dart +++ b/lib/contacts/cubits/conversation_cubit.dart @@ -173,8 +173,8 @@ class ConversationCubit extends Cubit> { await localConversationCubit.close(); final conversation = data.value; final messagesKey = conversation.messages.toVeilid(); - await pool.delete(messagesKey); - await pool.delete(_localConversationRecordKey!); + await pool.deleteRecord(messagesKey); + await pool.deleteRecord(_localConversationRecordKey!); _localConversationRecordKey = null; }); } @@ -191,8 +191,8 @@ class ConversationCubit extends Cubit> { await remoteConversationCubit.close(); final conversation = data.value; final messagesKey = conversation.messages.toVeilid(); - await pool.delete(messagesKey); - await pool.delete(_remoteConversationRecordKey!); + await pool.deleteRecord(messagesKey); + await pool.deleteRecord(_remoteConversationRecordKey!); }); } 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 304d534..0d8650e 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 cilState = context.watch().state; final cilBusy = cilState.busy; final contactInvitationRecordList = - cilState.state.asData?.value ?? const IListConst([]); + cilState.state.asData?.value.map((x) => x.value).toIList() ?? + const IListConst([]); final ciState = context.watch().state; final ciBusy = ciState.busy; - final contactList = ciState.state.asData?.value ?? const IListConst([]); + final contactList = + ciState.state.asData?.value.map((x) => x.value).toIList() ?? + const IListConst([]); return SizedBox( child: Column(children: [ diff --git a/lib/proto/veilidchat.proto b/lib/proto/veilidchat.proto index 1727ee1..42692ac 100644 --- a/lib/proto/veilidchat.proto +++ b/lib/proto/veilidchat.proto @@ -27,8 +27,6 @@ message Attachment { } // A single message as part of a series of messages -// Messages are stored in a DHTLog -// DHT Schema: SMPL(0,1,[identityPublicKey]) message Message { // Author of the message veilid.TypedKey author = 1; @@ -53,7 +51,6 @@ message Message { // DHT Key (UnicastOutbox): localConversation // DHT Secret: None // Encryption: DH(IdentityA, IdentityB) - message Conversation { // Profile to publish to friend Profile profile = 1; diff --git a/lib/router/cubit/router_cubit.dart b/lib/router/cubit/router_cubit.dart index 83bc477..f30a617 100644 --- a/lib/router/cubit/router_cubit.dart +++ b/lib/router/cubit/router_cubit.dart @@ -16,11 +16,20 @@ import '../../veilid_processor/views/developer.dart'; part 'router_cubit.freezed.dart'; part 'router_cubit.g.dart'; -part 'router_state.dart'; final _rootNavKey = GlobalKey(debugLabel: 'rootNavKey'); final _homeNavKey = GlobalKey(debugLabel: 'homeNavKey'); +@freezed +class RouterState with _$RouterState { + const factory RouterState( + {required bool hasAnyAccount, + required bool hasActiveChat}) = _RouterState; + + factory RouterState.fromJson(dynamic json) => + _$RouterStateFromJson(json as Map); +} + class RouterCubit extends Cubit { RouterCubit(AccountRepository accountRepository) : super(RouterState( diff --git a/lib/router/cubit/router_state.dart b/lib/router/cubit/router_state.dart deleted file mode 100644 index ac60c39..0000000 --- a/lib/router/cubit/router_state.dart +++ /dev/null @@ -1,11 +0,0 @@ -part of 'router_cubit.dart'; - -@freezed -class RouterState with _$RouterState { - const factory RouterState( - {required bool hasAnyAccount, - required bool hasActiveChat}) = _RouterState; - - factory RouterState.fromJson(dynamic json) => - _$RouterStateFromJson(json as Map); -} diff --git a/packages/bloc_tools/lib/src/future_cubit.dart b/packages/bloc_tools/lib/src/future_cubit.dart index b14ac72..39be126 100644 --- a/packages/bloc_tools/lib/src/future_cubit.dart +++ b/packages/bloc_tools/lib/src/future_cubit.dart @@ -5,12 +5,20 @@ import 'package:bloc/bloc.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)); - })); + _initWait.add(() async => fut.then((value) { + emit(AsyncValue.data(value)); + // ignore: avoid_types_on_closure_parameters + }, onError: (Object e, StackTrace stackTrace) { + emit(AsyncValue.error(e, stackTrace)); + })); } FutureCubit.value(State state) : super(AsyncValue.data(state)); + + @override + Future close() async { + await _initWait(); + await super.close(); + } + + final WaitSet _initWait = WaitSet(); } 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 0e23c5a..af16842 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 @@ -86,7 +86,7 @@ class DHTRecord { if (_open) { await close(); } - await DHTRecordPool.instance.delete(key); + await DHTRecordPool.instance.deleteRecord(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 bcdbc42..333558e 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 @@ -109,7 +109,7 @@ class OpenedRecordInfo { String get sharedDetails => shared.toString(); } -class DHTRecordPool with TableDBBacked { +class DHTRecordPool with TableDBBackedJson { DHTRecordPool._(Veilid veilid, VeilidRoutingContext routingContext) : _state = const DHTRecordPoolAllocations(), _mutex = Mutex(), @@ -150,7 +150,7 @@ class DHTRecordPool with TableDBBacked { ? DHTRecordPoolAllocations.fromJson(obj) : const DHTRecordPoolAllocations(); @override - Object? valueToJson(DHTRecordPoolAllocations val) => val.toJson(); + Object? valueToJson(DHTRecordPoolAllocations? val) => val?.toJson(); ////////////////////////////////////////////////////////////// @@ -161,7 +161,7 @@ class DHTRecordPool with TableDBBacked { final globalPool = DHTRecordPool._(Veilid.instance, routingContext); globalPool .._logger = logger - .._state = await globalPool.load(); + .._state = await globalPool.load() ?? const DHTRecordPoolAllocations(); _singleton = globalPool; } @@ -279,7 +279,7 @@ class DHTRecordPool with TableDBBacked { if (openedRecordInfo.records.isEmpty) { await _routingContext.closeDHTRecord(key); if (openedRecordInfo.shared.deleteOnClose) { - await _deleteInner(key); + await _deleteRecordInner(key); } _opened.remove(key); } @@ -316,7 +316,7 @@ class DHTRecordPool with TableDBBacked { } } - Future _deleteInner(TypedKey recordKey) async { + Future _deleteRecordInner(TypedKey recordKey) async { log('deleteDHTRecord: key=$recordKey'); // Remove this child from parents @@ -324,7 +324,7 @@ class DHTRecordPool with TableDBBacked { await _routingContext.deleteDHTRecord(recordKey); } - Future delete(TypedKey recordKey) async { + Future deleteRecord(TypedKey recordKey) async { await _mutex.protect(() async { final allDeps = _collectChildrenInner(recordKey); @@ -339,7 +339,7 @@ class DHTRecordPool with TableDBBacked { ori.shared.deleteOnClose = true; } else { // delete now - await _deleteInner(recordKey); + await _deleteRecordInner(recordKey); } }); } 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 dcf5cb4..5a4210f 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 @@ -69,7 +69,7 @@ class DHTShortArray { return dhtShortArray; } on Exception catch (_) { await dhtRecord.close(); - await pool.delete(dhtRecord.key); + await pool.deleteRecord(dhtRecord.key); rethrow; } } @@ -152,7 +152,7 @@ class DHTShortArray { /// Free all resources for the DHTShortArray and delete it from the DHT Future delete() async { await close(); - await DHTRecordPool.instance.delete(recordKey); + await DHTRecordPool.instance.deleteRecord(recordKey); } /// Runs a closure that guarantees the DHTShortArray @@ -212,6 +212,8 @@ class DHTShortArray { return closure(writer); }, timeout: timeout); + /// Listen to and any all changes to the structure of this short array + /// regardless of where the changes are coming from 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 52214ba..cdce828 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 @@ -3,11 +3,24 @@ import 'dart:async'; import 'package:async_tools/async_tools.dart'; import 'package:bloc/bloc.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'; import '../../../veilid_support.dart'; -typedef DHTShortArrayState = AsyncValue>; +@immutable +class DHTShortArrayElementState extends Equatable { + const DHTShortArrayElementState( + {required this.value, required this.isOffline}); + final T value; + final bool isOffline; + + @override + List get props => [value, isOffline]; +} + +typedef DHTShortArrayState = AsyncValue>>; typedef DHTShortArrayBusyState = BlocBusyState>; class DHTShortArrayCubit extends Cubit> @@ -49,13 +62,19 @@ class DHTShortArrayCubit extends Cubit> Future _refreshNoWait({bool forceRefresh = false}) async => busy((emit) async => _refreshInner(emit, forceRefresh: forceRefresh)); - Future _refreshInner(void Function(AsyncValue>) emit, + Future _refreshInner(void Function(DHTShortArrayState) emit, {bool forceRefresh = false}) async { try { - final newState = (await _shortArray.operate( - (reader) => reader.getAllItems(forceRefresh: forceRefresh))) - ?.map(_decodeElement) - .toIList(); + final newState = await _shortArray.operate((reader) async { + final offlinePositions = await reader.getOfflinePositions(); + final allItems = (await reader.getAllItems(forceRefresh: forceRefresh)) + ?.indexed + .map((x) => DHTShortArrayElementState( + value: _decodeElement(x.$2), + isOffline: offlinePositions.contains(x.$1))) + .toIList(); + return allItems; + }); if (newState != null) { emit(AsyncValue.data(newState)); } 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 index 3151e6e..44e565d 100644 --- 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 @@ -15,6 +15,9 @@ abstract class DHTShortArrayRead { /// 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}); + + /// Get a list of the positions that were written offline and not flushed yet + Future> getOfflinePositions(); } extension DHTShortArrayReadExt on DHTShortArrayRead { @@ -96,6 +99,40 @@ class _DHTShortArrayRead implements DHTShortArrayRead { return out; } + /// Get a list of the positions that were written offline and not flushed yet + @override + Future> getOfflinePositions() async { + final indexOffline = {}; + final inspects = await [ + _head._headRecord.inspect(), + ..._head._linkedRecords.map((lr) => lr.inspect()) + ].wait; + + // Add to offline index + var strideOffset = 0; + for (final inspect in inspects) { + for (final r in inspect.offlineSubkeys) { + for (var i = r.low; i <= r.high; i++) { + // If this is the head record, ignore the first head subkey + if (strideOffset != 0 || i != 0) { + indexOffline.add(i + ((strideOffset == 0) ? -1 : strideOffset)); + } + } + } + strideOffset += _head._stride; + } + + // See which positions map to offline indexes + final positionOffline = {}; + for (var i = 0; i < _head._index.length; i++) { + final idx = _head._index[i]; + if (indexOffline.contains(idx)) { + positionOffline.add(i); + } + } + return positionOffline; + } + //////////////////////////////////////////////////////////////////////////// // 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 index a91543c..af6204e 100644 --- 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 @@ -92,12 +92,11 @@ extension DHTShortArrayWriteExt on DHTShortArrayWrite { } //////////////////////////////////////////////////////////////////////////// -// Writer-only implementation +// Writer implementation -class _DHTShortArrayWrite implements DHTShortArrayWrite { - _DHTShortArrayWrite._(_DHTShortArrayHead head) - : _head = head, - _reader = _DHTShortArrayRead._(head); +class _DHTShortArrayWrite extends _DHTShortArrayRead + implements DHTShortArrayWrite { + _DHTShortArrayWrite._(super.head) : super._(); @override Future tryAddItem(Uint8List value) async { @@ -187,23 +186,4 @@ class _DHTShortArrayWrite implements DHTShortArrayWrite { } 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; } 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 a50d893..da278d4 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 @@ -2,48 +2,47 @@ import 'dart:async'; import 'package:async_tools/async_tools.dart'; import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; +import 'package:mutex/mutex.dart'; import 'table_db.dart'; -abstract class AsyncTableDBBackedCubit extends Cubit> - with TableDBBacked { +abstract class AsyncTableDBBackedCubit extends Cubit> + with TableDBBackedJson { AsyncTableDBBackedCubit() : super(const AsyncValue.loading()) { - unawaited(Future.delayed(Duration.zero, _build)); + _initWait.add(_build); + } + + @override + Future close() async { + // Ensure the init finished + await _initWait(); + // Wait for any setStates to finish + await _mutex.acquire(); + + await super.close(); } Future _build() async { try { - emit(AsyncValue.data(await load())); + await _mutex.protect(() async { + 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 { + @protected + Future setState(T? newState) async { + await _initWait(); try { emit(AsyncValue.data(await store(newState))); } on Exception catch (e, stackTrace) { emit(AsyncValue.error(e, stackTrace)); } } + + final WaitSet _initWait = WaitSet(); + final Mutex _mutex = Mutex(); } diff --git a/packages/veilid_support/lib/src/identity.dart b/packages/veilid_support/lib/src/identity.dart index 5810bc5..9d26a7d 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 pool.delete(masterRecordKey); + await pool.deleteRecord(masterRecordKey); } Future get identityCrypto => @@ -111,6 +111,9 @@ extension IdentityMasterExtension on IdentityMaster { TypedKey identityPublicTypedKey() => TypedKey(kind: identityRecordKey.kind, value: identityPublicKey); + TypedKey masterPublicTypedKey() => + TypedKey(kind: identityRecordKey.kind, value: masterPublicKey); + Future validateIdentitySecret( SecretKey identitySecret) async { final cs = await identityCrypto; diff --git a/packages/veilid_support/lib/src/persistent_queue_cubit.dart b/packages/veilid_support/lib/src/persistent_queue_cubit.dart new file mode 100644 index 0000000..6cc79c1 --- /dev/null +++ b/packages/veilid_support/lib/src/persistent_queue_cubit.dart @@ -0,0 +1,194 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:async_tools/async_tools.dart'; +import 'package:bloc/bloc.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:mutex/mutex.dart'; +import 'package:protobuf/protobuf.dart'; + +import 'table_db.dart'; + +class PersistentQueueCubit + extends Cubit>> with TableDBBackedFromBuffer> { + // + PersistentQueueCubit( + {required String table, + required String key, + required T Function(Uint8List) fromBuffer, + bool deleteOnClose = true}) + : _table = table, + _key = key, + _fromBuffer = fromBuffer, + _deleteOnClose = deleteOnClose, + super(const AsyncValue.loading()) { + _initWait.add(_build); + unawaited(Future.delayed(Duration.zero, () async { + await for (final elem in _syncAddController.stream) { + await addAll(elem); + } + })); + } + + @override + Future close() async { + // Ensure the init finished + await _initWait(); + + // Close the sync add stream + await _syncAddController.close(); + + // Wait for any setStates to finish + await _stateMutex.acquire(); + + // Clean up table if desired + if (_deleteOnClose) { + await delete(); + } + + await super.close(); + } + + Future _build() async { + await _stateMutex.protect(() async { + try { + emit(AsyncValue.data(await load() ?? await store(IList.empty()))); + } on Exception catch (e, stackTrace) { + emit(AsyncValue.error(e, stackTrace)); + } + }); + } + + Future _setStateInner(IList newState) async { + emit(AsyncValue.data(await store(newState))); + } + + Future add(T item) async { + await _initWait(); + await _stateMutex.protect(() async { + final queue = state.asData!.value.add(item); + await _setStateInner(queue); + }); + } + + Future addAll(IList items) async { + await _initWait(); + await _stateMutex.protect(() async { + var queue = state.asData!.value; + for (final item in items) { + queue = queue.add(item); + } + await _setStateInner(queue); + }); + } + + void addSync(T item) { + _syncAddController.sink.add([item].toIList()); + } + + void addAllSync(IList items) { + _syncAddController.sink.add(items.toIList()); + } + + Future get isEmpty async { + await _initWait(); + return state.asData!.value.isEmpty; + } + + Future get isNotEmpty async { + await _initWait(); + return state.asData!.value.isNotEmpty; + } + + Future get length async { + await _initWait(); + return state.asData!.value.length; + } + + // Future pop() async { + // await _initWait(); + // return _processingMutex.protect(() async => _stateMutex.protect(() async { + // final removedItem = Output(); + // final queue = state.asData!.value.removeAt(0, removedItem); + // await _setStateInner(queue); + // return removedItem.value; + // })); + // } + + // Future> popAll() async { + // await _initWait(); + // return _processingMutex.protect(() async => _stateMutex.protect(() async { + // final queue = state.asData!.value; + // await _setStateInner(IList.empty); + // return queue; + // })); + // } + + Future process(Future Function(IList) closure, + {int? count}) async { + await _initWait(); + // Only one processor at a time + return _processingMutex.protect(() async { + // Take 'count' items from the front of the list + final toProcess = await _stateMutex.protect(() async { + final queue = state.asData!.value; + final processCount = (count ?? queue.length).clamp(0, queue.length); + return queue.take(processCount).toIList(); + }); + + // Run the processing closure + final processCount = toProcess.length; + final out = await closure(toProcess); + + // If there was nothing to process just return + if (toProcess.isEmpty) { + return out; + } + + // If there was no exception, remove the processed items + return _stateMutex.protect(() async { + // Get the queue from the state again as items could + // have been added during processing + final queue = state.asData!.value; + final newQueue = queue.skip(processCount).toIList(); + await _setStateInner(newQueue); + return out; + }); + }); + } + + // TableDBBacked + @override + String tableKeyName() => _key; + + @override + String tableName() => _table; + + @override + IList valueFromBuffer(Uint8List bytes) { + final reader = CodedBufferReader(bytes); + var out = IList(); + while (!reader.isAtEnd()) { + out = out.add(_fromBuffer(reader.readBytesAsView())); + } + return out; + } + + @override + Uint8List valueToBuffer(IList val) { + final writer = CodedBufferWriter(); + for (final elem in val) { + writer.writeRawBytes(elem.writeToBuffer()); + } + return writer.toBuffer(); + } + + final String _table; + final String _key; + final T Function(Uint8List) _fromBuffer; + final bool _deleteOnClose; + final WaitSet _initWait = WaitSet(); + final Mutex _stateMutex = Mutex(); + final Mutex _processingMutex = Mutex(); + final StreamController> _syncAddController = StreamController(); +} diff --git a/packages/veilid_support/lib/src/table_db.dart b/packages/veilid_support/lib/src/table_db.dart index 1e09fc4..d0fb72e 100644 --- a/packages/veilid_support/lib/src/table_db.dart +++ b/packages/veilid_support/lib/src/table_db.dart @@ -1,6 +1,9 @@ import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; import 'package:async_tools/async_tools.dart'; +import 'package:meta/meta.dart'; import 'package:veilid/veilid.dart'; Future tableScope( @@ -32,14 +35,19 @@ Future transactionScope( } } -abstract mixin class TableDBBacked { +abstract mixin class TableDBBackedJson { + @protected String tableName(); + @protected String tableKeyName(); - T valueFromJson(Object? obj); - Object? valueToJson(T val); + @protected + T? valueFromJson(Object? obj); + @protected + Object? valueToJson(T? val); /// Load things from storage - Future load() async { + @protected + Future load() async { final obj = await tableScope(tableName(), (tdb) async { final objJson = await tdb.loadStringJson(0, tableKeyName()); return valueFromJson(objJson); @@ -48,28 +56,98 @@ abstract mixin class TableDBBacked { } /// Store things to storage + @protected Future store(T obj) async { await tableScope(tableName(), (tdb) async { await tdb.storeStringJson(0, tableKeyName(), valueToJson(obj)); }); return obj; } + + /// Delete things from storage + @protected + Future delete() async { + final obj = await tableScope(tableName(), (tdb) async { + final objJson = await tdb.deleteStringJson(0, tableKeyName()); + return valueFromJson(objJson); + }); + return obj; + } } -class TableDBValue extends TableDBBacked { +abstract mixin class TableDBBackedFromBuffer { + @protected + String tableName(); + @protected + String tableKeyName(); + @protected + T valueFromBuffer(Uint8List bytes); + @protected + Uint8List valueToBuffer(T val); + + /// Load things from storage + @protected + Future load() async { + final obj = await tableScope(tableName(), (tdb) async { + final objBytes = await tdb.load(0, utf8.encode(tableKeyName())); + if (objBytes == null) { + return null; + } + return valueFromBuffer(objBytes); + }); + return obj; + } + + /// Store things to storage + @protected + Future store(T obj) async { + await tableScope(tableName(), (tdb) async { + await tdb.store(0, utf8.encode(tableKeyName()), valueToBuffer(obj)); + }); + return obj; + } + + /// Delete things from storage + @protected + Future delete() async { + final obj = await tableScope(tableName(), (tdb) async { + final objBytes = await tdb.delete(0, utf8.encode(tableKeyName())); + if (objBytes == null) { + return null; + } + return valueFromBuffer(objBytes); + }); + return obj; + } +} + +class TableDBValue extends TableDBBackedJson { TableDBValue({ required String tableName, required String tableKeyName, - required T Function(Object? obj) valueFromJson, - required Object? Function(T obj) valueToJson, + required T? Function(Object? obj) valueFromJson, + required Object? Function(T? obj) valueToJson, + required T Function() makeInitialValue, }) : _tableName = tableName, _valueFromJson = valueFromJson, _valueToJson = valueToJson, _tableKeyName = tableKeyName, - _streamController = StreamController.broadcast(); + _makeInitialValue = makeInitialValue, + _streamController = StreamController.broadcast() { + _initWait.add(() async { + await get(); + }); + } - AsyncData? get value => _value; - T get requireValue => _value!.value; + Future init() async { + await _initWait(); + } + + Future close() async { + await _initWait(); + } + + T get value => _value!.value; Stream get stream => _streamController.stream; Future get() async { @@ -77,7 +155,7 @@ class TableDBValue extends TableDBBacked { if (val != null) { return val.value; } - final loadedValue = await load(); + final loadedValue = await load() ?? await store(_makeInitialValue()); _value = AsyncData(loadedValue); return loadedValue; } @@ -88,11 +166,13 @@ class TableDBValue extends TableDBBacked { } AsyncData? _value; + final T Function() _makeInitialValue; final String _tableName; final String _tableKeyName; - final T Function(Object? obj) _valueFromJson; - final Object? Function(T obj) _valueToJson; + final T? Function(Object? obj) _valueFromJson; + final Object? Function(T? obj) _valueToJson; final StreamController _streamController; + final WaitSet _initWait = WaitSet(); ////////////////////////////////////////////////////////////// /// AsyncTableDBBacked @@ -101,7 +181,7 @@ class TableDBValue extends TableDBBacked { @override String tableKeyName() => _tableKeyName; @override - T valueFromJson(Object? obj) => _valueFromJson(obj); + T? valueFromJson(Object? obj) => _valueFromJson(obj); @override - Object? valueToJson(T val) => _valueToJson(val); + Object? valueToJson(T? val) => _valueToJson(val); } diff --git a/packages/veilid_support/lib/veilid_support.dart b/packages/veilid_support/lib/veilid_support.dart index 56db796..3cad8e0 100644 --- a/packages/veilid_support/lib/veilid_support.dart +++ b/packages/veilid_support/lib/veilid_support.dart @@ -10,6 +10,7 @@ export 'src/config.dart'; export 'src/identity.dart'; export 'src/json_tools.dart'; export 'src/memory_tools.dart'; +export 'src/persistent_queue_cubit.dart'; export 'src/protobuf_tools.dart'; export 'src/table_db.dart'; export 'src/veilid_log.dart' hide veilidLoggy; From 8ac9a93f7226afe181be22d7948c946939590128 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 20 Apr 2024 13:43:17 -0400 Subject: [PATCH 086/270] updates and stuff --- ios/Podfile.lock | 4 +- lib/tools/state_logger.dart | 4 +- pubspec.lock | 88 ++++++++++++++++++------------------- pubspec.yaml | 22 +++++----- 4 files changed, 60 insertions(+), 58 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 636ef83..1411245 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -144,7 +144,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: camera_avfoundation: 759172d1a77ae7be0de08fc104cfb79738b8a59e Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef + flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 GoogleDataTransport: 54dee9d48d14580407f8f5fbf2f496e92437a2f2 GoogleMLKit: 2bd0dc6253c4d4f227aad460f69215a504b2980e GoogleToolboxForMac: 8bef7c7c5cf7291c687cf5354f39f9db6399ad34 @@ -160,7 +160,7 @@ SPEC CHECKSUMS: pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0 path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 - share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 + share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 smart_auth: 4bedbc118723912d0e45a07e8ab34039c19e04f2 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec diff --git a/lib/tools/state_logger.dart b/lib/tools/state_logger.dart index 150309c..b17727f 100644 --- a/lib/tools/state_logger.dart +++ b/lib/tools/state_logger.dart @@ -4,8 +4,10 @@ import 'loggy.dart'; const Map _blocChangeLogLevels = { 'ConnectionStateCubit': LogLevel.off, - 'ActiveConversationMessagesBlocMapCubit': LogLevel.off, + 'ActiveSingleContactChatBlocMapCubit': LogLevel.off, + 'ActiveConversationsBlocMapCubit': LogLevel.off, 'DHTShortArrayCubit': LogLevel.off, + 'PersistentQueueCubit': LogLevel.off, }; const Map _blocCreateCloseLogLevels = {}; const Map _blocErrorLogLevels = {}; diff --git a/pubspec.lock b/pubspec.lock index b6e252f..6d53fc0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.0" async: dependency: transitive description: @@ -68,10 +68,10 @@ packages: dependency: "direct main" description: name: awesome_extensions - sha256: "7d235d64a81543a7e200a91b1149bef7d32241290fa483bae25b31be41449a7c" + sha256: c3bf11d07a69fe10ff5541717b920661c7a87a791ee182851f1c92a2d15b95a2 url: "https://pub.dev" source: hosted - version: "2.0.13" + version: "2.0.14" badges: dependency: "direct main" description: @@ -219,18 +219,18 @@ packages: dependency: transitive description: name: camera_android - sha256: ae5b9a996dfb8d77b02031b67f5500873d6402f33bd6a5283e932eef08542a51 + sha256: "7b0aba6398afa8475e2bc9115d976efb49cf8db781e922572d443795c04a4f4f" url: "https://pub.dev" source: hosted - version: "0.10.9" + version: "0.10.9+1" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: "5d009ae48de1c8ab621b1c4496dadb6e2a83f3223b76c6e6a4a252414105f561" + sha256: "9dbbb253aaf201a69c40cf95571f366ca936305d2de012684e21f6f1b1433d31" url: "https://pub.dev" source: hosted - version: "0.9.15" + version: "0.9.15+4" camera_platform_interface: dependency: transitive description: @@ -371,10 +371,10 @@ packages: dependency: "direct main" description: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.8" dart_style: dependency: transitive description: @@ -533,10 +533,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da + sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f" url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.0.19" flutter_shaders: dependency: transitive description: @@ -594,10 +594,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "24f77b50776d4285cc4b3a1665bb79852714c09b878363efbe64788c179c4284" + sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.5.2" freezed_annotation: dependency: "direct main" description: @@ -634,10 +634,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "5ed2687bc961f33a752017ccaa7edead3e5601b28b6376a5901bf24728556b85" + sha256: "771c8feb40ad0ef639973d7ecf1b43d55ffcedb2207fd43fab030f5639e40446" url: "https://pub.dev" source: hosted - version: "13.2.2" + version: "13.2.4" graphs: dependency: transitive description: @@ -826,10 +826,10 @@ packages: dependency: "direct main" description: name: motion_toast - sha256: f3fe9f92d9956814a1aa040c22c8a6c432cfb0c9f783163d9ec64915838e4837 + sha256: "4763b2aa3499d0bf00ffd9737479b73141d0397f532542893156efb4a5ac1994" url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.9.1" mutex: dependency: "direct main" description: @@ -889,18 +889,18 @@ packages: dependency: "direct main" description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.4" path_provider_foundation: dependency: transitive description: @@ -977,10 +977,10 @@ packages: dependency: transitive description: name: pointycastle - sha256: "70fe966348fe08c34bf929582f1d8247d9d9408130723206472b4687227e4333" + sha256: "79fbafed02cfdbe85ef3fd06c7f4bc2cbcba0177e61b765264853d4253b21744" url: "https://pub.dev" source: hosted - version: "3.8.0" + version: "3.9.0" pool: dependency: transitive description: @@ -1041,10 +1041,10 @@ packages: dependency: "direct main" description: name: qr_code_dart_scan - sha256: "8e9732d5b6e4e28d50647dc6d7713bf421148cadf28c768a10e9810bf6f3d87a" + sha256: "948271f8dc39ab3798341783f0ab7bfdb723054fdc9ea0928c0a5be8503ee01c" url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.8.0" qr_flutter: dependency: "direct main" description: @@ -1113,18 +1113,18 @@ packages: dependency: "direct main" description: name: searchable_listview - sha256: "5535ea3efa4599cf23ce52870a9580b52ece5d691aa90655ebec76d5081c9592" + sha256: d8513a968bdd540cb011220a5670b23b346e04a7bcb99690a859ed58092f72a4 url: "https://pub.dev" source: hosted - version: "2.11.1" + version: "2.11.2" share_plus: dependency: "direct main" description: name: share_plus - sha256: "05ec043470319bfbabe0adbc90d3a84cbff0426b9d9f3a6e2ad3e131fa5fa629" + sha256: fb5319f3aab4c5dda5ebb92dca978179ba21f8c783ee4380910ef4c1c6824f51 url: "https://pub.dev" source: hosted - version: "8.0.2" + version: "8.0.3" share_plus_platform_interface: dependency: transitive description: @@ -1137,18 +1137,18 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" shared_preferences_foundation: dependency: transitive description: @@ -1398,18 +1398,18 @@ packages: dependency: transitive description: name: url_launcher - sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e" + sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" url: "https://pub.dev" source: hosted - version: "6.2.5" + version: "6.2.6" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 + sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_ios: dependency: transitive description: @@ -1446,10 +1446,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d" + sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" url_launcher_windows: dependency: transitive description: @@ -1462,10 +1462,10 @@ packages: dependency: "direct main" description: name: uuid - sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 + sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" url: "https://pub.dev" source: hosted - version: "4.3.3" + version: "4.4.0" vector_graphics: dependency: transitive description: @@ -1540,10 +1540,10 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.4.5" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3853a75..f8ac76c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: archive: ^3.4.10 async_tools: path: packages/async_tools - awesome_extensions: ^2.0.13 + awesome_extensions: ^2.0.14 badges: ^3.1.2 basic_utils: ^5.7.0 bloc: ^8.1.4 @@ -25,7 +25,7 @@ dependencies: circular_profile_avatar: ^2.0.5 circular_reveal_animation: ^2.0.1 cool_dropdown: ^2.1.0 - cupertino_icons: ^1.0.6 + cupertino_icons: ^1.0.8 equatable: ^2.0.5 fast_immutable_collections: ^10.2.2 fixnum: ^1.1.0 @@ -46,7 +46,7 @@ dependencies: flutter_translate: ^4.0.4 form_builder_validators: ^9.1.0 freezed_annotation: ^2.4.1 - go_router: ^13.2.2 + go_router: ^13.2.4 hydrated_bloc: ^9.1.5 image: ^4.1.7 intl: ^0.18.1 @@ -54,30 +54,30 @@ dependencies: loggy: ^2.0.3 meta: ^1.11.0 mobile_scanner: ^4.0.1 - motion_toast: ^2.9.0 + motion_toast: ^2.9.1 mutex: path: packages/mutex pasteboard: ^0.2.0 path: ^1.9.0 - path_provider: ^2.1.2 + path_provider: ^2.1.3 pinput: ^4.0.0 preload_page_view: ^0.2.0 protobuf: ^3.1.0 provider: ^6.1.2 - qr_code_dart_scan: ^0.7.6 + qr_code_dart_scan: ^0.8.0 qr_flutter: ^4.1.0 quickalert: ^1.1.0 radix_colors: ^1.0.4 reorderable_grid: ^1.0.10 - searchable_listview: ^2.11.1 - share_plus: ^8.0.2 - shared_preferences: ^2.2.2 + searchable_listview: ^2.11.2 + share_plus: ^8.0.3 + shared_preferences: ^2.2.3 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.1.0 - uuid: ^4.3.3 + uuid: ^4.4.0 veilid: # veilid: ^0.0.1 path: ../veilid/veilid-flutter @@ -89,7 +89,7 @@ dependencies: dev_dependencies: build_runner: ^2.4.9 - freezed: ^2.5.0 + freezed: ^2.5.2 icons_launcher: ^2.1.7 json_serializable: ^6.7.1 lint_hard: ^4.0.0 From 37b1717a710745277f3a87da66e5c64ab7319cb5 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 20 Apr 2024 21:24:03 -0400 Subject: [PATCH 087/270] fix persistent queue --- .../cubits/single_contact_messages_cubit.dart | 147 ++++++------- .../lib/src/persistent_queue.dart | 195 ++++++++++++++++++ .../lib/src/persistent_queue_cubit.dart | 194 ----------------- .../veilid_support/lib/veilid_support.dart | 2 +- 4 files changed, 259 insertions(+), 279 deletions(-) create mode 100644 packages/veilid_support/lib/src/persistent_queue.dart delete mode 100644 packages/veilid_support/lib/src/persistent_queue_cubit.dart diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 85eb9d6..72a6a90 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -63,14 +63,6 @@ class SingleContactMessagesCubit extends Cubit { _remoteConversationRecordKey = remoteConversationRecordKey, _remoteMessagesRecordKey = remoteMessagesRecordKey, _reconciledChatRecord = reconciledChatRecord, - _unreconciledMessagesQueue = PersistentQueueCubit( - table: 'SingleContactUnreconciledMessages', - key: remoteConversationRecordKey.toString(), - fromBuffer: proto.Message.fromBuffer), - _sendingMessagesQueue = PersistentQueueCubit( - table: 'SingleContactSendingMessages', - key: remoteConversationRecordKey.toString(), - fromBuffer: proto.Message.fromBuffer), super(const AsyncValue.loading()) { // Async Init _initWait.add(_init); @@ -93,6 +85,20 @@ class SingleContactMessagesCubit extends Cubit { // Initialize everything Future _init() async { + // Late initialization of queues with closures + _unreconciledMessagesQueue = PersistentQueue( + table: 'SingleContactUnreconciledMessages', + key: _remoteConversationRecordKey.toString(), + fromBuffer: proto.Message.fromBuffer, + closure: _processUnreconciledMessages, + ); + _sendingMessagesQueue = PersistentQueue( + table: 'SingleContactSendingMessages', + key: _remoteConversationRecordKey.toString(), + fromBuffer: proto.Message.fromBuffer, + closure: _processSendingMessages, + ); + // Make crypto await _initMessagesCrypto(); @@ -104,32 +110,6 @@ class SingleContactMessagesCubit extends Cubit { // Remote messages key await _initRcvdMessagesCubit(); - - // Unreconciled messages processing queue listener - Future.delayed(Duration.zero, () async { - await for (final entry in _unreconciledMessagesQueue.stream) { - final data = entry.asData; - if (data != null && data.value.isNotEmpty) { - // Process data using recoverable processing mechanism - await _unreconciledMessagesQueue.process((messages) async { - await _processUnreconciledMessages(data.value); - }); - } - } - }); - - // Sending messages processing queue listener - Future.delayed(Duration.zero, () async { - await for (final entry in _sendingMessagesQueue.stream) { - final data = entry.asData; - if (data != null && data.value.isNotEmpty) { - // Process data using recoverable processing mechanism - await _sendingMessagesQueue.process((messages) async { - await _processSendingMessages(data.value); - }); - } - } - }); } // Make crypto @@ -145,8 +125,8 @@ class SingleContactMessagesCubit extends Cubit { _sentMessagesCubit = DHTShortArrayCubit( open: () async => DHTShortArray.openWrite( _localMessagesRecordKey, writer, - debugName: - 'SingleContactMessagesCubit::_initSentMessagesCubit::SentMessages', + debugName: 'SingleContactMessagesCubit::_initSentMessagesCubit::' + 'SentMessages', parent: _localConversationRecordKey, crypto: _messagesCrypto), decodeElement: proto.Message.fromBuffer); @@ -176,7 +156,8 @@ class SingleContactMessagesCubit extends Cubit { _reconciledMessagesCubit = DHTShortArrayCubit( open: () async => DHTShortArray.openOwned(_reconciledChatRecord, - debugName: 'SingleContactMessagesCubit::_initReconciledMessages::' + debugName: + 'SingleContactMessagesCubit::_initReconciledMessagesCubit::' 'ReconciledMessages', parent: accountRecordKey), decodeElement: proto.Message.fromBuffer); @@ -185,34 +166,35 @@ class SingleContactMessagesCubit extends Cubit { _updateReconciledMessagesState(_reconciledMessagesCubit!.state); } - // Called when the remote messages list gets a change + //////////////////////////////////////////////////////////////////////////// + + // Called when the sent messages cubit gets a change + // This will re-render when messages are sent from another machine + void _updateSentMessagesState( + DHTShortArrayBusyState avmessages) { + final sentMessages = avmessages.state.asData?.value; + if (sentMessages == null) { + return; + } + // Don't reconcile, the sending machine will have already added + // to the reconciliation queue on that machine + + // Update the view + _renderState(); + } + + // Called when the received messages cubit gets a change void _updateRcvdMessagesState( DHTShortArrayBusyState avmessages) { - final remoteMessages = avmessages.state.asData?.value; - if (remoteMessages == null) { + final rcvdMessages = avmessages.state.asData?.value; + if (rcvdMessages == null) { return; } // Add remote messages updates to queue to process asynchronously // Ignore offline state because remote messages are always fully delivered // This may happen once per client but should be idempotent - _unreconciledMessagesQueue - .addAllSync(remoteMessages.map((x) => x.value).toIList()); - - // Update the view - _renderState(); - } - - // Called when the send messages list gets a change - // This will re-render when messages are sent from another machine - void _updateSentMessagesState( - DHTShortArrayBusyState avmessages) { - final remoteMessages = avmessages.state.asData?.value; - if (remoteMessages == null) { - return; - } - // Don't reconcile, the sending machine will have already added - // to the reconciliation queue on that machine + _unreconciledMessagesQueue.addAllSync(rcvdMessages.map((x) => x.value)); // Update the view _renderState(); @@ -227,6 +209,25 @@ class SingleContactMessagesCubit extends Cubit { _renderState(); } + // Async process to reconcile messages sent or received in the background + Future _processUnreconciledMessages( + IList messages) async { + await _reconciledMessagesCubit! + .operateWrite((reconciledMessagesWriter) async { + await _reconcileMessagesInner( + reconciledMessagesWriter: reconciledMessagesWriter, + messages: messages); + }); + } + + // Async process to send messages in the background + Future _processSendingMessages(IList messages) async { + for (final message in messages) { + await _sentMessagesCubit!.operateWriteEventual( + (writer) => writer.tryAddItem(message.writeToBuffer())); + } + } + Future _reconcileMessagesInner( {required DHTShortArrayWrite reconciledMessagesWriter, required IList messages}) async { @@ -288,25 +289,6 @@ class SingleContactMessagesCubit extends Cubit { } } - // Async process to reconcile messages sent or received in the background - Future _processUnreconciledMessages( - IList messages) async { - await _reconciledMessagesCubit! - .operateWrite((reconciledMessagesWriter) async { - await _reconcileMessagesInner( - reconciledMessagesWriter: reconciledMessagesWriter, - messages: messages); - }); - } - - // Async process to send messages in the background - Future _processSendingMessages(IList messages) async { - for (final message in messages) { - await _sentMessagesCubit!.operateWriteEventual( - (writer) => writer.tryAddItem(message.writeToBuffer())); - } - } - // Produce a state for this cubit from the input cubits and queues void _renderState() { // Get all reconciled messages @@ -315,15 +297,12 @@ class SingleContactMessagesCubit extends Cubit { // Get all sent messages final sentMessages = _sentMessagesCubit?.state.state.asData?.value; // Get all items in the unreconciled queue - final unreconciledMessages = _unreconciledMessagesQueue.state.asData?.value; + final unreconciledMessages = _unreconciledMessagesQueue.queue; // Get all items in the unsent queue - final sendingMessages = _sendingMessagesQueue.state.asData?.value; + final sendingMessages = _sendingMessagesQueue.queue; // If we aren't ready to render a state, say we're loading - if (reconciledMessages == null || - sentMessages == null || - unreconciledMessages == null || - sendingMessages == null) { + if (reconciledMessages == null || sentMessages == null) { emit(const AsyncLoading()); return; } @@ -428,8 +407,8 @@ class SingleContactMessagesCubit extends Cubit { DHTShortArrayCubit? _rcvdMessagesCubit; DHTShortArrayCubit? _reconciledMessagesCubit; - final PersistentQueueCubit _unreconciledMessagesQueue; - final PersistentQueueCubit _sendingMessagesQueue; + late final PersistentQueue _unreconciledMessagesQueue; + late final PersistentQueue _sendingMessagesQueue; StreamSubscription>? _sentSubscription; StreamSubscription>? _rcvdSubscription; diff --git a/packages/veilid_support/lib/src/persistent_queue.dart b/packages/veilid_support/lib/src/persistent_queue.dart new file mode 100644 index 0000000..fb76a10 --- /dev/null +++ b/packages/veilid_support/lib/src/persistent_queue.dart @@ -0,0 +1,195 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:async_tools/async_tools.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:mutex/mutex.dart'; +import 'package:protobuf/protobuf.dart'; + +import 'table_db.dart'; + +class PersistentQueue + /*extends Cubit>>*/ with + TableDBBackedFromBuffer> { + // + PersistentQueue( + {required String table, + required String key, + required T Function(Uint8List) fromBuffer, + required Future Function(IList) closure, + bool deleteOnClose = true}) + : _table = table, + _key = key, + _fromBuffer = fromBuffer, + _closure = closure, + _deleteOnClose = deleteOnClose { + _initWait.add(_init); + } + + Future close() async { + // Ensure the init finished + await _initWait(); + + // Close the sync add stream + await _syncAddController.close(); + + // Stop the processing trigger + await _queueReady.close(); + + // Wait for any setStates to finish + await _queueMutex.acquire(); + + // Clean up table if desired + if (_deleteOnClose) { + await delete(); + } + } + + Future _init() async { + // Start the processor + unawaited(Future.delayed(Duration.zero, () async { + await _initWait(); + await for (final _ in _queueReady.stream) { + await _process(); + } + })); + + // Start the sync add controller + unawaited(Future.delayed(Duration.zero, () async { + await _initWait(); + await for (final elem in _syncAddController.stream) { + await addAll(elem); + } + })); + + // Load the queue if we have one + await _queueMutex.protect(() async { + _queue = await load() ?? await store(IList.empty()); + }); + } + + Future _updateQueueInner(IList newQueue) async { + _queue = await store(newQueue); + if (_queue.isNotEmpty) { + _queueReady.sink.add(null); + } + } + + Future add(T item) async { + await _initWait(); + await _queueMutex.protect(() async { + final newQueue = _queue.add(item); + await _updateQueueInner(newQueue); + }); + } + + Future addAll(Iterable items) async { + await _initWait(); + await _queueMutex.protect(() async { + final newQueue = _queue.addAll(items); + await _updateQueueInner(newQueue); + }); + } + + void addSync(T item) { + _syncAddController.sink.add([item]); + } + + void addAllSync(Iterable items) { + _syncAddController.sink.add(items); + } + + // Future get isEmpty async { + // await _initWait(); + // return state.asData!.value.isEmpty; + // } + + // Future get isNotEmpty async { + // await _initWait(); + // return state.asData!.value.isNotEmpty; + // } + + // Future get length async { + // await _initWait(); + // return state.asData!.value.length; + // } + + // Future pop() async { + // await _initWait(); + // return _processingMutex.protect(() async => _stateMutex.protect(() async { + // final removedItem = Output(); + // final queue = state.asData!.value.removeAt(0, removedItem); + // await _setStateInner(queue); + // return removedItem.value; + // })); + // } + + // Future> popAll() async { + // await _initWait(); + // return _processingMutex.protect(() async => _stateMutex.protect(() async { + // final queue = state.asData!.value; + // await _setStateInner(IList.empty); + // return queue; + // })); + // } + + Future _process() async { + // Take a copy of the current queue + // (doesn't need queue mutex because this is a sync operation) + final toProcess = _queue; + final processCount = toProcess.length; + if (processCount == 0) { + return; + } + + // Run the processing closure + await _closure(toProcess); + + // If there was no exception, remove the processed items + await _queueMutex.protect(() async { + // Get the queue from the state again as items could + // have been added during processing + final newQueue = _queue.skip(processCount).toIList(); + await _updateQueueInner(newQueue); + }); + } + + IList get queue => _queue; + + // TableDBBacked + @override + String tableKeyName() => _key; + + @override + String tableName() => _table; + + @override + IList valueFromBuffer(Uint8List bytes) { + final reader = CodedBufferReader(bytes); + var out = IList(); + while (!reader.isAtEnd()) { + out = out.add(_fromBuffer(reader.readBytesAsView())); + } + return out; + } + + @override + Uint8List valueToBuffer(IList val) { + final writer = CodedBufferWriter(); + for (final elem in val) { + writer.writeRawBytes(elem.writeToBuffer()); + } + return writer.toBuffer(); + } + + final String _table; + final String _key; + final T Function(Uint8List) _fromBuffer; + final bool _deleteOnClose; + final WaitSet _initWait = WaitSet(); + final Mutex _queueMutex = Mutex(); + IList _queue = IList.empty(); + final StreamController> _syncAddController = StreamController(); + final StreamController _queueReady = StreamController(); + final Future Function(IList) _closure; +} diff --git a/packages/veilid_support/lib/src/persistent_queue_cubit.dart b/packages/veilid_support/lib/src/persistent_queue_cubit.dart deleted file mode 100644 index 6cc79c1..0000000 --- a/packages/veilid_support/lib/src/persistent_queue_cubit.dart +++ /dev/null @@ -1,194 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:async_tools/async_tools.dart'; -import 'package:bloc/bloc.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:mutex/mutex.dart'; -import 'package:protobuf/protobuf.dart'; - -import 'table_db.dart'; - -class PersistentQueueCubit - extends Cubit>> with TableDBBackedFromBuffer> { - // - PersistentQueueCubit( - {required String table, - required String key, - required T Function(Uint8List) fromBuffer, - bool deleteOnClose = true}) - : _table = table, - _key = key, - _fromBuffer = fromBuffer, - _deleteOnClose = deleteOnClose, - super(const AsyncValue.loading()) { - _initWait.add(_build); - unawaited(Future.delayed(Duration.zero, () async { - await for (final elem in _syncAddController.stream) { - await addAll(elem); - } - })); - } - - @override - Future close() async { - // Ensure the init finished - await _initWait(); - - // Close the sync add stream - await _syncAddController.close(); - - // Wait for any setStates to finish - await _stateMutex.acquire(); - - // Clean up table if desired - if (_deleteOnClose) { - await delete(); - } - - await super.close(); - } - - Future _build() async { - await _stateMutex.protect(() async { - try { - emit(AsyncValue.data(await load() ?? await store(IList.empty()))); - } on Exception catch (e, stackTrace) { - emit(AsyncValue.error(e, stackTrace)); - } - }); - } - - Future _setStateInner(IList newState) async { - emit(AsyncValue.data(await store(newState))); - } - - Future add(T item) async { - await _initWait(); - await _stateMutex.protect(() async { - final queue = state.asData!.value.add(item); - await _setStateInner(queue); - }); - } - - Future addAll(IList items) async { - await _initWait(); - await _stateMutex.protect(() async { - var queue = state.asData!.value; - for (final item in items) { - queue = queue.add(item); - } - await _setStateInner(queue); - }); - } - - void addSync(T item) { - _syncAddController.sink.add([item].toIList()); - } - - void addAllSync(IList items) { - _syncAddController.sink.add(items.toIList()); - } - - Future get isEmpty async { - await _initWait(); - return state.asData!.value.isEmpty; - } - - Future get isNotEmpty async { - await _initWait(); - return state.asData!.value.isNotEmpty; - } - - Future get length async { - await _initWait(); - return state.asData!.value.length; - } - - // Future pop() async { - // await _initWait(); - // return _processingMutex.protect(() async => _stateMutex.protect(() async { - // final removedItem = Output(); - // final queue = state.asData!.value.removeAt(0, removedItem); - // await _setStateInner(queue); - // return removedItem.value; - // })); - // } - - // Future> popAll() async { - // await _initWait(); - // return _processingMutex.protect(() async => _stateMutex.protect(() async { - // final queue = state.asData!.value; - // await _setStateInner(IList.empty); - // return queue; - // })); - // } - - Future process(Future Function(IList) closure, - {int? count}) async { - await _initWait(); - // Only one processor at a time - return _processingMutex.protect(() async { - // Take 'count' items from the front of the list - final toProcess = await _stateMutex.protect(() async { - final queue = state.asData!.value; - final processCount = (count ?? queue.length).clamp(0, queue.length); - return queue.take(processCount).toIList(); - }); - - // Run the processing closure - final processCount = toProcess.length; - final out = await closure(toProcess); - - // If there was nothing to process just return - if (toProcess.isEmpty) { - return out; - } - - // If there was no exception, remove the processed items - return _stateMutex.protect(() async { - // Get the queue from the state again as items could - // have been added during processing - final queue = state.asData!.value; - final newQueue = queue.skip(processCount).toIList(); - await _setStateInner(newQueue); - return out; - }); - }); - } - - // TableDBBacked - @override - String tableKeyName() => _key; - - @override - String tableName() => _table; - - @override - IList valueFromBuffer(Uint8List bytes) { - final reader = CodedBufferReader(bytes); - var out = IList(); - while (!reader.isAtEnd()) { - out = out.add(_fromBuffer(reader.readBytesAsView())); - } - return out; - } - - @override - Uint8List valueToBuffer(IList val) { - final writer = CodedBufferWriter(); - for (final elem in val) { - writer.writeRawBytes(elem.writeToBuffer()); - } - return writer.toBuffer(); - } - - final String _table; - final String _key; - final T Function(Uint8List) _fromBuffer; - final bool _deleteOnClose; - final WaitSet _initWait = WaitSet(); - final Mutex _stateMutex = Mutex(); - final Mutex _processingMutex = Mutex(); - final StreamController> _syncAddController = StreamController(); -} diff --git a/packages/veilid_support/lib/veilid_support.dart b/packages/veilid_support/lib/veilid_support.dart index 3cad8e0..fcbbaf4 100644 --- a/packages/veilid_support/lib/veilid_support.dart +++ b/packages/veilid_support/lib/veilid_support.dart @@ -10,7 +10,7 @@ export 'src/config.dart'; export 'src/identity.dart'; export 'src/json_tools.dart'; export 'src/memory_tools.dart'; -export 'src/persistent_queue_cubit.dart'; +export 'src/persistent_queue.dart'; export 'src/protobuf_tools.dart'; export 'src/table_db.dart'; export 'src/veilid_log.dart' hide veilidLoggy; From 64d0019e6e50fe20324336e77a323a647aaf8db7 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 21 Apr 2024 22:16:22 -0400 Subject: [PATCH 088/270] watchvalue fixes --- assets/i18n/en.json | 1 + .../cubits/single_contact_messages_cubit.dart | 10 ++++------ .../views/invitation_dialog.dart | 9 +++++++-- .../dht_support/src/dht_record/dht_record.dart | 16 ++++++++-------- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 2a1e4ac..f074031 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -96,6 +96,7 @@ "failed_to_accept": "Failed to accept contact invitation", "failed_to_reject": "Failed to reject contact invitation", "invalid_invitation": "Invalid invitation", + "try_again_online": "Invitation could not be reached, try again when online", "protected_with_pin": "Contact invitation is protected with a PIN", "protected_with_password": "Contact invitation is protected with a password", "invalid_pin": "Invalid PIN", diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 72a6a90..41c872d 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -23,13 +23,11 @@ class RenderStateElement { if (!isLocal) { return null; } - if (reconciled && sent) { - if (!reconciledOffline && !sentOffline) { - return MessageSendState.delivered; - } - return MessageSendState.sent; - } + if (sent && !sentOffline) { + return MessageSendState.delivered; + } + if (reconciled && !reconciledOffline) { return MessageSendState.sent; } return MessageSendState.sending; diff --git a/lib/contact_invitation/views/invitation_dialog.dart b/lib/contact_invitation/views/invitation_dialog.dart index 60d8784..2f1bd1c 100644 --- a/lib/contact_invitation/views/invitation_dialog.dart +++ b/lib/contact_invitation/views/invitation_dialog.dart @@ -224,8 +224,13 @@ class InvitationDialogState extends State { _validInvitation = null; widget.onValidationFailed(); }); - } on VeilidAPIException { - final errorText = translate('invitation_dialog.invalid_invitation'); + } on VeilidAPIException catch (e) { + late final String errorText; + if (e is VeilidAPIExceptionTryAgain) { + errorText = translate('invitation_dialog.try_again_online'); + } else { + errorText = translate('invitation_dialog.invalid_invitation'); + } if (mounted) { showErrorToast(context, errorText); } 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 af16842..f7df9a9 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 @@ -390,14 +390,14 @@ class DHTRecord { // 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 watched subkeys - watchController?.add(DHTRecordWatchChange( - local: local, data: updatedData, subkeys: overlappedSubkeys)); + if (overlappedFirstSubkey != null && updateFirstSubkey != null) { + final updatedData = + overlappedFirstSubkey == updateFirstSubkey ? data : null; + + // Report only watched subkeys + watchController?.add(DHTRecordWatchChange( + local: local, data: updatedData, subkeys: overlappedSubkeys)); + } } } } From 0b4de3ad13572d967e4fe879585f5904d315eeeb Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 24 Apr 2024 22:43:42 -0400 Subject: [PATCH 089/270] better handling of subkey sequence numbers --- .../cubits/single_contact_messages_cubit.dart | 1 + lib/chat/views/chat_component.dart | 8 ++ lib/tools/loggy.dart | 5 +- lib/tools/state_logger.dart | 1 + .../src/dht_record/dht_record.dart | 90 ++++++++++++++----- .../src/dht_record/dht_record_pool.dart | 83 +++++++++++++---- .../dht_short_array/dht_short_array_head.dart | 9 +- .../dht_short_array/dht_short_array_read.dart | 11 ++- .../dht_short_array_write.dart | 34 +++++-- 9 files changed, 185 insertions(+), 57 deletions(-) diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 41c872d..6605ab6 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -379,6 +379,7 @@ class SingleContactMessagesCubit extends Cubit { .toIList(); // Emit the rendered state + emit(AsyncValue.data(renderedState)); } diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index d0d3f39..8ca471e 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -158,9 +158,17 @@ class ChatComponent extends StatelessWidget { // Convert protobuf messages to chat messages final chatMessages = []; + final tsSet = {}; for (final message in messages) { final chatMessage = messageToChatMessage(message); chatMessages.insert(0, chatMessage); + if (!tsSet.add(chatMessage.id)) { + // ignore: avoid_print + print('duplicate id found: ${chatMessage.id}:\n' + 'Messages:\n$messages\n' + 'ChatMessages:\n$chatMessages'); + assert(false, 'should not have duplicate id'); + } } return DefaultTextStyle( diff --git a/lib/tools/loggy.dart b/lib/tools/loggy.dart index 9947308..c422ec8 100644 --- a/lib/tools/loggy.dart +++ b/lib/tools/loggy.dart @@ -9,6 +9,7 @@ import 'package:loggy/loggy.dart'; import 'package:veilid_support/veilid_support.dart'; import '../veilid_processor/views/developer.dart'; +import 'responsive.dart'; import 'state_logger.dart'; String wrapWithLogColor(LogLevel? level, String text) { @@ -111,7 +112,9 @@ class CallbackPrinter extends LoggyPrinter { @override void onLog(LogRecord record) { final out = record.pretty(); - debugPrint(out); + if (isDesktop) { + debugPrintSynchronously(out); + } globalDebugTerminal.write('$out\n'.replaceAll('\n', '\r\n')); callback?.call(record); } diff --git a/lib/tools/state_logger.dart b/lib/tools/state_logger.dart index b17727f..4c8e17a 100644 --- a/lib/tools/state_logger.dart +++ b/lib/tools/state_logger.dart @@ -8,6 +8,7 @@ const Map _blocChangeLogLevels = { 'ActiveConversationsBlocMapCubit': LogLevel.off, 'DHTShortArrayCubit': LogLevel.off, 'PersistentQueueCubit': LogLevel.off, + 'SingleContactMessagesCubit': LogLevel.off, }; const Map _blocCreateCloseLogLevels = {}; const Map _blocErrorLogLevels = {}; 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 f7df9a9..7d29292 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 @@ -104,7 +104,8 @@ class DHTRecord { {int subkey = -1, DHTRecordCrypto? crypto, bool forceRefresh = false, - bool onlyUpdates = false}) async { + bool onlyUpdates = false, + Output? outSeqNum}) async { subkey = subkeyOrDefault(subkey); final valueData = await _routingContext.getDHTValue(key, subkey, forceRefresh: forceRefresh); @@ -116,6 +117,9 @@ class DHTRecord { return null; } final out = (crypto ?? _crypto).decrypt(valueData.data, subkey); + if (outSeqNum != null) { + outSeqNum.save(valueData.seq); + } _sharedDHTRecordData.subkeySeqCache[subkey] = valueData.seq; return out; } @@ -124,12 +128,14 @@ class DHTRecord { {int subkey = -1, DHTRecordCrypto? crypto, bool forceRefresh = false, - bool onlyUpdates = false}) async { + bool onlyUpdates = false, + Output? outSeqNum}) async { final data = await get( subkey: subkey, crypto: crypto, forceRefresh: forceRefresh, - onlyUpdates: onlyUpdates); + onlyUpdates: onlyUpdates, + outSeqNum: outSeqNum); if (data == null) { return null; } @@ -141,12 +147,14 @@ class DHTRecord { {int subkey = -1, DHTRecordCrypto? crypto, bool forceRefresh = false, - bool onlyUpdates = false}) async { + bool onlyUpdates = false, + Output? outSeqNum}) async { final data = await get( subkey: subkey, crypto: crypto, forceRefresh: forceRefresh, - onlyUpdates: onlyUpdates); + onlyUpdates: onlyUpdates, + outSeqNum: outSeqNum); if (data == null) { return null; } @@ -154,7 +162,10 @@ class DHTRecord { } Future tryWriteBytes(Uint8List newValue, - {int subkey = -1, DHTRecordCrypto? crypto, KeyPair? writer}) async { + {int subkey = -1, + DHTRecordCrypto? crypto, + KeyPair? writer, + Output? outSeqNum}) async { subkey = subkeyOrDefault(subkey); final lastSeq = _sharedDHTRecordData.subkeySeqCache[subkey]; final encryptedNewValue = @@ -175,6 +186,9 @@ class DHTRecord { // Record new sequence number final isUpdated = newValueData.seq != lastSeq; + if (isUpdated && outSeqNum != null) { + outSeqNum.save(newValueData.seq); + } _sharedDHTRecordData.subkeySeqCache[subkey] = newValueData.seq; // See if the encrypted data returned is exactly the same @@ -197,7 +211,10 @@ class DHTRecord { } Future eventualWriteBytes(Uint8List newValue, - {int subkey = -1, DHTRecordCrypto? crypto, KeyPair? writer}) async { + {int subkey = -1, + DHTRecordCrypto? crypto, + KeyPair? writer, + Output? outSeqNum}) async { subkey = subkeyOrDefault(subkey); final lastSeq = _sharedDHTRecordData.subkeySeqCache[subkey]; final encryptedNewValue = @@ -222,6 +239,9 @@ class DHTRecord { } // Record new sequence number + if (outSeqNum != null) { + outSeqNum.save(newValueData.seq); + } _sharedDHTRecordData.subkeySeqCache[subkey] = newValueData.seq; // The encrypted data returned should be exactly the same @@ -239,12 +259,14 @@ class DHTRecord { Future Function(Uint8List? oldValue) update, {int subkey = -1, DHTRecordCrypto? crypto, - KeyPair? writer}) async { + KeyPair? writer, + Output? outSeqNum}) async { subkey = subkeyOrDefault(subkey); // 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, crypto: crypto); + var oldValue = + await get(subkey: subkey, crypto: crypto, outSeqNum: outSeqNum); do { // Update the data @@ -252,16 +274,22 @@ class DHTRecord { // Try to write it back to the network oldValue = await tryWriteBytes(updatedValue, - subkey: subkey, crypto: crypto, writer: writer); + subkey: subkey, crypto: crypto, writer: writer, outSeqNum: outSeqNum); // Repeat update if newer data on the network was found } while (oldValue != null); } Future tryWriteJson(T Function(dynamic) fromJson, T newValue, - {int subkey = -1, DHTRecordCrypto? crypto, KeyPair? writer}) => + {int subkey = -1, + DHTRecordCrypto? crypto, + KeyPair? writer, + Output? outSeqNum}) => tryWriteBytes(jsonEncodeBytes(newValue), - subkey: subkey, crypto: crypto, writer: writer) + subkey: subkey, + crypto: crypto, + writer: writer, + outSeqNum: outSeqNum) .then((out) { if (out == null) { return null; @@ -271,9 +299,15 @@ class DHTRecord { Future tryWriteProtobuf( T Function(List) fromBuffer, T newValue, - {int subkey = -1, DHTRecordCrypto? crypto, KeyPair? writer}) => + {int subkey = -1, + DHTRecordCrypto? crypto, + KeyPair? writer, + Output? outSeqNum}) => tryWriteBytes(newValue.writeToBuffer(), - subkey: subkey, crypto: crypto, writer: writer) + subkey: subkey, + crypto: crypto, + writer: writer, + outSeqNum: outSeqNum) .then((out) { if (out == null) { return null; @@ -282,26 +316,38 @@ class DHTRecord { }); Future eventualWriteJson(T newValue, - {int subkey = -1, DHTRecordCrypto? crypto, KeyPair? writer}) => + {int subkey = -1, + DHTRecordCrypto? crypto, + KeyPair? writer, + Output? outSeqNum}) => eventualWriteBytes(jsonEncodeBytes(newValue), - subkey: subkey, crypto: crypto, writer: writer); + subkey: subkey, crypto: crypto, writer: writer, outSeqNum: outSeqNum); Future eventualWriteProtobuf(T newValue, - {int subkey = -1, DHTRecordCrypto? crypto, KeyPair? writer}) => + {int subkey = -1, + DHTRecordCrypto? crypto, + KeyPair? writer, + Output? outSeqNum}) => eventualWriteBytes(newValue.writeToBuffer(), - subkey: subkey, crypto: crypto, writer: writer); + subkey: subkey, crypto: crypto, writer: writer, outSeqNum: outSeqNum); Future eventualUpdateJson( T Function(dynamic) fromJson, Future Function(T?) update, - {int subkey = -1, DHTRecordCrypto? crypto, KeyPair? writer}) => + {int subkey = -1, + DHTRecordCrypto? crypto, + KeyPair? writer, + Output? outSeqNum}) => eventualUpdateBytes(jsonUpdate(fromJson, update), - subkey: subkey, crypto: crypto, writer: writer); + subkey: subkey, crypto: crypto, writer: writer, outSeqNum: outSeqNum); Future eventualUpdateProtobuf( T Function(List) fromBuffer, Future Function(T?) update, - {int subkey = -1, DHTRecordCrypto? crypto, KeyPair? writer}) => + {int subkey = -1, + DHTRecordCrypto? crypto, + KeyPair? writer, + Output? outSeqNum}) => eventualUpdateBytes(protobufUpdate(fromBuffer, update), - subkey: subkey, crypto: crypto, writer: writer); + subkey: subkey, crypto: crypto, writer: writer, outSeqNum: outSeqNum); Future watch( {List? subkeys, 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 333558e..0be4f25 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 @@ -10,6 +10,9 @@ import 'package:protobuf/protobuf.dart'; import '../../../../veilid_support.dart'; +export 'package:fast_immutable_collections/fast_immutable_collections.dart' + show Output; + part 'dht_record_pool.freezed.dart'; part 'dht_record_pool.g.dart'; part 'dht_record.dart'; @@ -17,6 +20,10 @@ part 'dht_record.dart'; const int watchBackoffMultiplier = 2; const int watchBackoffMax = 30; +const int? defaultWatchDurationSecs = null; // 600 +const int watchRenewalNumerator = 4; +const int watchRenewalDenominator = 5; + typedef DHTRecordPoolLogger = void Function(String message); /// Record pool that managed DHTRecords and allows for tagged deletion @@ -56,14 +63,17 @@ class WatchState extends Equatable { {required this.subkeys, required this.expiration, required this.count, - this.realExpiration}); + this.realExpiration, + this.renewalTime}); final List? subkeys; final Timestamp? expiration; final int? count; final Timestamp? realExpiration; + final Timestamp? renewalTime; @override - List get props => [subkeys, expiration, count, realExpiration]; + List get props => + [subkeys, expiration, count, realExpiration, renewalTime]; } /// Data shared amongst all DHTRecord instances @@ -77,6 +87,7 @@ class SharedDHTRecordData { VeilidRoutingContext defaultRoutingContext; Map subkeySeqCache = {}; bool needsWatchStateUpdate = false; + WatchState? unionWatchState; bool deleteOnClose = false; } @@ -616,6 +627,7 @@ class DHTRecordPool with TableDBBackedJson { int? totalCount; Timestamp? maxExpiration; List? allSubkeys; + Timestamp? earliestRenewalTime; var noExpiration = false; var everySubkey = false; @@ -648,6 +660,15 @@ class DHTRecordPool with TableDBBackedJson { } else { everySubkey = true; } + final wsRenewalTime = ws.renewalTime; + if (wsRenewalTime != null) { + earliestRenewalTime = earliestRenewalTime == null + ? wsRenewalTime + : Timestamp( + value: (wsRenewalTime.value < earliestRenewalTime.value + ? wsRenewalTime.value + : earliestRenewalTime.value)); + } } } if (noExpiration) { @@ -661,11 +682,14 @@ class DHTRecordPool with TableDBBackedJson { } return WatchState( - subkeys: allSubkeys, expiration: maxExpiration, count: totalCount); + subkeys: allSubkeys, + expiration: maxExpiration, + count: totalCount, + renewalTime: earliestRenewalTime); } - void _updateWatchRealExpirations( - Iterable records, Timestamp realExpiration) { + void _updateWatchRealExpirations(Iterable records, + Timestamp realExpiration, Timestamp renewalTime) { for (final rec in records) { final ws = rec.watchState; if (ws != null) { @@ -673,7 +697,8 @@ class DHTRecordPool with TableDBBackedJson { subkeys: ws.subkeys, expiration: ws.expiration, count: ws.count, - realExpiration: realExpiration); + realExpiration: realExpiration, + renewalTime: renewalTime); } } } @@ -689,6 +714,7 @@ class DHTRecordPool with TableDBBackedJson { } _inTick = true; _tickCount = 0; + final now = veilid.now(); try { final allSuccess = await _mutex.protect(() async { @@ -700,12 +726,24 @@ class DHTRecordPool with TableDBBackedJson { final openedRecordInfo = kv.value; final dhtctx = openedRecordInfo.shared.defaultRoutingContext; - if (openedRecordInfo.shared.needsWatchStateUpdate) { - final watchState = + var wantsWatchStateUpdate = + openedRecordInfo.shared.needsWatchStateUpdate; + + // Check if we have reached renewal time for the watch + if (openedRecordInfo.shared.unionWatchState != null && + openedRecordInfo.shared.unionWatchState!.renewalTime != null && + now.value > + openedRecordInfo.shared.unionWatchState!.renewalTime!.value) { + wantsWatchStateUpdate = true; + } + + if (wantsWatchStateUpdate) { + // Update union watch state + final unionWatchState = openedRecordInfo.shared.unionWatchState = _collectUnionWatchState(openedRecordInfo.records); // Apply watch changes for record - if (watchState == null) { + if (unionWatchState == null) { unord.add(() async { // Record needs watch cancel var success = false; @@ -727,26 +765,39 @@ class DHTRecordPool with TableDBBackedJson { // Record needs new watch var success = false; try { - final subkeys = watchState.subkeys?.toList(); - final count = watchState.count; - final expiration = watchState.expiration; + final subkeys = unionWatchState.subkeys?.toList(); + final count = unionWatchState.count; + final expiration = unionWatchState.expiration; + final now = veilid.now(); final realExpiration = await dhtctx.watchDHTValues( openedRecordKey, - subkeys: watchState.subkeys?.toList(), - count: watchState.count, - expiration: watchState.expiration); + subkeys: unionWatchState.subkeys?.toList(), + count: unionWatchState.count, + expiration: unionWatchState.expiration ?? + (defaultWatchDurationSecs == null + ? null + : veilid.now().offset( + TimestampDuration.fromMillis( + defaultWatchDurationSecs! * 1000)))); + + final expirationDuration = realExpiration.diff(now); + final renewalTime = now.offset(TimestampDuration( + value: expirationDuration.value * + BigInt.from(watchRenewalNumerator) ~/ + BigInt.from(watchRenewalDenominator))); log('watchDHTValues: key=$openedRecordKey, subkeys=$subkeys, ' 'count=$count, expiration=$expiration, ' 'realExpiration=$realExpiration, ' + 'renewalTime=$renewalTime, ' 'debugNames=${openedRecordInfo.debugNames}'); // Update watch states with real expiration if (realExpiration.value != BigInt.zero) { openedRecordInfo.shared.needsWatchStateUpdate = false; _updateWatchRealExpirations( - openedRecordInfo.records, realExpiration); + openedRecordInfo.records, realExpiration, renewalTime); success = true; } } on VeilidAPIException catch (e) { 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 2fd1a60..f6f3a5a 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 @@ -424,21 +424,18 @@ class _DHTShortArrayHead { /// 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 updatePositionSeq(int pos, bool write) async { + void updatePositionSeq(int pos, bool write, int newSeq) { final idx = _index[pos]; - final lookup = await lookupIndex(idx); - final report = await lookup.record - .inspect(subkeys: [ValueSubkeyRange.single(lookup.recordSubkey)]); while (_localSeqs.length <= idx) { _localSeqs.add(0xFFFFFFFF); } - _localSeqs[idx] = report.localSeqs[0]; + _localSeqs[idx] = newSeq; if (write) { while (_seqs.length <= idx) { _seqs.add(0xFFFFFFFF); } - _seqs[idx] = report.localSeqs[0]; + _seqs[idx] = newSeq; } } 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 index 44e565d..0dbe51e 100644 --- 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 @@ -74,9 +74,14 @@ class _DHTShortArrayRead implements DHTShortArrayRead { final lookup = await _head.lookupPosition(pos); final refresh = forceRefresh || _head.positionNeedsRefresh(pos); - final out = - lookup.record.get(subkey: lookup.recordSubkey, forceRefresh: refresh); - await _head.updatePositionSeq(pos, false); + final outSeqNum = Output(); + final out = lookup.record.get( + subkey: lookup.recordSubkey, + forceRefresh: refresh, + outSeqNum: outSeqNum); + if (outSeqNum.value != null) { + _head.updatePositionSeq(pos, false, outSeqNum.value!); + } return out; } 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 index af6204e..d1c8b2f 100644 --- 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 @@ -110,9 +110,6 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead return false; } - // Get sequence number written - await _head.updatePositionSeq(pos, true); - return true; } @@ -127,9 +124,6 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead return false; } - // Get sequence number written - await _head.updatePositionSeq(pos, true); - return true; } @@ -153,9 +147,17 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead throw IndexError.withLength(pos, _head.length); } final lookup = await _head.lookupPosition(pos); + + final outSeqNum = Output(); + final result = lookup.seq == 0xFFFFFFFF ? null : await lookup.record.get(subkey: lookup.recordSubkey); + + if (outSeqNum.value != null) { + _head.updatePositionSeq(pos, false, outSeqNum.value!); + } + if (result == null) { throw StateError('Element does not exist'); } @@ -175,11 +177,25 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead throw IndexError.withLength(pos, _head.length); } final lookup = await _head.lookupPosition(pos); + + final outSeqNum = Output(); + final oldValue = lookup.seq == 0xFFFFFFFF ? null - : await lookup.record.get(subkey: lookup.recordSubkey); - final result = await lookup.record - .tryWriteBytes(newValue, subkey: lookup.recordSubkey); + : await lookup.record + .get(subkey: lookup.recordSubkey, outSeqNum: outSeqNum); + + if (outSeqNum.value != null) { + _head.updatePositionSeq(pos, false, outSeqNum.value!); + } + + final result = await lookup.record.tryWriteBytes(newValue, + subkey: lookup.recordSubkey, outSeqNum: outSeqNum); + + if (outSeqNum.value != null) { + _head.updatePositionSeq(pos, true, outSeqNum.value!); + } + if (result != null) { // A result coming back means the element was overwritten already return (result, false); From 7e9254faac049fe351f0a6b1e6f585b27b1faf53 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 28 Apr 2024 21:18:30 -0400 Subject: [PATCH 090/270] parallelize getAllItems --- .../dht_support/src/dht_record/dht_record_pool.dart | 3 +++ .../src/dht_short_array/dht_short_array.dart | 1 + .../src/dht_short_array/dht_short_array_read.dart | 13 +++++++++---- packages/veilid_support/pubspec.lock | 2 +- packages/veilid_support/pubspec.yaml | 1 + 5 files changed, 15 insertions(+), 5 deletions(-) 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 0be4f25..608131c 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 @@ -24,6 +24,9 @@ const int? defaultWatchDurationSecs = null; // 600 const int watchRenewalNumerator = 4; const int watchRenewalDenominator = 5; +// Maximum number of concurrent DHT operations to perform on the network +const int maxDHTConcurrency = 8; + typedef DHTRecordPoolLogger = void Function(String message); /// Record pool that managed DHTRecords and allows for tagged deletion 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 5a4210f..de17b62 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:collection/collection.dart'; import 'package:mutex/mutex.dart'; import 'package:protobuf/protobuf.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 index 0dbe51e..342e67a 100644 --- 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 @@ -93,12 +93,17 @@ class _DHTShortArrayRead implements DHTShortArrayRead { 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) { + final chunks = Iterable.generate(_head.length) + .slices(maxDHTConcurrency) + .map((chunk) => + chunk.map((pos) => getItem(pos, forceRefresh: forceRefresh))); + + for (final chunk in chunks) { + final elems = await chunk.wait; + if (elems.contains(null)) { return null; } - out.add(elem); + out.addAll(elems.cast()); } return out; diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index 00f3cdd..9dcb93e 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -168,7 +168,7 @@ packages: source: hosted version: "4.10.0" collection: - dependency: transitive + dependency: "direct main" description: name: collection sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index d7344ef..0d2d439 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: bloc: ^8.1.3 bloc_tools: path: ../bloc_tools + collection: ^1.18.0 equatable: ^2.0.5 fast_immutable_collections: ^10.1.1 freezed_annotation: ^2.4.1 From 87735dfb8ea8f314edf16f9ba78e8fd20fc8c2c9 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 29 Apr 2024 13:41:19 -0400 Subject: [PATCH 091/270] start of integration test --- android/app/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- dev-setup/_script_common | 6 +- dev-setup/flutter_config.sh | 18 +- dev-setup/wasm_update.sh | 2 +- .../veilid_support/debug_integration_tests.sh | 4 + packages/veilid_support/example/.gitignore | 43 + packages/veilid_support/example/.metadata | 45 + packages/veilid_support/example/README.md | 16 + .../example/analysis_options.yaml | 28 + .../veilid_support/example/android/.gitignore | 13 + .../example/android/app/build.gradle | 69 ++ .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 44 + .../com/example/example/MainActivity.kt | 5 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/styles.xml | 18 + .../app/src/profile/AndroidManifest.xml | 7 + .../example/android/build.gradle | 18 + .../example/android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 5 + .../example/android/settings.gradle | 26 + .../example/dev-setup/_script_common | 16 + .../example/dev-setup/flutter_config.sh | 35 + .../example/dev-setup/wasm_update.sh | 25 + .../example/integration_test/app_test.dart | 39 + .../example/integration_test/fixtures.dart | 152 ++++ .../test_dht_record_pool.dart | 9 + .../test_dht_short_array.dart | 9 + .../veilid_support/example/ios/.gitignore | 34 + .../ios/Flutter/AppFrameworkInfo.plist | 26 + .../example/ios/Flutter/Debug.xcconfig | 2 + .../example/ios/Flutter/Release.xcconfig | 2 + packages/veilid_support/example/ios/Podfile | 44 + .../ios/Runner.xcodeproj/project.pbxproj | 619 ++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 98 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../example/ios/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 122 +++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 295 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 450 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 282 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 462 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 704 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 586 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 1674 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 762 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 1226 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 1418 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + .../ios/Runner/Base.lproj/Main.storyboard | 26 + .../example/ios/Runner/Info.plist | 49 ++ .../ios/Runner/Runner-Bridging-Header.h | 1 + .../example/ios/RunnerTests/RunnerTests.swift | 12 + packages/veilid_support/example/lib/main.dart | 125 +++ .../veilid_support/example/linux/.gitignore | 1 + .../example/linux/CMakeLists.txt | 145 ++++ .../example/linux/flutter/CMakeLists.txt | 88 ++ .../flutter/generated_plugin_registrant.cc | 15 + .../flutter/generated_plugin_registrant.h | 15 + .../linux/flutter/generated_plugins.cmake | 24 + packages/veilid_support/example/linux/main.cc | 6 + .../example/linux/my_application.cc | 124 +++ .../example/linux/my_application.h | 18 + .../veilid_support/example/macos/.gitignore | 7 + .../macos/Flutter/Flutter-Debug.xcconfig | 2 + .../macos/Flutter/Flutter-Release.xcconfig | 2 + .../Flutter/GeneratedPluginRegistrant.swift | 14 + packages/veilid_support/example/macos/Podfile | 43 + .../veilid_support/example/macos/Podfile.lock | 29 + .../macos/Runner.xcodeproj/project.pbxproj | 801 ++++++++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 98 +++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../example/macos/Runner/AppDelegate.swift | 9 + .../AppIcon.appiconset/Contents.json | 68 ++ .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 102994 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 5680 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 520 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 14142 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1066 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 36406 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 2218 bytes .../macos/Runner/Base.lproj/MainMenu.xib | 343 ++++++++ .../macos/Runner/Configs/AppInfo.xcconfig | 14 + .../macos/Runner/Configs/Debug.xcconfig | 2 + .../macos/Runner/Configs/Release.xcconfig | 2 + .../macos/Runner/Configs/Warnings.xcconfig | 13 + .../macos/Runner/DebugProfile.entitlements | 12 + .../example/macos/Runner/Info.plist | 32 + .../macos/Runner/MainFlutterWindow.swift | 15 + .../example/macos/Runner/Release.entitlements | 8 + .../macos/RunnerTests/RunnerTests.swift | 12 + packages/veilid_support/example/pubspec.lock | 492 +++++++++++ packages/veilid_support/example/pubspec.yaml | 90 ++ .../example/test/widget_test.dart | 30 + .../veilid_support/example/web/favicon.png | Bin 0 -> 917 bytes .../example/web/icons/Icon-192.png | Bin 0 -> 5292 bytes .../example/web/icons/Icon-512.png | Bin 0 -> 8252 bytes .../example/web/icons/Icon-maskable-192.png | Bin 0 -> 5594 bytes .../example/web/icons/Icon-maskable-512.png | Bin 0 -> 20998 bytes .../veilid_support/example/web/index.html | 59 ++ .../veilid_support/example/web/manifest.json | 35 + .../veilid_support/example/windows/.gitignore | 17 + .../example/windows/CMakeLists.txt | 108 +++ .../example/windows/flutter/CMakeLists.txt | 109 +++ .../flutter/generated_plugin_registrant.cc | 14 + .../flutter/generated_plugin_registrant.h | 15 + .../windows/flutter/generated_plugins.cmake | 24 + .../example/windows/runner/CMakeLists.txt | 40 + .../example/windows/runner/Runner.rc | 121 +++ .../example/windows/runner/flutter_window.cpp | 71 ++ .../example/windows/runner/flutter_window.h | 33 + .../example/windows/runner/main.cpp | 43 + .../example/windows/runner/resource.h | 16 + .../windows/runner/resources/app_icon.ico | Bin 0 -> 33772 bytes .../windows/runner/runner.exe.manifest | 20 + .../example/windows/runner/utils.cpp | 65 ++ .../example/windows/runner/utils.h | 19 + .../example/windows/runner/win32_window.cpp | 288 +++++++ .../example/windows/runner/win32_window.h | 102 +++ .../veilid_support/run_integration_tests.sh | 4 + .../run_integration_tests_web.sh | 5 + packages/veilid_support/run_unit_tests.sh | 2 + 147 files changed, 5787 insertions(+), 15 deletions(-) create mode 100755 packages/veilid_support/debug_integration_tests.sh create mode 100644 packages/veilid_support/example/.gitignore create mode 100644 packages/veilid_support/example/.metadata create mode 100644 packages/veilid_support/example/README.md create mode 100644 packages/veilid_support/example/analysis_options.yaml create mode 100644 packages/veilid_support/example/android/.gitignore create mode 100644 packages/veilid_support/example/android/app/build.gradle create mode 100644 packages/veilid_support/example/android/app/src/debug/AndroidManifest.xml create mode 100644 packages/veilid_support/example/android/app/src/main/AndroidManifest.xml create mode 100644 packages/veilid_support/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt create mode 100644 packages/veilid_support/example/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 packages/veilid_support/example/android/app/src/main/res/drawable/launch_background.xml create mode 100644 packages/veilid_support/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 packages/veilid_support/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 packages/veilid_support/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 packages/veilid_support/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 packages/veilid_support/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 packages/veilid_support/example/android/app/src/main/res/values-night/styles.xml create mode 100644 packages/veilid_support/example/android/app/src/main/res/values/styles.xml create mode 100644 packages/veilid_support/example/android/app/src/profile/AndroidManifest.xml create mode 100644 packages/veilid_support/example/android/build.gradle create mode 100644 packages/veilid_support/example/android/gradle.properties create mode 100644 packages/veilid_support/example/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 packages/veilid_support/example/android/settings.gradle create mode 100644 packages/veilid_support/example/dev-setup/_script_common create mode 100755 packages/veilid_support/example/dev-setup/flutter_config.sh create mode 100755 packages/veilid_support/example/dev-setup/wasm_update.sh create mode 100644 packages/veilid_support/example/integration_test/app_test.dart create mode 100644 packages/veilid_support/example/integration_test/fixtures.dart create mode 100644 packages/veilid_support/example/integration_test/test_dht_record_pool.dart create mode 100644 packages/veilid_support/example/integration_test/test_dht_short_array.dart create mode 100644 packages/veilid_support/example/ios/.gitignore create mode 100644 packages/veilid_support/example/ios/Flutter/AppFrameworkInfo.plist create mode 100644 packages/veilid_support/example/ios/Flutter/Debug.xcconfig create mode 100644 packages/veilid_support/example/ios/Flutter/Release.xcconfig create mode 100644 packages/veilid_support/example/ios/Podfile create mode 100644 packages/veilid_support/example/ios/Runner.xcodeproj/project.pbxproj create mode 100644 packages/veilid_support/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 packages/veilid_support/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/veilid_support/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 packages/veilid_support/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 packages/veilid_support/example/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 packages/veilid_support/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/veilid_support/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 packages/veilid_support/example/ios/Runner/AppDelegate.swift create mode 100644 packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 packages/veilid_support/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 packages/veilid_support/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 packages/veilid_support/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 packages/veilid_support/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 packages/veilid_support/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 packages/veilid_support/example/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 packages/veilid_support/example/ios/Runner/Base.lproj/Main.storyboard create mode 100644 packages/veilid_support/example/ios/Runner/Info.plist create mode 100644 packages/veilid_support/example/ios/Runner/Runner-Bridging-Header.h create mode 100644 packages/veilid_support/example/ios/RunnerTests/RunnerTests.swift create mode 100644 packages/veilid_support/example/lib/main.dart create mode 100644 packages/veilid_support/example/linux/.gitignore create mode 100644 packages/veilid_support/example/linux/CMakeLists.txt create mode 100644 packages/veilid_support/example/linux/flutter/CMakeLists.txt create mode 100644 packages/veilid_support/example/linux/flutter/generated_plugin_registrant.cc create mode 100644 packages/veilid_support/example/linux/flutter/generated_plugin_registrant.h create mode 100644 packages/veilid_support/example/linux/flutter/generated_plugins.cmake create mode 100644 packages/veilid_support/example/linux/main.cc create mode 100644 packages/veilid_support/example/linux/my_application.cc create mode 100644 packages/veilid_support/example/linux/my_application.h create mode 100644 packages/veilid_support/example/macos/.gitignore create mode 100644 packages/veilid_support/example/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 packages/veilid_support/example/macos/Flutter/Flutter-Release.xcconfig create mode 100644 packages/veilid_support/example/macos/Flutter/GeneratedPluginRegistrant.swift create mode 100644 packages/veilid_support/example/macos/Podfile create mode 100644 packages/veilid_support/example/macos/Podfile.lock create mode 100644 packages/veilid_support/example/macos/Runner.xcodeproj/project.pbxproj create mode 100644 packages/veilid_support/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/veilid_support/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 packages/veilid_support/example/macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 packages/veilid_support/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/veilid_support/example/macos/Runner/AppDelegate.swift create mode 100644 packages/veilid_support/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 packages/veilid_support/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 packages/veilid_support/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 packages/veilid_support/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 packages/veilid_support/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 packages/veilid_support/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 packages/veilid_support/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 packages/veilid_support/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png create mode 100644 packages/veilid_support/example/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 packages/veilid_support/example/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 packages/veilid_support/example/macos/Runner/Configs/Debug.xcconfig create mode 100644 packages/veilid_support/example/macos/Runner/Configs/Release.xcconfig create mode 100644 packages/veilid_support/example/macos/Runner/Configs/Warnings.xcconfig create mode 100644 packages/veilid_support/example/macos/Runner/DebugProfile.entitlements create mode 100644 packages/veilid_support/example/macos/Runner/Info.plist create mode 100644 packages/veilid_support/example/macos/Runner/MainFlutterWindow.swift create mode 100644 packages/veilid_support/example/macos/Runner/Release.entitlements create mode 100644 packages/veilid_support/example/macos/RunnerTests/RunnerTests.swift create mode 100644 packages/veilid_support/example/pubspec.lock create mode 100644 packages/veilid_support/example/pubspec.yaml create mode 100644 packages/veilid_support/example/test/widget_test.dart create mode 100644 packages/veilid_support/example/web/favicon.png create mode 100644 packages/veilid_support/example/web/icons/Icon-192.png create mode 100644 packages/veilid_support/example/web/icons/Icon-512.png create mode 100644 packages/veilid_support/example/web/icons/Icon-maskable-192.png create mode 100644 packages/veilid_support/example/web/icons/Icon-maskable-512.png create mode 100644 packages/veilid_support/example/web/index.html create mode 100644 packages/veilid_support/example/web/manifest.json create mode 100644 packages/veilid_support/example/windows/.gitignore create mode 100644 packages/veilid_support/example/windows/CMakeLists.txt create mode 100644 packages/veilid_support/example/windows/flutter/CMakeLists.txt create mode 100644 packages/veilid_support/example/windows/flutter/generated_plugin_registrant.cc create mode 100644 packages/veilid_support/example/windows/flutter/generated_plugin_registrant.h create mode 100644 packages/veilid_support/example/windows/flutter/generated_plugins.cmake create mode 100644 packages/veilid_support/example/windows/runner/CMakeLists.txt create mode 100644 packages/veilid_support/example/windows/runner/Runner.rc create mode 100644 packages/veilid_support/example/windows/runner/flutter_window.cpp create mode 100644 packages/veilid_support/example/windows/runner/flutter_window.h create mode 100644 packages/veilid_support/example/windows/runner/main.cpp create mode 100644 packages/veilid_support/example/windows/runner/resource.h create mode 100644 packages/veilid_support/example/windows/runner/resources/app_icon.ico create mode 100644 packages/veilid_support/example/windows/runner/runner.exe.manifest create mode 100644 packages/veilid_support/example/windows/runner/utils.cpp create mode 100644 packages/veilid_support/example/windows/runner/utils.h create mode 100644 packages/veilid_support/example/windows/runner/win32_window.cpp create mode 100644 packages/veilid_support/example/windows/runner/win32_window.h create mode 100755 packages/veilid_support/run_integration_tests.sh create mode 100755 packages/veilid_support/run_integration_tests_web.sh create mode 100755 packages/veilid_support/run_unit_tests.sh diff --git a/android/app/build.gradle b/android/app/build.gradle index c2eb2d7..f5e4e3d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -35,7 +35,7 @@ if (keystorePropertiesFile.exists()) { } android { - ndkVersion "25.1.8937393" + ndkVersion "26.3.11579264" compileSdkVersion flutter.compileSdkVersion compileOptions { diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 3c472b9..e1ca574 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip diff --git a/dev-setup/_script_common b/dev-setup/_script_common index c0b656c..991088e 100644 --- a/dev-setup/_script_common +++ b/dev-setup/_script_common @@ -6,11 +6,11 @@ get_abs_filename() { } # Veilid location -VEILIDDIR=$(get_abs_filename "$SCRIPTDIR/../../veilid") +VEILIDDIR=$(get_abs_filename "$(git rev-parse --show-toplevel)/../veilid") if [ ! -d "$VEILIDDIR" ]; then echo 'Veilid git clone needs to be at $VEILIDDIR' exit 1 fi -# VeilidChat location -VEILIDCHATDIR=$(get_abs_filename "$SCRIPTDIR/../../veilid") +# App location +APPDIR=$(git rev-parse --show-toplevel) diff --git a/dev-setup/flutter_config.sh b/dev-setup/flutter_config.sh index eeec7ca..1168d74 100755 --- a/dev-setup/flutter_config.sh +++ b/dev-setup/flutter_config.sh @@ -6,12 +6,12 @@ SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" source $SCRIPTDIR/_script_common # iOS: Set deployment target -sed -i '' 's/IPHONEOS_DEPLOYMENT_TARGET = [^;]*/IPHONEOS_DEPLOYMENT_TARGET = 12.4/g' $VEILIDCHATDIR/ios/Runner.xcodeproj/project.pbxproj -sed -i '' "s/platform :ios, '[^']*'/platform :ios, '12.4'/g" $VEILIDCHATDIR/ios/Podfile +sed -i '' 's/IPHONEOS_DEPLOYMENT_TARGET = [^;]*/IPHONEOS_DEPLOYMENT_TARGET = 12.4/g' $APPDIR/ios/Runner.xcodeproj/project.pbxproj +sed -i '' "s/platform :ios, '[^']*'/platform :ios, '12.4'/g" $APPDIR/ios/Podfile # MacOS: Set deployment target -sed -i '' 's/MACOSX_DEPLOYMENT_TARGET = [^;]*/MACOSX_DEPLOYMENT_TARGET = 10.14.6/g' $VEILIDCHATDIR/macos/Runner.xcodeproj/project.pbxproj -sed -i '' "s/platform :osx, '[^']*'/platform :osx, '10.14.6'/g" $VEILIDCHATDIR/macos/Podfile +sed -i '' 's/MACOSX_DEPLOYMENT_TARGET = [^;]*/MACOSX_DEPLOYMENT_TARGET = 10.14.6/g' $APPDIR/macos/Runner.xcodeproj/project.pbxproj +sed -i '' "s/platform :osx, '[^']*'/platform :osx, '10.14.6'/g" $APPDIR/macos/Podfile # Android: Set NDK version if [[ "$TMPDIR" != "" ]]; then @@ -20,16 +20,16 @@ else ANDTMP=/tmp/andtmp_$(date +%s) fi cat < $ANDTMP - ndkVersion '25.1.8937393' + ndkVersion '26.3.11579264' EOF -sed -i '' -e "/android {/r $ANDTMP" $VEILIDCHATDIR/android/app/build.gradle +sed -i '' -e "/android {/r $ANDTMP" $APPDIR/android/app/build.gradle rm -- $ANDTMP # Android: Set min sdk version -sed -i '' 's/minSdkVersion .*/minSdkVersion Math.max(flutter.minSdkVersion, 24)/g' $VEILIDCHATDIR/android/app/build.gradle +sed -i '' 's/minSdkVersion .*/minSdkVersion Math.max(flutter.minSdkVersion, 24)/g' $APPDIR/android/app/build.gradle # Android: Set gradle plugin version -sed -i '' "s/classpath \'com.android.tools.build:gradle:[^\']*\'/classpath 'com.android.tools.build:gradle:7.2.0'/g" $VEILIDCHATDIR/android/build.gradle +sed -i '' "s/classpath \'com.android.tools.build:gradle:[^\']*\'/classpath 'com.android.tools.build:gradle:7.2.0'/g" $APPDIR/android/build.gradle # Android: Set gradle version -sed -i '' 's/distributionUrl=.*/distributionUrl=https:\/\/services.gradle.org\/distributions\/gradle-7.3.3-all.zip/g' $VEILIDCHATDIR/android/gradle/wrapper/gradle-wrapper.properties +sed -i '' 's/distributionUrl=.*/distributionUrl=https:\/\/services.gradle.org\/distributions\/gradle-7.6.3-all.zip/g' $APPDIR/android/gradle/wrapper/gradle-wrapper.properties diff --git a/dev-setup/wasm_update.sh b/dev-setup/wasm_update.sh index 01633a3..6dec701 100755 --- a/dev-setup/wasm_update.sh +++ b/dev-setup/wasm_update.sh @@ -5,7 +5,7 @@ source $SCRIPTDIR/_script_common pushd $SCRIPTDIR >/dev/null # WASM output dir -WASMDIR=$VEILIDCHATDIR/web/wasm +WASMDIR=$APPDIR/web/wasm # Build veilid-wasm, passing any arguments here to the build script pushd $VEILIDDIR/veilid-wasm >/dev/null diff --git a/packages/veilid_support/debug_integration_tests.sh b/packages/veilid_support/debug_integration_tests.sh new file mode 100755 index 0000000..c689e64 --- /dev/null +++ b/packages/veilid_support/debug_integration_tests.sh @@ -0,0 +1,4 @@ +#!/bin/bash +pushd example 2>/dev/null +flutter run integration_test/app_test.dart $@ +popd 2>/dev/null diff --git a/packages/veilid_support/example/.gitignore b/packages/veilid_support/example/.gitignore new file mode 100644 index 0000000..29a3a50 --- /dev/null +++ b/packages/veilid_support/example/.gitignore @@ -0,0 +1,43 @@ +# 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 +.pub-cache/ +.pub/ +/build/ + +# 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 diff --git a/packages/veilid_support/example/.metadata b/packages/veilid_support/example/.metadata new file mode 100644 index 0000000..d2765fc --- /dev/null +++ b/packages/veilid_support/example/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "54e66469a933b60ddf175f858f82eaeb97e48c8d" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + - platform: android + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + - platform: ios + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + - platform: linux + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + - platform: macos + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + - platform: web + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + - platform: windows + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/veilid_support/example/README.md b/packages/veilid_support/example/README.md new file mode 100644 index 0000000..2b3fce4 --- /dev/null +++ b/packages/veilid_support/example/README.md @@ -0,0 +1,16 @@ +# example + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/veilid_support/example/analysis_options.yaml b/packages/veilid_support/example/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/packages/veilid_support/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/veilid_support/example/android/.gitignore b/packages/veilid_support/example/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/packages/veilid_support/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/packages/veilid_support/example/android/app/build.gradle b/packages/veilid_support/example/android/app/build.gradle new file mode 100644 index 0000000..033f4ac --- /dev/null +++ b/packages/veilid_support/example/android/app/build.gradle @@ -0,0 +1,69 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +android { + ndkVersion '26.3.11579264' + ndkVersion '26.3.11579264' + namespace "com.example.example" + compileSdk flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.example" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion Math.max(flutter.minSdkVersion, 24) + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies {} diff --git a/packages/veilid_support/example/android/app/src/debug/AndroidManifest.xml b/packages/veilid_support/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/packages/veilid_support/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/veilid_support/example/android/app/src/main/AndroidManifest.xml b/packages/veilid_support/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..aff7dec --- /dev/null +++ b/packages/veilid_support/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/veilid_support/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/packages/veilid_support/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt new file mode 100644 index 0000000..70f8f08 --- /dev/null +++ b/packages/veilid_support/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() diff --git a/packages/veilid_support/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/veilid_support/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/packages/veilid_support/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/veilid_support/example/android/app/src/main/res/drawable/launch_background.xml b/packages/veilid_support/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/packages/veilid_support/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/veilid_support/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/veilid_support/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/packages/veilid_support/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/veilid_support/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/packages/veilid_support/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/veilid_support/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/packages/veilid_support/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/veilid_support/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/packages/veilid_support/example/android/app/src/main/res/values-night/styles.xml b/packages/veilid_support/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/packages/veilid_support/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/veilid_support/example/android/app/src/main/res/values/styles.xml b/packages/veilid_support/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/packages/veilid_support/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/veilid_support/example/android/app/src/profile/AndroidManifest.xml b/packages/veilid_support/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/packages/veilid_support/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/veilid_support/example/android/build.gradle b/packages/veilid_support/example/android/build.gradle new file mode 100644 index 0000000..bc157bd --- /dev/null +++ b/packages/veilid_support/example/android/build.gradle @@ -0,0 +1,18 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/packages/veilid_support/example/android/gradle.properties b/packages/veilid_support/example/android/gradle.properties new file mode 100644 index 0000000..598d13f --- /dev/null +++ b/packages/veilid_support/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx4G +android.useAndroidX=true +android.enableJetifier=true diff --git a/packages/veilid_support/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/veilid_support/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..c3433f7 --- /dev/null +++ b/packages/veilid_support/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https://services.gradle.org/distributions/gradle-7.3.3-all.zip diff --git a/packages/veilid_support/example/android/settings.gradle b/packages/veilid_support/example/android/settings.gradle new file mode 100644 index 0000000..1d6d19b --- /dev/null +++ b/packages/veilid_support/example/android/settings.gradle @@ -0,0 +1,26 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + } + settings.ext.flutterSdkPath = flutterSdkPath() + + includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.3.0" apply false + id "org.jetbrains.kotlin.android" version "1.7.10" apply false +} + +include ":app" diff --git a/packages/veilid_support/example/dev-setup/_script_common b/packages/veilid_support/example/dev-setup/_script_common new file mode 100644 index 0000000..c8aa85e --- /dev/null +++ b/packages/veilid_support/example/dev-setup/_script_common @@ -0,0 +1,16 @@ +set -eo pipefail + +get_abs_filename() { + # $1 : relative filename + echo "$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" +} + +# Veilid location +VEILIDDIR=$(get_abs_filename "$(git rev-parse --show-toplevel)/../veilid") +if [ ! -d "$VEILIDDIR" ]; then + echo 'Veilid git clone needs to be at $VEILIDDIR' + exit 1 +fi + +# App location +APPDIR=$(get_abs_filename "$SCRIPTDIR/..") diff --git a/packages/veilid_support/example/dev-setup/flutter_config.sh b/packages/veilid_support/example/dev-setup/flutter_config.sh new file mode 100755 index 0000000..9cb53c7 --- /dev/null +++ b/packages/veilid_support/example/dev-setup/flutter_config.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# +# Run this if you regenerate and need to reconfigure platform specific make system project files +# +SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +source $SCRIPTDIR/_script_common + +# iOS: Set deployment target +sed -i '' 's/IPHONEOS_DEPLOYMENT_TARGET = [^;]*/IPHONEOS_DEPLOYMENT_TARGET = 12.4/g' $APPDIR/ios/Runner.xcodeproj/project.pbxproj +sed -i '' "s/platform :ios, '[^']*'/platform :ios, '12.4'/g" $APPDIR/ios/Podfile + +# MacOS: Set deployment target +sed -i '' 's/MACOSX_DEPLOYMENT_TARGET = [^;]*/MACOSX_DEPLOYMENT_TARGET = 10.14.6/g' $APPDIR/macos/Runner.xcodeproj/project.pbxproj +sed -i '' "s/platform :osx, '[^']*'/platform :osx, '10.14.6'/g" $APPDIR/macos/Podfile + +# Android: Set NDK version +if [[ "$TMPDIR" != "" ]]; then + ANDTMP=$TMPDIR/andtmp_$(date +%s) +else + ANDTMP=/tmp/andtmp_$(date +%s) +fi +cat < $ANDTMP + ndkVersion '26.3.11579264' +EOF +sed -i '' -e "/android {/r $ANDTMP" $APPDIR/android/app/build.gradle +rm -- $ANDTMP + +# Android: Set min sdk version +sed -i '' 's/minSdkVersion .*/minSdkVersion Math.max(flutter.minSdkVersion, 24)/g' $APPDIR/android/app/build.gradle + +# Android: Set gradle plugin version +sed -i '' "s/classpath \'com.android.tools.build:gradle:[^\']*\'/classpath 'com.android.tools.build:gradle:7.2.0'/g" $APPDIR/android/build.gradle + +# Android: Set gradle version +sed -i '' 's/distributionUrl=.*/distributionUrl=https:\/\/services.gradle.org\/distributions\/gradle-7.3.3-all.zip/g' $APPDIR/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/veilid_support/example/dev-setup/wasm_update.sh b/packages/veilid_support/example/dev-setup/wasm_update.sh new file mode 100755 index 0000000..6dec701 --- /dev/null +++ b/packages/veilid_support/example/dev-setup/wasm_update.sh @@ -0,0 +1,25 @@ +#!/bin/bash +SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +source $SCRIPTDIR/_script_common + +pushd $SCRIPTDIR >/dev/null + +# WASM output dir +WASMDIR=$APPDIR/web/wasm + +# Build veilid-wasm, passing any arguments here to the build script +pushd $VEILIDDIR/veilid-wasm >/dev/null +PKGDIR=$(./wasm_build.sh $@ | grep SUCCESS:OUTPUTDIR | cut -d= -f2) +popd >/dev/null + +# Copy wasm blob into place +echo Updating WASM from $PKGDIR to $WASMDIR +if [ -d $WASMDIR ]; then + rm -f $WASMDIR/* +fi +mkdir -p $WASMDIR +cp -f $PKGDIR/* $WASMDIR/ + +#### Done + +popd >/dev/null diff --git a/packages/veilid_support/example/integration_test/app_test.dart b/packages/veilid_support/example/integration_test/app_test.dart new file mode 100644 index 0000000..542ce54 --- /dev/null +++ b/packages/veilid_support/example/integration_test/app_test.dart @@ -0,0 +1,39 @@ +@Timeout(Duration(seconds: 60)) + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'fixtures.dart'; +import 'test_dht_record_pool.dart'; +import 'test_dht_short_array.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final fixture = DefaultFixture(); + + group('Started Tests', () { + setUpAll(fixture.setUp); + tearDownAll(fixture.tearDown); + + // group('Crypto Tests', () { + // test('best cryptosystem', testBestCryptoSystem); + // test('get cryptosystem', testGetCryptoSystem); + // test('get cryptosystem invalid', testGetCryptoSystemInvalid); + // test('hash and verify password', testHashAndVerifyPassword); + // }); + + group('Attached Tests', () { + setUpAll(fixture.attach); + tearDownAll(fixture.detach); + + group('DHT Support Tests', () { + group('DHTRecordPool Tests', () { + test('create pool', testDHTRecordPoolCreate); + }); + group('DHTShortArray Tests', () { + test('create shortarray', testDHTShortArrayCreate); + }); + }); + }); + }); +} diff --git a/packages/veilid_support/example/integration_test/fixtures.dart b/packages/veilid_support/example/integration_test/fixtures.dart new file mode 100644 index 0000000..ef69e81 --- /dev/null +++ b/packages/veilid_support/example/integration_test/fixtures.dart @@ -0,0 +1,152 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:mutex/mutex.dart'; +import 'package:veilid/veilid.dart'; + +class DefaultFixture { + DefaultFixture(); + + StreamSubscription? _veilidUpdateSubscription; + Stream? _veilidUpdateStream; + final StreamController _updateStreamController = + StreamController.broadcast(); + + static final _fixtureMutex = Mutex(); + + Future setUp() async { + await _fixtureMutex.acquire(); + + assert(_veilidUpdateStream == null, 'should not set up fixture twice'); + + 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(); + + final Map platformConfigJson; + if (kIsWeb) { + final platformConfig = VeilidWASMConfig( + logging: VeilidWASMConfigLogging( + performance: VeilidWASMConfigLoggingPerformance( + enabled: true, + level: VeilidConfigLogLevel.debug, + logsInTimings: true, + logsInConsole: false, + ignoreLogTargets: ignoreLogTargets, + ), + api: VeilidWASMConfigLoggingApi( + enabled: true, + level: VeilidConfigLogLevel.info, + ignoreLogTargets: ignoreLogTargets, + ))); + platformConfigJson = platformConfig.toJson(); + } else { + final platformConfig = VeilidFFIConfig( + logging: VeilidFFIConfigLogging( + terminal: VeilidFFIConfigLoggingTerminal( + enabled: false, + level: VeilidConfigLogLevel.debug, + ignoreLogTargets: ignoreLogTargets, + ), + otlp: VeilidFFIConfigLoggingOtlp( + enabled: false, + level: VeilidConfigLogLevel.trace, + grpcEndpoint: 'localhost:4317', + serviceName: 'Veilid Tests', + ignoreLogTargets: ignoreLogTargets, + ), + api: VeilidFFIConfigLoggingApi( + enabled: true, + // level: VeilidConfigLogLevel.debug, + level: VeilidConfigLogLevel.info, + ignoreLogTargets: ignoreLogTargets, + ))); + platformConfigJson = platformConfig.toJson(); + } + Veilid.instance.initializeVeilidCore(platformConfigJson); + + var config = await getDefaultVeilidConfig( + isWeb: kIsWeb, + programName: 'Veilid Tests', + // 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'), + ); + + config = + config.copyWith(tableStore: config.tableStore.copyWith(delete: true)); + config = config.copyWith( + protectedStore: config.protectedStore.copyWith(delete: true)); + config = + config.copyWith(blockStore: config.blockStore.copyWith(delete: true)); + + final us = + _veilidUpdateStream = await Veilid.instance.startupVeilidCore(config); + + _veilidUpdateSubscription = us.listen((update) { + if (update is VeilidLog) { + // print(update.message); + } else if (update is VeilidUpdateAttachment) { + } else if (update is VeilidUpdateConfig) { + } else if (update is VeilidUpdateNetwork) { + } else if (update is VeilidAppMessage) { + } else if (update is VeilidAppCall) { + } else if (update is VeilidUpdateValueChange) { + } else if (update is VeilidUpdateRouteChange) { + } else { + throw Exception('unexpected update: $update'); + } + _updateStreamController.sink.add(update); + }); + } + + Stream get updateStream => _updateStreamController.stream; + + Future attach() async { + await Veilid.instance.attach(); + + // Wait for attached state + while (true) { + final state = await Veilid.instance.getVeilidState(); + var done = false; + if (state.attachment.publicInternetReady) { + switch (state.attachment.state) { + case AttachmentState.detached: + break; + case AttachmentState.attaching: + break; + case AttachmentState.detaching: + break; + default: + done = true; + break; + } + } + if (done) { + break; + } + await Future.delayed(const Duration(seconds: 1)); + } + } + + Future detach() async { + await Veilid.instance.detach(); + } + + Future tearDown() async { + assert(_veilidUpdateStream != null, 'should not tearDown without setUp'); + + final cancelFut = _veilidUpdateSubscription?.cancel(); + await Veilid.instance.shutdownVeilidCore(); + await cancelFut; + + _veilidUpdateSubscription = null; + _veilidUpdateStream = null; + + _fixtureMutex.release(); + } +} diff --git a/packages/veilid_support/example/integration_test/test_dht_record_pool.dart b/packages/veilid_support/example/integration_test/test_dht_record_pool.dart new file mode 100644 index 0000000..56d398e --- /dev/null +++ b/packages/veilid_support/example/integration_test/test_dht_record_pool.dart @@ -0,0 +1,9 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:veilid_support/veilid_support.dart'; + +Future testDHTRecordPoolCreate() async { + // final cs = await Veilid.instance.bestCryptoSystem(); + // expect(await cs.defaultSaltLength(), equals(16)); +} diff --git a/packages/veilid_support/example/integration_test/test_dht_short_array.dart b/packages/veilid_support/example/integration_test/test_dht_short_array.dart new file mode 100644 index 0000000..4a3c627 --- /dev/null +++ b/packages/veilid_support/example/integration_test/test_dht_short_array.dart @@ -0,0 +1,9 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:veilid_support/veilid_support.dart'; + +Future testDHTShortArrayCreate() async { + // final cs = await Veilid.instance.bestCryptoSystem(); + // expect(await cs.defaultSaltLength(), equals(16)); +} diff --git a/packages/veilid_support/example/ios/.gitignore b/packages/veilid_support/example/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/packages/veilid_support/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/packages/veilid_support/example/ios/Flutter/AppFrameworkInfo.plist b/packages/veilid_support/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..7c56964 --- /dev/null +++ b/packages/veilid_support/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/packages/veilid_support/example/ios/Flutter/Debug.xcconfig b/packages/veilid_support/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/packages/veilid_support/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/veilid_support/example/ios/Flutter/Release.xcconfig b/packages/veilid_support/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/packages/veilid_support/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/veilid_support/example/ios/Podfile b/packages/veilid_support/example/ios/Podfile new file mode 100644 index 0000000..cc206f6 --- /dev/null +++ b/packages/veilid_support/example/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.4' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/veilid_support/example/ios/Runner.xcodeproj/project.pbxproj b/packages/veilid_support/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..c3b1948 --- /dev/null +++ b/packages/veilid_support/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,619 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.4; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = W2TA5TB8Q5; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.4; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.4; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = W2TA5TB8Q5; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = W2TA5TB8Q5; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/veilid_support/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/veilid_support/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/packages/veilid_support/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/veilid_support/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/veilid_support/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/packages/veilid_support/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/veilid_support/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/veilid_support/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/packages/veilid_support/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/veilid_support/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/veilid_support/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..8e3ca5d --- /dev/null +++ b/packages/veilid_support/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/veilid_support/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/veilid_support/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/packages/veilid_support/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/veilid_support/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/veilid_support/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/packages/veilid_support/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/veilid_support/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/veilid_support/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/packages/veilid_support/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/veilid_support/example/ios/Runner/AppDelegate.swift b/packages/veilid_support/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/packages/veilid_support/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc9ada4725e9b0ddb1deab583e5b5102493aa332 GIT binary patch literal 10932 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 literal 0 HcmV?d00001 diff --git a/packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..797d452e458972bab9d994556c8305db4c827017 GIT binary patch literal 406 zcmV;H0crk;P))>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed2d933e1120817fe9182483a228007b18ab6ae GIT binary patch literal 450 zcmV;z0X_bSP)iGWQ_5NJQ_~rNh*z)}eT%KUb z`7gNk0#AwF^#0T0?hIa^`~Ck;!}#m+_uT050aTR(J!bU#|IzRL%^UsMS#KsYnTF*!YeDOytlP4VhV?b} z%rz_<=#CPc)tU1MZTq~*2=8~iZ!lSa<{9b@2Jl;?IEV8)=fG217*|@)CCYgFze-x? zIFODUIA>nWKpE+bn~n7;-89sa>#DR>TSlqWk*!2hSN6D~Qb#VqbP~4Fk&m`@1$JGr zXPIdeRE&b2Thd#{MtDK$px*d3-Wx``>!oimf%|A-&-q*6KAH)e$3|6JV%HX{Hig)k suLT-RhftRq8b9;(V=235Wa|I=027H2wCDra;{X5v07*qoM6N<$f;9x^2LJ#7 literal 0 HcmV?d00001 diff --git a/packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd7b0099ca80c806f8fe495613e8d6c69460d76 GIT binary patch literal 282 zcmV+#0p(^bcu7P-R4C8Q z&e;xxFbF_Vrezo%_kH*OKhshZ6BFpG-Y1e10`QXJKbND7AMQ&cMj60B5TNObaZxYybcN07*qoM6N<$g3m;S%K!iX literal 0 HcmV?d00001 diff --git a/packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fe730945a01f64a61e2235dbe3f45b08f7729182 GIT binary patch literal 462 zcmV;<0WtoGP)-}iV`2<;=$?g5M=KQbZ{F&YRNy7Nn@%_*5{gvDM0aKI4?ESmw z{NnZg)A0R`+4?NF_RZexyVB&^^ZvN!{I28tr{Vje;QNTz`dG&Jz0~Ek&f2;*Z7>B|cg}xYpxEFY+0YrKLF;^Q+-HreN0P{&i zK~zY`?b7ECf-n?@;d<&orQ*Q7KoR%4|C>{W^h6@&01>0SKS`dn{Q}GT%Qj_{PLZ_& zs`MFI#j-(>?bvdZ!8^xTwlY{qA)T4QLbY@j(!YJ7aXJervHy6HaG_2SB`6CC{He}f zHVw(fJWApwPq!6VY7r1w-Fs)@ox~N+q|w~e;JI~C4Vf^@d>Wvj=fl`^u9x9wd9 zR%3*Q+)t%S!MU_`id^@&Y{y7-r98lZX0?YrHlfmwb?#}^1b{8g&KzmkE(L>Z&)179 zp<)v6Y}pRl100G2FL_t(o!|l{-Q-VMg#&MKg7c{O0 z2wJImOS3Gy*Z2Qifdv~JYOp;v+U)a|nLoc7hNH;I$;lzDt$}rkaFw1mYK5_0Q(Sut zvbEloxON7$+HSOgC9Z8ltuC&0OSF!-mXv5caV>#bc3@hBPX@I$58-z}(ZZE!t-aOG zpjNkbau@>yEzH(5Yj4kZiMH32XI!4~gVXNnjAvRx;Sdg^`>2DpUEwoMhTs_st8pKG z(%SHyHdU&v%f36~uERh!bd`!T2dw;z6PrOTQ7Vt*#9F2uHlUVnb#ev_o^fh}Dzmq} zWtlk35}k=?xj28uO|5>>$yXadTUE@@IPpgH`gJ~Ro4>jd1IF|(+IX>8M4Ps{PNvmI zNj4D+XgN83gPt_Gm}`Ybv{;+&yu-C(Grdiahmo~BjG-l&mWM+{e5M1sm&=xduwgM9 z`8OEh`=F3r`^E{n_;%9weN{cf2%7=VzC@cYj+lg>+3|D|_1C@{hcU(DyQG_BvBWe? zvTv``=%b1zrol#=R`JB)>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..502f463a9bc882b461c96aadf492d1729e49e725 GIT binary patch literal 586 zcmV-Q0=4~#P)+}#`wDE{8-2Mebf5<{{PqV{TgVcv*r8?UZ3{-|G?_}T*&y;@cqf{ z{Q*~+qr%%p!1pS*_Uicl#q9lc(D`!D`LN62sNwq{oYw(Wmhk)k<@f$!$@ng~_5)Ru z0Z)trIA5^j{DIW^c+vT2%lW+2<(RtE2wR;4O@)Tm`Xr*?A(qYoM}7i5Yxw>D(&6ou zxz!_Xr~yNF+waPe00049Nkl*;a!v6h%{rlvIH#gW3s8p;bFr=l}mRqpW2h zw=OA%hdyL~z+UHOzl0eKhEr$YYOL-c-%Y<)=j?(bzDweB7{b+%_ypvm_cG{SvM=DK zhv{K@m>#Bw>2W$eUI#iU)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G! zhkE!s;%oku3;IwG3U^2kw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn`0X*2 zy3(k600_CSZj?O$Qu%&$;|TGUJrptR(HzyIx>5E(2r{eA(<6t3e3I0B)7d6s7?Z5J zZ!rtKvA{MiEBm&KFtoifx>5P^Z=vl)95XJn()aS5%ad(s?4-=Tkis9IGu{`Fy8r+H07*qoM6N<$f20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec303439225b78712f49115768196d8d76f6790 GIT binary patch literal 862 zcmV-k1EKthP)20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f5fea27c705180eb716271f41b582e76dcbd90 GIT binary patch literal 1674 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 literal 0 HcmV?d00001 diff --git a/packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/veilid_support/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0467bf12aa4d28f374bb26596605a46dcbb3e7c8 GIT binary patch literal 1418 zcmV;51$Fv~P)q zKfU)WzW*n(@|xWGCA9ScMt*e9`2kdxPQ&&>|-UCa7_51w+ zLUsW@ZzZSW0y$)Hp~e9%PvP|a03ks1`~K?q{u;6NC8*{AOqIUq{CL&;p56Lf$oQGq z^={4hPQv)y=I|4n+?>7Fim=dxt1 z2H+Dm+1+fh+IF>G0SjJMkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJT zkdTm&kdTm&kdTm&kdP`esgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>Tmf` zm1{eLgw!b{bXkjWbF%dTkTZEJWyWOb##Lfw4EK2}<0d6%>AGS{po>WCOy&f$Tay_> z?NBlkpo@s-O;0V%Y_Xa-G#_O08q5LR*~F%&)}{}r&L%Sbs8AS4t7Y0NEx*{soY=0MZExqA5XHQkqi#4gW3 zqODM^iyZl;dvf)-bOXtOru(s)Uc7~BFx{w-FK;2{`VA?(g&@3z&bfLFyctOH!cVsF z7IL=fo-qBndRUm;kAdXR4e6>k-z|21AaN%ubeVrHl*<|s&Ax@W-t?LR(P-24A5=>a z*R9#QvjzF8n%@1Nw@?CG@6(%>+-0ASK~jEmCV|&a*7-GKT72W<(TbSjf)&Eme6nGE z>Gkj4Sq&2e+-G%|+NM8OOm5zVl9{Z8Dd8A5z3y8mZ=4Bv4%>as_{9cN#bm~;h>62( zdqY93Zy}v&c4n($Vv!UybR8ocs7#zbfX1IY-*w~)p}XyZ-SFC~4w>BvMVr`dFbelV{lLL0bx7@*ZZdebr3`sP;? zVImji)kG)(6Juv0lz@q`F!k1FE;CQ(D0iG$wchPbKZQELlsZ#~rt8#90Y_Xh&3U-< z{s<&cCV_1`^TD^ia9!*mQDq& zn2{r`j};V|uV%_wsP!zB?m%;FeaRe+X47K0e+KE!8C{gAWF8)lCd1u1%~|M!XNRvw zvtqy3iz0WSpWdhn6$hP8PaRBmp)q`#PCA`Vd#Tc$@f1tAcM>f_I@bC)hkI9|o(Iqv zo}Piadq!j76}004RBio<`)70k^`K1NK)q>w?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF z34$0Z;QO!JOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUC YUoZo%k(ykuW&i*H07*qoM6N<$f+CH{y8r+H literal 0 HcmV?d00001 diff --git a/packages/veilid_support/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/veilid_support/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/packages/veilid_support/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/veilid_support/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/veilid_support/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/packages/veilid_support/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/veilid_support/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/packages/veilid_support/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/veilid_support/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/packages/veilid_support/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/veilid_support/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/packages/veilid_support/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/veilid_support/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/veilid_support/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/packages/veilid_support/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/veilid_support/example/ios/Runner/Base.lproj/Main.storyboard b/packages/veilid_support/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/packages/veilid_support/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/veilid_support/example/ios/Runner/Info.plist b/packages/veilid_support/example/ios/Runner/Info.plist new file mode 100644 index 0000000..5458fc4 --- /dev/null +++ b/packages/veilid_support/example/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/packages/veilid_support/example/ios/Runner/Runner-Bridging-Header.h b/packages/veilid_support/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/packages/veilid_support/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/packages/veilid_support/example/ios/RunnerTests/RunnerTests.swift b/packages/veilid_support/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/packages/veilid_support/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/packages/veilid_support/example/lib/main.dart b/packages/veilid_support/example/lib/main.dart new file mode 100644 index 0000000..8e94089 --- /dev/null +++ b/packages/veilid_support/example/lib/main.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + // This is the theme of your application. + // + // TRY THIS: Try running your application with "flutter run". You'll see + // the application has a purple toolbar. Then, without quitting the app, + // try changing the seedColor in the colorScheme below to Colors.green + // and then invoke "hot reload" (save your changes or press the "hot + // reload" button in a Flutter-supported IDE, or press "r" if you used + // the command line to start the app). + // + // Notice that the counter didn't reset back to zero; the application + // state is not lost during the reload. To reset the state, use hot + // restart instead. + // + // This works for code too, not just values: Most code changes can be + // tested with just a hot reload. + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, + ), + home: const MyHomePage(title: 'Flutter Demo Home Page'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, required this.title}); + + // This widget is the home page of your application. It is stateful, meaning + // that it has a State object (defined below) that contains fields that affect + // how it looks. + + // This class is the configuration for the state. It holds the values (in this + // case the title) provided by the parent (in this case the App widget) and + // used by the build method of the State. Fields in a Widget subclass are + // always marked "final". + + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + int _counter = 0; + + void _incrementCounter() { + setState(() { + // This call to setState tells the Flutter framework that something has + // changed in this State, which causes it to rerun the build method below + // so that the display can reflect the updated values. If we changed + // _counter without calling setState(), then the build method would not be + // called again, and so nothing would appear to happen. + _counter++; + }); + } + + @override + Widget build(BuildContext context) { + // This method is rerun every time setState is called, for instance as done + // by the _incrementCounter method above. + // + // The Flutter framework has been optimized to make rerunning build methods + // fast, so that you can just rebuild anything that needs updating rather + // than having to individually change instances of widgets. + return Scaffold( + appBar: AppBar( + // TRY THIS: Try changing the color here to a specific color (to + // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar + // change color while the other colors stay the same. + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + // Here we take the value from the MyHomePage object that was created by + // the App.build method, and use it to set our appbar title. + title: Text(widget.title), + ), + body: Center( + // Center is a layout widget. It takes a single child and positions it + // in the middle of the parent. + child: Column( + // Column is also a layout widget. It takes a list of children and + // arranges them vertically. By default, it sizes itself to fit its + // children horizontally, and tries to be as tall as its parent. + // + // Column has various properties to control how it sizes itself and + // how it positions its children. Here we use mainAxisAlignment to + // center the children vertically; the main axis here is the vertical + // axis because Columns are vertical (the cross axis would be + // horizontal). + // + // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" + // action in the IDE, or press "p" in the console), to see the + // wireframe for each widget. + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'You have pushed the button this many times:', + ), + Text( + '$_counter', + style: Theme.of(context).textTheme.headlineMedium, + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: _incrementCounter, + tooltip: 'Increment', + child: const Icon(Icons.add), + ), // This trailing comma makes auto-formatting nicer for build methods. + ); + } +} diff --git a/packages/veilid_support/example/linux/.gitignore b/packages/veilid_support/example/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/packages/veilid_support/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/packages/veilid_support/example/linux/CMakeLists.txt b/packages/veilid_support/example/linux/CMakeLists.txt new file mode 100644 index 0000000..9cb0d1d --- /dev/null +++ b/packages/veilid_support/example/linux/CMakeLists.txt @@ -0,0 +1,145 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "example") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/packages/veilid_support/example/linux/flutter/CMakeLists.txt b/packages/veilid_support/example/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/packages/veilid_support/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/packages/veilid_support/example/linux/flutter/generated_plugin_registrant.cc b/packages/veilid_support/example/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..cebc32d --- /dev/null +++ b/packages/veilid_support/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) veilid_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "VeilidPlugin"); + veilid_plugin_register_with_registrar(veilid_registrar); +} diff --git a/packages/veilid_support/example/linux/flutter/generated_plugin_registrant.h b/packages/veilid_support/example/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/packages/veilid_support/example/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/veilid_support/example/linux/flutter/generated_plugins.cmake b/packages/veilid_support/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..003d7b5 --- /dev/null +++ b/packages/veilid_support/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + veilid +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/veilid_support/example/linux/main.cc b/packages/veilid_support/example/linux/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/packages/veilid_support/example/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/packages/veilid_support/example/linux/my_application.cc b/packages/veilid_support/example/linux/my_application.cc new file mode 100644 index 0000000..c0530d4 --- /dev/null +++ b/packages/veilid_support/example/linux/my_application.cc @@ -0,0 +1,124 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/packages/veilid_support/example/linux/my_application.h b/packages/veilid_support/example/linux/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/packages/veilid_support/example/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/packages/veilid_support/example/macos/.gitignore b/packages/veilid_support/example/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/packages/veilid_support/example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/packages/veilid_support/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/veilid_support/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/packages/veilid_support/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/veilid_support/example/macos/Flutter/Flutter-Release.xcconfig b/packages/veilid_support/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/packages/veilid_support/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/veilid_support/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/veilid_support/example/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..48d1dff --- /dev/null +++ b/packages/veilid_support/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import path_provider_foundation +import veilid + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + VeilidPlugin.register(with: registry.registrar(forPlugin: "VeilidPlugin")) +} diff --git a/packages/veilid_support/example/macos/Podfile b/packages/veilid_support/example/macos/Podfile new file mode 100644 index 0000000..036dad0 --- /dev/null +++ b/packages/veilid_support/example/macos/Podfile @@ -0,0 +1,43 @@ +platform :osx, '10.14.6' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/packages/veilid_support/example/macos/Podfile.lock b/packages/veilid_support/example/macos/Podfile.lock new file mode 100644 index 0000000..ac31a59 --- /dev/null +++ b/packages/veilid_support/example/macos/Podfile.lock @@ -0,0 +1,29 @@ +PODS: + - FlutterMacOS (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - veilid (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - FlutterMacOS (from `Flutter/ephemeral`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - veilid (from `Flutter/ephemeral/.symlinks/plugins/veilid/macos`) + +EXTERNAL SOURCES: + FlutterMacOS: + :path: Flutter/ephemeral + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + veilid: + :path: Flutter/ephemeral/.symlinks/plugins/veilid/macos + +SPEC CHECKSUMS: + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + veilid: a54f57b7bcf0e4e072fe99272d76ca126b2026d0 + +PODFILE CHECKSUM: 16208599a12443d53889ba2270a4985981cfb204 + +COCOAPODS: 1.15.2 diff --git a/packages/veilid_support/example/macos/Runner.xcodeproj/project.pbxproj b/packages/veilid_support/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..b6717eb --- /dev/null +++ b/packages/veilid_support/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,801 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 6CFA599ADEA1F061ADEDAE10 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 80431090FA5BD99A5B868F84 /* Pods_Runner.framework */; }; + 91F51E973F11EFA10521D360 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C8526B53F6575BAE32CE8D8 /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 3597F23CF6B8931362B671EB /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 4C8526B53F6575BAE32CE8D8 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 80431090FA5BD99A5B868F84 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 836CFD5B5891A750B1490B1C /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 83A502A4616091D33488BCEE /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 8B20F93F35956FDE2766A851 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + A24D16EEC9CB70E4E4BE3331 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + C581BE4277030CD81FD25B44 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 91F51E973F11EFA10521D360 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6CFA599ADEA1F061ADEDAE10 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 7C41EBAAEDA4C42598BAF422 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* example.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 7C41EBAAEDA4C42598BAF422 /* Pods */ = { + isa = PBXGroup; + children = ( + 3597F23CF6B8931362B671EB /* Pods-Runner.debug.xcconfig */, + 8B20F93F35956FDE2766A851 /* Pods-Runner.release.xcconfig */, + C581BE4277030CD81FD25B44 /* Pods-Runner.profile.xcconfig */, + 836CFD5B5891A750B1490B1C /* Pods-RunnerTests.debug.xcconfig */, + A24D16EEC9CB70E4E4BE3331 /* Pods-RunnerTests.release.xcconfig */, + 83A502A4616091D33488BCEE /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 80431090FA5BD99A5B868F84 /* Pods_Runner.framework */, + 4C8526B53F6575BAE32CE8D8 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + EFC9A90D4C4DA1DCAD0E0DE7 /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 291F0D415C392B5146AB5BB7 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 7C1E3A39CE352DBCED3F1270 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 291F0D415C392B5146AB5BB7 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 7C1E3A39CE352DBCED3F1270 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + EFC9A90D4C4DA1DCAD0E0DE7 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 836CFD5B5891A750B1490B1C /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A24D16EEC9CB70E4E4BE3331 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 83A502A4616091D33488BCEE /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14.6; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14.6; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14.6; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/packages/veilid_support/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/veilid_support/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/packages/veilid_support/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/veilid_support/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/veilid_support/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..15368ec --- /dev/null +++ b/packages/veilid_support/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/veilid_support/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/veilid_support/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/packages/veilid_support/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/veilid_support/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/veilid_support/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/packages/veilid_support/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/veilid_support/example/macos/Runner/AppDelegate.swift b/packages/veilid_support/example/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..d53ef64 --- /dev/null +++ b/packages/veilid_support/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/packages/veilid_support/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/veilid_support/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/packages/veilid_support/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/veilid_support/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/veilid_support/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..82b6f9d9a33e198f5747104729e1fcef999772a5 GIT binary patch literal 102994 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ⅅ8MglG&ER{i5lN315cO?EdHNLrg? zgxkP+ytd)OMWe7QvTf8yj4;V=?m172!BEt@6*TPUT4m3)yir}esnIodFGatGnsSfJ z**;;yw=1VCb2J|A7cBz-F5QFOQh2JDQFLarE>;4ZMzQ$s^)fOscIVv2-o{?ct3~Zv zy{0zU>3`+-PluS|ADraI9n~=3#Tvfx{pDr^5i$^-h5tL*CV@AeQFLxv4Y<$xI{9y< zZ}li*WIQ+XS!IK;?IVD0)C?pNBA(DMxqozMy1L#j+ba1Cd+2w&{^d-OEWSSHmNH>9 z%1Ldo(}5*>a8rjQF&@%Ka`-M|HM+m<^E#bJtVg&YM}uMb7UVJ|OVQI-zt-*BqQ zG&mq`Bn7EY;;+b%Obs9i{gC^%>kUz`{Qnc=ps7ra_UxEP$!?f&|5fHnU(rr?7?)D z$3m9e{&;Zu6yfa1ixTr;80IP7KLgkKCbgv1%f_weZK6b7tY+AS%fyjf6dR(wQa9TD zYG9`#!N4DqpMim|{uViKVf0B+Vmsr7p)Y+;*T~-2HFr!IOedrpiXXz+BDppd5BTf3 ztsg4U?0wR?9@~`iV*nwGmtYFGnq`X< zf?G%=o!t50?gk^qN#J(~!sxi=_yeg?Vio04*w<2iBT+NYX>V#CFuQGLsX^u8dPIkP zPraQK?ro`rqA4t7yUbGYk;pw6Z})Bv=!l-a5^R5Ra^TjoXI?=Qdup)rtyhwo<(c9_ zF>6P%-6Aqxb8gf?wY1z!4*hagIch)&A4treifFk=E9v@kRXyMm?V*~^LEu%Y%0u(| z52VvVF?P^D<|fG)_au(!iqo~1<5eF$Sc5?)*$4P3MAlSircZ|F+9T66-$)0VUD6>e zl2zlSl_QQ?>ULUA~H?QbWazYeh61%B!!u;c(cs`;J|l z=7?q+vo^T#kzddr>C;VZ5h*;De8^F2y{iA#9|(|5@zYh4^FZ-3r)xej=GghMN3K2Y z=(xE`TM%V8UHc4`6Cdhz4%i0OY^%DSguLUXQ?Y3LP+5x3jyN)-UDVhEC}AI5wImt; zHY|*=UW}^bS3va-@L$-fJz2P2LbCl)XybkY)p%2MjPJd-FzkdyWW~NBC@NlPJkz{v z+6k6#nif`E>>KCGaP34oY*c#nBFm#G8a0^px1S6mm6Cs+d}E8{J;DX=NEHb|{fZm0 z@Ors@ebTgbf^Jg&DzVS|h&Or)56$+;%&sh0)`&6VkS@QxQ=#6WxF5g+FWSr7Lp9uF zV#rc`yLe?f*u6oZoi3WpOkKFf^>lHb2GC6t!)dyGaQbK7&BNZ7oyP)hUX1Y(LdW-I z6LI2$i%+g!zsjT(5l}5ROLb)8`9kkldbklcq6tfLSrAyh#s(C1U2Sz9`h3#T9eX#Hryi1AU^!uv*&6I~qdM_B7-@`~8#O^jN&t7+S zTKI6;T$1@`Kky-;;$rU1*TdY;cUyg$JXalGc&3-Rh zJ&7kx=}~4lEx*%NUJA??g8eIeavDIDC7hTvojgRIT$=MlpU}ff0BTTTvjsZ0=wR)8 z?{xmc((XLburb0!&SA&fc%%46KU0e&QkA%_?9ZrZU%9Wt{*5DCUbqIBR%T#Ksp?)3 z%qL(XlnM!>F!=q@jE>x_P?EU=J!{G!BQq3k#mvFR%lJO2EU2M8egD?0r!2s*lL2Y} zdrmy`XvEarM&qTUz4c@>Zn}39Xi2h?n#)r3C4wosel_RUiL8$t;FSuga{9}-%FuOU z!R9L$Q!njtyY!^070-)|#E8My)w*~4k#hi%Y77)c5zfs6o(0zaj~nla0Vt&7bUqfD zrZmH~A50GOvk73qiyfXX6R9x3Qh)K=>#g^^D65<$5wbZjtrtWxfG4w1f<2CzsKj@e zvdsQ$$f6N=-%GJk~N7G(+-29R)Cbz8SIn_u|(VYVSAnlWZhPp8z6qm5=hvS$Y zULkbE?8HQ}vkwD!V*wW7BDBOGc|75qLVkyIWo~3<#nAT6?H_YSsvS+%l_X$}aUj7o z>A9&3f2i-`__#MiM#|ORNbK!HZ|N&jKNL<-pFkqAwuMJi=(jlv5zAN6EW`ex#;d^Z z<;gldpFcVD&mpfJ1d7><79BnCn~z8U*4qo0-{i@1$CCaw+<$T{29l1S2A|8n9ccx0!1Pyf;)aGWQ15lwEEyU35_Y zQS8y~9j9ZiByE-#BV7eknm>ba75<_d1^*% zB_xp#q`bpV1f9o6C(vbhN((A-K+f#~3EJtjWVhRm+g$1$f2scX!eZkfa%EIZd2ZVG z6sbBo@~`iwZQC4rH9w84rlHjd!|fHc9~12Il&?-FldyN50A`jzt~?_4`OWmc$qkgI zD_@7^L@cwg4WdL(sWrBYmkH;OjZGE^0*^iWZM3HBfYNw(hxh5>k@MH>AerLNqUg*Og9LiYmTgPw zX9IiqU)s?_obULF(#f~YeK#6P>;21x+cJ$KTL}|$xeG?i`zO;dAk0{Uj6GhT-p-=f zP2NJUcRJ{fZy=bbsN1Jk3q}(!&|Fkt_~GYdcBd7^JIt)Q!!7L8`3@so@|GM9b(D$+ zlD&69JhPnT>;xlr(W#x`JJvf*DPX(4^OQ%1{t@)Lkw5nc5zLVmRt|s+v zn(25v*1Z(c8RP@=3l_c6j{{=M$=*aO^ zPMUbbEKO7m2Q$4Xn>GIdwm#P_P4`or_w0+J+joK&qIP#uEiCo&RdOaP_7Z;PvfMh@ zsXUTn>ppdoEINmmq5T1BO&57*?QNLolW-8iz-jv7VAIgoV&o<<-vbD)--SD%FFOLd z>T$u+V>)4Dl6?A24xd1vgm}MovrQjf-@YH7cIk6tP^eq-xYFymnoSxcw}{lsbCP1g zE_sX|c_nq(+INR3iq+Oj^TwkjhbdOo}FmpPS2*#NGxNgl98|H0M*lu)Cu0TrA|*t=i`KIqoUl(Q7jN zb6!H-rO*!&_>-t)vG5jG>WR6z#O9O&IvA-4ho9g;as~hSnt!oF5 z6w(4pxz|WpO?HO<>sC_OB4MW)l`-E9DZJ$!=ytzO}fWXwnP>`8yWm5tYw`b1KDdg zp@oD;g===H+sj+^v6DCpEu7R?fh7>@pz>f74V5&#PvBN+95?28`mIdGR@f*L@j2%% z%;Rz5R>l#1U zYCS_5_)zUjgq#0SdO#)xEfYJ)JrHLXfe8^GK3F*CA(Y)jsSPJ{j&Ae!SeWN%Ev727 zxdd3Y0n^OBOtBSKdglEBL)i5=NdKfqK=1n~6LX`ja;#Tr!II$AAH{Z#sp%`rwNGT5 zvHT%(LJB+kD{5N}7c_Rk6}@tikIeq%@MqxX%$P!(238YD(H<_d;xxo*oMiv^1io>g zt5z&6`}cjci90q2r0hutQXr!UA~|4e*u=k81D(Cp7n{4LVCa+u0%-8Uha+sqI#Om~ z!&)KN(#Zone^~&@Ja{|l?X64Dxk)q>tLRv{=0|t$`Kdaj z#{AJr>{_BtpS|XEgTVJ4WMvBRk-(mk@ZYGdY1VwI z81;z(MBGV|2j*Cj%dvl8?b2{{B#e0B7&7wfv+>g`R2^Ai5C_WUx|CnTrHm+RFGXrt zs<~zBtk@?Niu%|o6IEL+y60Q>zJlv``ePCa07C%*O~lj?74|}&A0!uA)3V7ST8b_- z6CBP1;x+S@xTzgOY2#s%@=bhZ@i@BwmS)neQG&=9KUtRf^K=MvjC5JnqLqykCE_P0 zjf#V4SdH2#%2EuDb!>FLHK7j;nd6VLW|$3gJuegpEl3DZ`BpJU$<}}A(rW?<6OB@9 zKP9G3An?T5BztrLdlximA;{>Tr7GAeSU=^<*y;%RHj+7;v+tonyh(8d;Izn}2{oz& zW)fsZ9gHYpI?B|uekS3zHUue3mI zb7?0+&Zm>Kq(F>~%VYEn)0b32I3~O^?Wx-HI|Zu?1-OA2yfyJ;gWygLOeU;)vRm3u z5J4vDIQYztnEm=QauX2(WJO{yzI0HUFl+oO&isMf!Yh2pu@p}65)|0EdWRbg(@J6qo5_Els>#|_2a1p0&y&UP z8x#Z69q=d663NPPi>DHx3|QhJl5Ka$Cfqbvl*oRLYYXiH>g8*vriy!0XgmT~&jh3l z+!|~l=oCj<*PD>1EY*#+^a{rVk3T(66rJ^DxGt|~XTNnJf$vix1v1qdYu+d@Jn~bh z!7`a`y+IEcS#O*fSzA;I`e_T~XYzpW7alC%&?1nr);tSkNwO&J`JnX+7X1Q8fRh_d zx%)Xh_YjI3hwTCmGUeq_Z@H#ovkk_b(`osa$`aNmt`9A#t&<^jvuf z1E1DrW(%7PpAOQGwURz@luEW9-)L!`Jy*aC*4mcD?Si~mb=3Kn#M#1il9%`C0wkZ` zbpJ-qEPaOE5Y5iv_z%Wr{y4jh#U+o^KtP{pPCq-Qf&!=Uu)cEE(Iu9`uT#oHwHj+w z_R=kr7vmr~{^5sxXkj|WzNhAlXkW^oB4V)BZ{({~4ylOcM#O>DR)ZhD;RWwmf|(}y zDn)>%iwCE=*82>zP0db>I4jN#uxcYWod+<;#RtdMGPDpQW;riE;3cu``1toL|FaWa zK)MVA%ogXt3q55(Q&q+sjOG`?h=UJE9P;8i#gI*#f}@JbV(DuGEkee;La*9{p&Z?;~lE!&-kUFCtoDHY*MS zzj+S$L9+aTs(F^4ufZe6>SBg;m@>0&+kEZMFmD*~p~sx?rx=!>Ge;KYw<33y#*&77 zFZI`YE(Iz?+tH;Fq;y=MaSqT{Ayh*HFv0(z{_?Q+7@nE%p?S8%X6c!+y;!0NLXwJV8Co_}R3*7>n+oMsQpv8}8ZS-P@(Rg|gmxZHzf=nMOUAAY}AZGfWVzZjE@4$=7xkIrs8BE%606aVU%kxz_04ipig51k& z(>c9rJL2q%xvU%Zj#GR9C9)HLCR;#zQBB@x;e_9$ayn(JmSg_*0G?+wOF?&iu@}S{ zt$;TPf*Lj$3=d<}Q3o!Hq@3~lFxoiCyeEt}o3fihIn{x2s1)e2@3##&GYDq~YO|!q zUs0P-zy)+ohl-VQ`bhvUpC{-d$lkpML_M%Kl6@#_@A}w{jWCDsPa#cSbWA#C4Sf|*C*&Z{ zz?hOU7Cc`?>H$WGqITA2P~fYudnQHxB8^;0ZFKC;19F#~n_2P@{cE{Czq-#K5L_8| zc3aOEwq4%zL5>YU_mc9fc-p~{fBTWUkxTiZvxt9FOqC{s#TBp(#dWc+{Ee{dZ#B!g zHnaOJ8;KO1G;QU2ciodE+#Z$Wuz*Hc6NRO!AUMi|gov=>=cwcZeL&`>Jfn!35hV1J z;B2@0!bIR853w%T*m6)gQ?DPnQ)o6EtKaN3L;o?*q<83d&lG&U=A|6hcT?f0)4h6{ zGIZ0|!}-?*n{zr}-}cC}qWxEN%g60+{my)o^57{QEn(tSrmD7o)|r0+HVpQPopFu; z0<S}pW8W2vXzSxEqGD+qePj^x?R$e2LO&*ewsLo{+_Z)Wl|Z1K47j zsKoNRlX)h2z^ls_>IZ0!2X5t&irUs%RAO$Dr>0o$-D+$!Kb9puSgpoWza1jnX6(eG zTg-U z6|kf1atI!_>#@|=d01Ro@Rg)BD?mY3XBsG7U9%lmq>4;Gf&2k3_oyEOdEN&X6Hl5K zCz^hyt67G;IE&@w1n~%ji_{sob_ssP#Ke|qd!Xx?J&+|2K=^`WfwZ-zt|sklFouxC zXZeDgluD2a?Zd3e{MtE$gQfAY9eO@KLX;@8N`(?1-m`?AWp!a8bA%UN>QTntIcJX zvbY+C-GD&F?>E?jo$xhyKa@ps9$Dnwq>&)GB=W~2V3m)k;GNR$JoPRk%#f3#hgVdZ zhW3?cSQ*((Fog26jiEeNvum-6ID-fbfJ?q1ZU#)dgnJ^FCm`+sdP?g;d4VD$3XKx{ zs|Y4ePJp|93fpu)RL+#lIN9Ormd;<_5|oN!k5CENnpO>{60X;DN>vgHCX$QZYtgrj z*1{bEA1LKi8#U%oa!4W-4G+458~`5O4S1&tuyv>%H9DjLip7cC~RRS@HvdJ<|c z$TxEL=)r)XTfTgVxaG!gtZhLL`$#=gz1X=j|I@n~eHDUCW39r=o_ml@B z0cDx$5;3OA2l)&41kiKY^z7sO_U%1=)Ka4gV(P#(<^ z_zhThw=}tRG|2|1m4EP|p{Swfq#eNzDdi&QcVWwP+7920UQB*DpO0(tZHvLVMIGJl zdZ5;2J%a!N1lzxFwAkq05DPUg2*6SxcLRsSNI6dLiK0&JRuYAqwL}Z!YVJ$?mdnDF z82)J_t=jbY&le6Hq$Qs}@AOZGpB1}$Ah#i;&SzD1QQNwi6&1ddUf7UG0*@kX?E zDCbHypPZ9+H~KnDwBeOXZ-W-Y80wpoGB*A) z_;26Z`#s0tKrf~QBi2rl2=>;CS1w)rcD3-sB!8NI*1iQo59PJ>OLnqeV4iK7`RBi^ zFW{*6;nlD&cSunmU3v4JKj|K4xeN(q>H%;SsY8yDdw5BJ75q8>Ov)&D5OPZ`XiRHl z;)mAA0Woy6f!xCK(9H2rq?qzp83liZAIpBPl-dQ&$2=&H?Im~%g;vnIw1I+8q|kr! z36&^9}CMmR(U2rf|j12oG=vb%Ypsq8u9Kq}U*ANX*)9uK}fAi8;V_7Z;0_4*iydDxN-? zv?qJ=T*{MzL~-xUv{_Kh_q9#F{8gPV!yPUUS8pEq*=}2-#1d=sC_|U-rX~F0 zBLawgCWy#?#ax{~DAnDvh^`}wyUO`ioMK~jgh%L7^}#h?beSyvQ_g>+`2`}`-1h7# zg*?qJdm=53hwN8~B=^|LPmYtOVrQ(W{sNm4uofq=4P@dUA%$onWbw_m-KWia&n9iv zi)!9#OJ#^}eg8tE{wSb9(c0D^PS1 z9EBS5*ypSiVRS_G0v?$hyoZOS7hFWlp4qbYkf9Y&{%OzhsIdHskLptn96@k6@^K@U zszd8POehITDK+AyW#JKpnWY;ju#MC$JjB1Y*~(E6N%{p#kO+bVxG3X<34n3fW=k{A zCZt|KP%x^GQ9%mU)KE0{LA=vaZvRQbxSlK~eAkwWo2Z<{j5eS5NVTMe`m%re8%~7K zZLtU&b~YDN%~uA9wPf>x2=PI=MA6_oVe>Ek$s5&&Z=8vvF5EODP4Av(b|dlNgF1O8 zy83W0WRdzjz2iNA~t1piEqlyU&`$yZtqR`6X_PmuP>W+D|8iH;FQ zN{JuU#Tz9mV=4R_IewROL1|mK^`lLat#LcIBfggzM(iO$pQT*-c_ z94^LUWw#5B9~sp2W1p`c)Y(xfR<{O^9n4E6vDDw{#-R4UMBKo{>Hqlqn*a9rl_>+0 zS5MwJC~nCC`1X%VCyWFsiDX;bfAJQAUkU#105f_s5U-8rqO}n8fA1{b>Fr6Q|Ea(V z5B11Lo^ooWF?`^{-U#?iatokWI-e$632frzY?Yzzx(xJc@LFM4A~-eg!u|tl{)8Nx ztZLXsSC*68g%9TFu(f&J9nmc^9hgyy#uUOMJFCaifSaDcyQ&6=8e9=t zIFEAQ{EK{|73{($!a4=!wj4ABcQrUQp#+gGM?wEUp(w@+Fzi{!lt}|3`PM%&d-seeR zB$}BrFGD3R10CE>Hsb>;PrP}pd` zaY4}6+Wu(`#uAV+E5SV7VIT7ES#b(U0%%DgN1}USJH>)mm;CHPv>}B18&0F~Kj@1= z&^Jyo+z-E)GRT4U*7$8wJO1OibWg0Jw>C$%Ge|=YwV@Y1(4fR>cV#6aGtRoF@I`*w_V4;)V231NzNqb6g@jdpjmjv*<2j02yU$F8ZS$fTvCC`%|Yn#x< zXUnP&b!GLpOY-TY3d?<-Hhxom_LM9`JC9LEX2{t1P-Nj%nG+0Vq)vQwvO^}coPH-> zAo8w#s>Je^Yy*#PlK=XDxpVS~pFe-j#jN-(As&LRewOf(kN-aKF(H+s*{*!0xrlZw zchJu@XAvQWX7DI1E8?F}Wc8m46eT+C<0eXVB+Z^(g=Kl@FG-cn@u$suj)1V2(KNg_ zh29ws6&6(q~+sOAoHY^o86A<#n*?Pg2)cK$+y;cY$hJLq4)4V84=j+3ShSr##Tk5kgmxB zkW+8A1GtceEx~^Ebhwm36U?oA)h)!mt=eg0QE$D1QsLNZ_T3NH?=B&0j~#298!6iv zhc0|-{46*3`Rx&nKSXnf1&w-Rs>#PGAGuY@cBTU-j|Fxbn3z49S#6KBaP^Lx*AOXxIibr z!1ysMi(&kr!1wwQB5w`BDH2~>T4bI`T1}A2RM0zd7ikC&kuBRsB`Z2@J!Udm{AmSN zrr0k6_qCZL**=)xRW`MFu(OY=OT;3G8eF~ z2mmkXZ9X(sjuKmq+_<=LSjphB$~R1o^Yb=rO!j!(4ErIox^x55o{pXSE9X$!76^*$ zoKhlAX6y%n^U=C~@!vIlEgXQGD@>oOU=_(aXF-Sjas*$AKESfRzxQ8#3yOj|y0OCU z>6Z-0%LCcjla&7I+CXm&caKp@@jQ!5M`(_{CL=@4#JJ}cHeZw>^b6fpv269LSV?gV5Q{kk?4;;y9RIsy5vk%DIRiL(9xe1aA@4!VX zDh2}xgUd5X?6nji%&7-%QuyKSYA-Z{PwJijUQ}In+EJl|x@dF1P<5bPa5W3&&?^h$ zZCo8LepKo0a(Fsln*cHL;D(gu9MMkoiM0*n31u)jHqX5x^F95tnI&^}^yKx3YwEm@ zo8?EZ710ykx@19{=yz5IXb8w4yjdveWb{IVL6Z(Cs>!a_0X^1E27o!4e&b43+J*u2Gb(59k2uK0goLwhO{ujLS ziI9LA9`&x~Y$6JNX!aEXR``}LUI}Gr#=<^wBHmg%v<)zRWDVtq)kT$-P7iU1R)2XZ zi~bYhV@EZ`@prgK(cs{>2jn$pxg$<|KjJ7%26Km>%KcXh^bU@y@V_Lf@=j1x%R4{v zOcQn{I}!2W<~08FOVnoV>zOTH=+>v9!jFo|q)ucqIe!N4{U5_G`>>*sVD{8I~4FqyU8imZ**-Gy`~Xd z4w35GMf%7^i65HdX{Iz|f2Kg193#KhPIeR)-=eYx3Z!%RM=JjwLrdk^B#6rg!ym2w zPbFqYyO4>W_Z6PonAwiu7?!h=x%sR-T+_*xZOGh2wWhWr%}%2^$$ zQvACIB~pi=m|`hXIMvoq`TOCx=J_D2>pi6$NPy3&8#vy|oX)=kM0Z}$BR$r0G}MzOk-OqG+VmZtOZoj6x4(tLh|5h) zBv64Y{DPHsy&_H(5_l(&Y}FhVvr9m_*_Q~Zy-}V9+VmGnvndEjYW4qt4K~N&Y&6g| zfpz*V=A#^mVmuOAz)(KVI<%v5NY0%Goy!{9&o41upsPWk(yFuRP|A4q6NMnX%V~MT zi_Rb-Bno2kI+j0Cw`@ydy{e%ARS#Z%b6I%_yfo_ZKXr4BLVoHzBKJ^ZG z-2>2IzU)55@9C|?_P$ew^-7zEiAKG1XAi{!3h%1m#9s%^pGy6S9wKFYY4<$djeoJP z{GI}Vd%idY$4_fh(7NXm7#;cC!DS&-{tGr!Qze{^%bUx2jgG@-kMta^q-EwrKB}d8 z{%FT>rFk_bzW<{lc%eYlrsiYTZXGgzD1&lmRyp+c1O=0=zAX=KV62bx-a~JP{cPF4 zU$-XT#(9&T>l@bMu3nSr{)%-5lV+0t&bxip4DVJ~vlL$J2P6X~ zd{FS8vm{Lhrieul*7&(AgPuXhjpGila%6_?-+k#b)cdk#M1jB*nE>G6NGOr+Ek{`= z9b%S1`$`=g0CC$>0$Db;l_szReLYVmce*(()9%Zz1`*fNXhI*oRlerWHarD(v^W^c zuc1Vuw6Gbp7ZsoRH>QGt#&lv;5G~Ovt$%7VFd*-rN2>UjbOWBFGNGO`bru7CFB4tn zL`^?69Lj_g_TA&`9`dSI8s|)K|QM0 zybvV7!>xDY|6c6y;Q}qs`){1+WQu_5Dgd8Qe|q}}bxjH+joQQtqs1IVZn6{e7T{ia zF|=^xa%eWO%(x<7j*QZbcU_;aVaVP!arexOLOtoSNt*hvsRL%}%)jPetSich(`b-^ zMZ$PM9%s@%*jPVz0Z^W*cK_>G4f}+eEVX`HOaHg#!B`<4v;x}zDLMR*M27`kNfp!! zOfdt(>k-g>7jf^{Se@3$8<+;R*cYtw+wD_Z8Pl~!JDCUEPq{Ea*!J9`%ihyNJZ30i zmfve}S5<$Uso}_?SuI$ks|{-ddGLu9WR9`^9)Kdi@Vs;x#SY-xp}wHPU0|vEA7234 z@BN1z7OF=OOQtPF$4twn3!HTVlUVD_)ubMM7PEPoiC6lQgL2q9PK4~e8v-OuH%lie z?NgBLkIdPMG$QBq(>r^AOHB`|*1#*!2Z? zuU8H|FD`OBRu^(R?Z-Vhr0j;FLpS~a34KREnd}B=EYHS*>Hm+f%tgJt!4J8Q`qn^4 z9F=tO#JRJ}tzA`vx$nZ)O%wC?Uiv0+_nz}5Lj4ki*&=K&*#U`=rv z`Q@Q{+IhAj@6lrNK2B=8Yln!O2%zomfRehFT~;!O@(@Xy|1Jlw*uOB-M$#6K^)QBm z_7%#QVUDPwnW{iOV-grMQQU|3{=BQMh}c5(yMGdoQf*)k9-B zMQ(^GdJh+y)>qJprknS!%WxqM>HlHOP#7UVdy>%PW$!l72J`n-p7j(DBKoGxXWh(Y z>BFDZl|7knU_jg_SSbvFk8)39%2)Hu5W0}HKlh>EaqvFoXI&56Yy)3) zQkE4X^P0QnPn?iUUVHJZXzPp`s5uv?pG{K9IgGoHvcmlBxubi|iF7n{)mhenIcxGs zgr0OpQy#Y#u=5lOyiECfE_Sn?Fj1LyoRKcbTgX{p<T*v!CGkPc)pcA2D=4Ekp0Gb*wpy7S88C%Ywsbr?MI(3UdsCM?XJ1X%*hNjB)XqZ*W(qDdtSb z<3XN74ARXL3=c^bfW~F%NM^5*Zx92>Wq`&M625p~j$8mYwLbk%Kf)jbn#<2z$%vP5 zy#b>-tF-S2_AB4;R^K&^-1LJrUmi@9rB^FLF)-k&YHK8P+k@RCJ1qSTZ@=kHxA3l$ zmK_ZG)l6(nmCR1a8|;QF-B5e_ELnjJ1$m-;4UXX?WytF_wz7#&AjwZYTMVieLbq@R z3t-q|G4^BB#EpNu4uyfDebB+-uu_$9>y-dzB30Y9F=R zrW-Heqnj*InPTWHgR9v^R7~hokldh&h8=HDhMW(EFfim1*{)5Lc1-+eBVkK-2!u=N zuZKABgJs3I--NbjE;>Undg6uK`^U>AQ6V zhc!RhYgvrmeGNsftr+(C<_MtuV$`5RZTf#5r=DR?gWG->#})#=(td%C3`oO+2B7im zUqY}&a_QNTn?s+?=mNXiREN%x_=(H)L|DtYPY>SR3pQfBOel7G_jR_{!9`dSj8Up-`JgcB;=Oor)U=_EVjF3C5{Sqh8cq=~bRjoBpoc$kJCgtTyZGSpQ4= zYi$6b$-dGmuTDF&@amhV?cU05g(AZV&v2$4m&j_~GZk;&keSO(@LRESRZ&p`dV*6w z2$em~p*8yM6j;SYorw`M5K2mluJq7P5Yn$VtZj8DEs2Zk=O@4T&Q}>~f31Z{uk}`E z{Dp{KObh1kk~~MfLUod72{Pk6G@T$_0_N??lOrdR=Z;VV#m0l)&@hz{Z?)@sgImi-&i1@95g53rON83v!yVPDHRU*Mzc4yZ(-Fr z{8{WXmIJf7jeswk$;6s~Qac6QyM3W&`}m#gRt=rr95A+Ad&wSAgvXZ|F))rBJVJ5W1CsjN`QaOzct2ocq#0!v zmj#075)C!3oS>&N;aHS@<+c>RHL)8j^p)k(8#7$LEx!1g_1^02!4_qA=;uhKW=+ix zGX%+vBMiRiF^^jm{mdO(?GdWJ#unO#_F^7mhT8)s(z_WlwFyJ#Xh)k5+RG2f;LC*K**1dr`#}~6A=0B=I&V;%zDA1)d@G!X#Rng)7G*2k8Kg447r0ox> z5NK`d(H-afBwo9feDOUi>;BbPsu!2|=@g=3j*PY}@YrOb+SX6?#Yb2xaaK!?>SX1J z_!VsB`2n1=wwSftkydm!39|-1?c%Epx?TO<(#GO~I&{f4+)XwRk<7RQ1~5>QcKH|D z?!}j1ueO0Lk;FZ{k4FA_(S`Ot0w~tl&m0duID*f6RY#bkw||o;kZ# zISYNTb|{~|X$m$Q-Jv#uxyw)eM0gIv`V#wOAp&Vv@>X4_tSZ&L#juM@$S9 zx_X_tLh<_^-F;LAQ09s@sPb%PMTrcw*HUV0P=RYSlM&AXEOI&&R&YCm_S<7DRBx^L zA^R^iwW+LMk(r*$Pq-fKU5X@=mQ=`ErO30H@@&qqnI7zJcrbSh+H<V ze&7Uli0xj@WrW#&-9%*FP~kPYF_YYM_hs5~|ExMynQ%qvq`leRB6W0yhC@pCb8>_P zlf=F~WMv_u*-DV=UaVu#2rlzK{q8D95VwZrfV?gj@rSNWXFvktUq)V5+YrlxwX302ae(;aG4e>L-M@3J+-f3IT{b9l!kg*2M zC1+ND9}6m^()LE87Mt+^Q|)!y#suc&v26C=0W88%a{?)E8Yvo@kM&KNMaOst#|-_CbUTm}WS@-c>nRb;&z^ zYr)+IE$1=jov(CZ%3uR+`~NI>1&Gs6W(jaamjcN$a`2!*nO}l|b%?)Q%%UWzw>A`C zR@px(P*7j$TK?jbv*%x)e^|jcLsv}aF(Z0=7(%Oa7+1wY>{B>d+i&ZA$}k(qgZPZY z;VkW~8eWnU&HPIAbco?&tc2O1$6=7n{u|^Y*nXoac{o1W-6aXfy~KlNbJfLoq~6;+ zDYmnv--Fhqrl+UV#k@_(1=gWNtqhyVKN=9CZ-{Ohi>e=~bm4IKbhM%%W zW8oXE!rGpV7Wt(_^4nndH1_imheaWzDi|I})9ZVZ9>pN+P%dVc5wG`Ze*4`@rjn1^ z`ln(;vPBHQUb}y8S>=8q__r7g+=z$>!pReVB0@XKchAvyGjLQs-u>+w%`frV4FeIG zj=7n~hGrwx*&5aHy(7X$bDZ7YhcP%(*>G^lAYMK;qG~V8Jz@b7oNg;IA1z$9@TbzW z;@I51@Ekef#qbxnG$Y8Z%bm~ibZ=4#%yKr%#b)CDrfKN`ujIY?tA4h9)i~dZ4E;ZM znvb$n2)zn$Wx&zlW%mJZDh28ox$@%`w3i7YFepXUChw}$UXKI=-TM51`M#FH=tdr*mQ!c=aB1296Lu>iTTKZWss0f z5~ihdImPN$aTle_AdbYC^31}_^EK|9R&l#%3hbx;8vJ+Gp^tm{9JDILu*1PW!rh^Dn9p<)h#Sl4kKM%nm<+!ESSk* zC;lLNT$fgr-!+{aBsSx$41b}yy6o>r3F#1&iv3cfY2N<+`0qJ+>=&Qxs}JOEkD?^l-F5i`t5+zNuvJf z3Fh4$mNqiFXL-aq4U4K@Ae$fq-TDT`rvrx;gqx96w^*@s=mcthCaIyPe(w)6kI{EqV10tcShHU9eeAPs)s?6#vrq}>y3FeTJu$Udha+z zs7}rmA@yR(L&>35sNjQqrw}o^)UitMU!5g6nnG)(tgst!^`FKJEzI1(d@j_w@;^hr zgYxlIRYjho4U$bhczfq&YySCqCE(5_d>l(4tk1v9!V7PB%Vx{QO=G2NC@c1%3rEzw zN<6i?h;CJX>h)kn49Sr)g#Em6km6ESP`1qc5C3ZHizN>r>V-fSS=X1nT{+Thh@kC! z(H=PlqDt7V6gOYezXUK-dretz!1?IUD6&eL2b!4=9h+HUO&DYZKMM>|YhlEEg?q?S z^XT4$2Fd|zT=x3U#L1|F;-#`to-Y6hiYkWdO=rRC)meY72pIfl`3zEGDU8($iWR^K zI$nq80aSJII<;#W5Pj>^_T&013BJ*O89Uoq z5>;Paa^E}xar^r=!pexg&OTM8wluk4R~Ru=)Hgk`Y#i_$jk{jc8hx}?(dW*X!l4vs z6_%$s#duJJFmaFc-5#>v6Yea=I~)s_pXGS>Tkz?s+WS}>Qp<9MappMLXpkXpSM~SmH6u)`Z5>o02kJs;w@KhdiZ3}29y*xr|6tMo zBHzGic+b+dTd!xOJ;p{Rguh^corJ;K?R6daayQKm+0rf7|AXg0qs!R9eS7t4{G=fs z1$=?kK1Ih=gEkI>@jgXDWHZt*C7FUEWs|u^pE3Z``^K|1KEC^sbN*4nQUfRc_AyE0 zn)?RrGjgPkzfE~_s!rDB!fDsV+*|kEX4+DyS#8%!cshn;s8svwBXSsDGX2ZRa0={* z=`p1F{zD17*Rk>Uk_cw3t5j=9-d6$}MoM~z{v{t^M!g75-+o8_XkP@CZWUQ2z!^26 zCNOu~hgrrK)y>bgqb{`Q_1^zrG4;cGarP!nb4E~(ZKWc`LVeEq;IewVneLp^ZU2+% z95PgN*M5v7Q;ZlGvM#`&u2NdHm%&gZ{bZM5wBCp&?HeZhwU87wyT_z!n4z+1?=RvXZ^72d*%+R1s1$KbAFtR|= zw;MEq=O7pMIKpFwKH6$OOszJAf<_Z<1)36cB>D>|Z6$gJL~jH`n3MMou$#Si%rDAu z4pSkJspG|^CJ86vg6kkfXsA_`8@8iOryOe!Qhn8SV6}mPlof3=WJRVqAr_b;e->`Z zMR(p|K|$L0^6;u~USxg#B6-ZNc%E1dv*^P=|2k*^NOBni#G%9Y?##{=)8KZwh85OL zSBG9|gb|hdmY^gn(ziY&O5#@I?W)W;361Yb^VQNpz0A7&^(7HRAsUvw#)fvhocvja zLxV65J0_$>&cVRctJFsn^qLos^tG`+B0_gQ{NeOwKt-!C^gGFufdtPT*Vi>l#X1|V z2XxsAcixN)Ekq=a##_^=k_^BFH5_zpvPDRP>u6+3$}i&b zy0@FdzAHw?i9OqnlTts_w5D@Nd#eM)KKEuN#m{|AJyscxa}(eA?z4&4yvXo{OBS65 z-?gW;<+;+ntM}U_yTmHm6*2zj0Imj<&ZgE9Wj|gfsXhrVH-c0p$7HXnR8bxDYOi z=_r3FA~u`L&2;Vir8}P3)k|@c?sK1U@&iWo{HEXcoy>6wQSuJ+b4l%aTBuigs&k@Y<2c=S3Ef?p zH>ki4yDuXdo_eu>X1{E$g(Q-u#zVXN^&%70guoizo7x(kQ0OZ}H$O9UB}(FaX8Ct1 zFpx~}EbHf2r6V;x=@8GH$C2|6*?K~?LrtMYd^bw*WYXhA z_))@RMH;nZedW3+qfWbv<|_#BYOxX^rhbN+!za)|!|8K*LRs(R$O*2SDM{g9k7e{u zN4VIdi}e#0&h?sBxu$>Yy%)j(k1V2fuhp8r!}gfF@b;F?U`6}YnnMh1&sSU&lR^?# zu!61+lGsuFEfDraX3+$QZibCbKzc{75G^T7@WZSQ)j5898G1AOXB*H*TSd`f<`IK# zm1%&t?i|2Z-a&r!pJehzg@!awNp)R)aa?q_SqGrxE5u+T#f?K2;GAHV?O&>!W@Q*k)7=g2vDW+7K zbyY9i{|nOF*SbMYoRQSAbSH2y$bE5(@d6xKxcF#@TE~X#3o=;`0sc!RupdRmQsML? z&>SCwS{FOpSr+@6Uuz3m`hj}(^g`Jz|6?({!%WVJn$H|ugxW+x-GEA?J&U^ugj3Nb z;65~)W<}iH2PJ@st8LtLfSOLXYgj=9<;?ih7rq$bXW9J#!B8!Wu6#U`A$wlcoC*&` z_9Js~7%m79#+edeT&P`@_Ng@e&5J+pqpx%31tAF71)pcz~-yJ>P5yX(nuM4;bUHDa8E(~~l{j~JeCGkX>nHJDpgSf&bTHEf)qw8{Q~CBPEVen|MW2P3vmf`8X9-g|>>ddp zcgfjbl~(?3Wa*NzQH>4nsM$3}Ul>pX1xC0oF3TZXe7=V!9!n?WgvH|R zpbruczmB%z=zkZ>=1R|gXwGThLELqD5KCUhtiRGT*JwKIvzbzV%ZU!e!VcNHSSX3> zObH|oohc8nvQZ2}q??C}@>!fe3gH+HF@4(qWqi>;ag~md#D;cl8&gQb^?2a@5cikT z=7r78@&5gV3Ggc9f=<<8v~yz`NcEGvbX1V_`IL(&+Z>LB zM~$ok2qXzod@1$TEl*U~H$V5g$er{Uj^($sWb7Nr{gsIbE(`$LRGECTOraXiU%=uq z0zvpi1S%)RxTjzoVcR4#10)fs()4Mtsa@e?9j)Bk!LsYyXIZga2q7d%`vQE!V@<1Y zmkpH3LeXJNO9f7l>F84g;huc=4nk(UnU}RLZmYk2TtB#lv34K(?8~gyx-mN%g=U44 zOPdr_!j-;IEbe|l9-buuKEy^Q9MLjSKG$S6dz)!U_32{1)N}L)3+COmlg=nY1@od$ zJ<0z-B%sisAR1yh>z-RfQQb6M4i-d#vxvb~f69M{JLPZv1JSCh1$gQ*LxOF-tH9!k zbQ0ZW)S7)qCSF|=2`q_A3}OHBNBueZwTTz^ar~gz#2KA74&&D)KHt~m4F_nK<^*7_ z!!pN@xiGkq%>1N(rNxw$zu-=1t*IpAy$ z4~dD0w%9;E?(greVWZ3(o9ux`elM>Rek#0 zO=#-(4p5B+wFzlEU7^k{3EdL6sIp|K*>xrriI`}E8ze|z-$YpN`^_teL_7P`%e>IN z7tNiH619P+0Q1hBR|W#POOta)1|LkIRtgz zMJ9VOxXN#o)mlXS=u%`Q>~PBuKEmOWsIuQRp{y%!ty{fEyL0gV)$LQeL#pqX3L@SR zJ2Gb^E9+KVd?;joVOXlGie3?z6>(>u(i!(qGz(W( ze~^xj&IRF<98ypEis{Y_FoHn%C0bW(XeF#Lj=2WUEBqKNPPFppEH?_a3}-h906X}C zSYKcZFU`Om5YlWhh@ogzCn3NvuM~F9jOX|xe-X*!YL+#ceh_tJoHXz`aTnvSrOAZ| zOtdGz?QdT!oAJr3(XL2G(p%2X4{xEohU&vd_zQ(U%ihHOlKPWnb$&YYhx48?|R++>`5?sxvM?!;ru|9 zZ#nwuTK^S%ce<+ggdJBE&fRrXN7O!{nu`%q`M{2Ef_+IRad2cf01P9pST9AOK>y75c!9}~)Et^6$`&Nm{wzWcm4c0j9DF!xJTpGrMp3esI4D_iiDe`sswXSu{dQZE_`^A11 z?Z@Hw=65mVu^%X`>;$mciK}XiZ{xw7I_!t)S00^JuxdCXhIRO~S*lPS(S^je`DH4E zxbKNs8RL`N?gCQ@YSOU=>0FE#Ku#DRO7JA&fu-X8b;3!^#{=7`WsDXUxfUsE(FKSQ z&=N`A7IwLq%+vt(F;z+T=uZNl=@K4|E%p{p^o5(BGjsE|WOR`%8+XgGW8xJTFJc4L zVY#L`OdnSM{HyS$fX1)3_JuNNH1aDsDqi>CzCT5=kY5zV<~29bX)c^I8R5n&ymHkx zj(QC4t#mDK;2xi8O%V;C{HqDQeM64=b4@sa*N_K0a&ro4+8LY6cFHz< ze|!g}zF|tDrP=`+U7KwKl20gdW1%!iN>1=uxA|NZJ2peruBOj?RBPb~8G;s6xIi6- z?_odhafsxoxiBf zwZZ)c*)FLc0#wE~bXw0TPBYl+h9hs|DYr_B4LR_YL@S1hQs=p zNEh%_fUvWZCbJtaF#kP5=(O#{8|g&Kmz1&8{@Lufw^DhtvKx955~aqxi2C=)Z-!Kd z+m-u+#^U4(HYn6a1w652kO0bYBt&goyx(n?MR^kI+{Q?0Y{G~W2) z0dS3fuJ?SU(6ZDp=kUley%PK}K_;YQyK|U|?7t9SHiyIfpT4a_kUVIhH4PSaj@3mo z`z}|mHhx1Pq?@(3vTBb5HTXuFAzFZEt0D-fw_kd=XvwIUh3VXTm{wbDA~cESd5cI1 zd>6=&AvG3yu+)`9oxmfrDQ(1fzv(_0l?bp{a364dXLRRBI8kBv!KsL;brY)#E3`o{ z3TlWUsS0{Voci?6MejccG9x_KiqN>So*1{25r6BSl9jUyR}1TgXBLL7Pr6Wv~Nu47;fbiU7TbL}>qmtl36YSZ() zVf@nqW(As~#`@bIC+AxSw!O5Pocf&rYaCFm?Jd?XR)p#@{!|5^Ws@wd855)mI^8y{ zws+VvGXW6%xoj@JkGb=~%oJ~7m6+uhOv?bH+jJJ~eFgp+}~*^C+3>R-MY!IZQoabCh( zN(T+z@Oyc^C)WqQESmh{d!!T8zS(!wX=R#hEKxMXy(eg zZ+Cwm1a%?;RH$h2_ws|nRjn8ZY!>3gn+6Ep4xT|AeFox7!rac2Lw?jsz}JqPE?5JG zok0}q1P;cuzs%Yrze|&d$oTr<`Lx{fbq2OV=!3v-ODq(n?|WxuhtmwJBIoW^^FB+D z-?Ok9HBKc5@)L(W&vmI{prL?4^OE9TR)bELS=<>*w%&aKjzi*@;5#P3moG@dm{Eke zhE#Is;&=o|{2GWai}7LYEI+gmc^Kj4K7w7n)+9godg?yB2?xs}pF1<*!Sv?D~Uvbkgs9xx9s#6zBv9l@ox>d#H6eqw^KZO;Vg}h!q zI33^$4}yF*q+q{DsJsa(SsV!YQ#zi^IF9MQV6i{SiN4dWWCi%YQ+hNc1r!^+<(YnB zG62-D`M3w3Q2;@X{S`n`{QO>migDpz0FK`->sYDOESs6u>-~<}_XN_6><2g7U#XC{ z$#Ig;n{_yEMnlvx-lP*;ts#DHV0r8j518>~33?Ak#jocW>uk>6V||p7{4rov#RS9c zdPD6r`qF1om9r!zS4Jk1>7fn#GCnmD=JIt1Na`X)=*LP7R!3XATgk`;&U*P<(0d z9p<0T&eYqQ9jot39FxpfuPSPYlfQ$s-*;+c1KL+cHIVcG5`H~^Ryu1Hk7%Nf$TCwR!SzG31@NHpm`mcp8v!wyWM49TjTxASJ-8JP*MTHLC}hF==PUOh8kaaXeGFGd<|e29vSDaS ztPeu&zv0^wN}Hahi`$pcDs~FVt2F;K!q}q*Y@{7i#stWfU`u2La4aerBKhV`^zG~j zJWvtZpcHIP7x*tfLSQcng6D(`HVp4=LWp_0Xt=2wEHjK)!DSz_Z?5J@>awRyk?azj zU-kdSs~cp))*pfJ_q7u`IsCq8F|OShB~D56S(Mwwlt?{yURE7#eI&WcpVq(@9Fd~g zeUiD!a4w51Nj(YzLnau+O3MDub|?loF0=<#jLztAM>PruE7yNDD0L}y=Ayuc?^?Ni zf~%GK=iEhn2}xKp7GonJx!JpDmDsco$|$XtRdUDwbM9$9s7x9-of2nKNj~?b@UOKz z9{`=Irz^ba-c&1vSQxSh;I2`cKc8-4)aCy%#bam;3_8vSJ-jw`_}lyukEC~z00EbC zI*dU3F21A)dSZr{qA5QF+{a%D`h#?8o%M?)*hWxuqnQD(TpcmfNq&UN$BmB)0!r8) zxno@Q?$_D&*4(rW6b+?-Y^5|*P`DHmJ%pI<6*yP)o}2^?>d7P#bd2j=vvx2mfLW@R zQLD`%buR*}nzNYNf%68w-D$7%v|=bXg1mYrdZy~}(@RRZ-U+Gx=nmCjVxr5Ag# zLw3R29-MHJl|`mRxj#sv@EfyR#-q>BE-XFEENbV$#dWM?!VjU8~kKZsd@G=HPrI{HiqN&j<92*-3$^M*;n@rG*i! zvi#?j;lc5w>@+r!6*CVUrN9as=S3?(ZBT979$5R#ZpPm?2VjIyQcEFp9orGR>f;G? zK<~FiYY6ow-&}|v7k?+03TC++so$)2~rN``u z>N%j$AbNQLX_!evzG8abf=15260vIXdz7K^a$YS)iw{@x5<|Rr#ii|ov=LJ{eu>dZYe_ip$ZuzvRu1dpjQK1BvP zH~m#t=2_wy>9+YkdNF-z` zQ*#7=^r%R*pIi2AI`>n9>(QJVE1k8?Ilav<)NUjW^O$}^yZZ{_Uwn!4Fq1`aslX;Y zj`XDIm`E1sz|wShA=?a@ZGKDSMU#Z3$E!1nZ)g^Eg3ZDoSN6@RXrGVCHvMIauS7d> zuJltXf9)LdTWdF!n%-iA9b#2$W#i??K)zYho^((ZqluvhAr@{H{diy0%@-~VW zKYC|2Ma)2^=skdLT@ZVqJfiCDqS@~qIGexL(BKy6Aw9ch0hoHN&E+m3*uka9+AIh3gTWdSe~W({-&^oFw`!j7$DcsF$7`pO?kRMK<9h=SV?cmyJIe`$4|zoI(6u9#qY9zM?#zNe^!Dl2>Z^dH`>`wSY# ztU;V*+g0R0DH6EnJA$U{QL&T~&s{`smeC2I-5mzv=v$l@iF;yN0hMibU=CG^e>J;+9k`Si9PzLaj$>}QKI6lWmO_o+_( zmhxA*0|-Na`+*J1qEMIXZf9rb#;pcOw>EDeDjb!|GumQ2!1ac;YqU|X;F@l1_lemzTN0J|U zFJF(kO21aHg)*KfuKT=BA{VDkOvlx(b{f|A9D69_BHUm#S$F>~`Mt@GesjLp3;reY zP~q>6Tt;`XkjqV?i7lqPbWGh`y<7dq<}pDHl-dDA4QG6`QDq)+vq_&HfW!}P6Cp4d zt>Qnli5ri*I1ILEOGD~3Y!@2^Jmcy1xDXmKolC?at}_6;neEfca0rLHT}NLpoUYh` zDbCtfZnYN&>}m-(F{5d1=)bBuZ?OcP`GmsQV@kn%JMJUIep`Avon#8=ATpEo-@hg& z12f-)R=HCD%pUjvbWa|P!}u)=wInpZG*LHKrZDMeC>Qils^IyY)x;kDRs4c3!DDOG zAptSsf#1X>kSli|Qka@S)6O4un-2aKL?bcV;$*>KSxHovjrfZ^-+c#>;(42yj71K| zzRyFiLrwv$rPcNA{mtv=o(*JDA0kS93>OE0D{KMJzLk$cc_5dCLWnJcFJd6_>BpE< z?aW9;^!;arQcIjloW&YL+~MkNO&a>N=pmhg>{SM<@`a&VeUA`ay*P@R$_+WS2%r?_ zs&Z%c`>ie+%!I=Lz>$9$7a`-`hoc&*dl60^whsaQ;~9~@JYn1Oc_bmgVVyAzUOYgZ z#j{`#D_YZ)(wa5;qzR#zo4a|-ANJjBB90r4Iun3*BkMxw_Ti>SjhktsmR|BPCLt>9 zZ_3eQjweI*-8+HNt)$9^s|+10w@sU!PY{`#BnF!ULS=#{k0Zr5`yOS?p8PfWbKT`6 z@T+PeRJ4`fj5t8bMs)0>o9|C>mBTlfQ*nFG#Rri-Q7}E}+eaz`LmO!`Y_pHkoAruu z`&!5VNnA3IG$}Pz)V&pt&AF!$E{J-;or3vWv3&Sl&9KzG+ae73Zf}=aP*SCI1{?0T z9SAC)W(?DSKOkcmW$(K5Bl?c@(5#>J#j@eq#ctX~$TIjkl>Wrfv%Ey+bl1Z-v?NxJ zwZ9!ae-MsHPUx&_W22?9$mCE%&~lzVG?hDXM%~gXGk+Q!Jf0BspkMWxy;^!n<6JIrSYjv z6F%~$8)0^qbUho9Sdf97b_n({$;|XH9-RHrohHuPcro@03KEPFejN&q?&nJFoIQY; zSI#uL6>2^^yOR!51OLO65xGas55dPG;3=uQ35ZYW04#+~byXQf^7Vq`G z zKpxF`G*X(YOz2^@7i#D+s-~A1E;3&x%%qL5hkiy^JhYjJ74{hvVmAx*6BH`M`!qGC zO9pjEsR)A-n1`6KLACSL%FS_Kcm+?4*z-V?WAZPs?RkzoijIr~I+oh1^~T`q^dCFvG$Gbd8AnTYBjLKYUmayaQz#S1le7Q^Hyr#;X&h*1wDpm+gZC!rSKom zq|+o&UGpeXtlQ1;?@JukKG!8PGS1Io0z6O}ZeL&DsON^I0K+>Mxv#ohK+;ByAZ`Eb z2orY{j0Pa3edA(#-pJA0AaJ6h& z81Gl(pd#j~mrizktoid14K5ig7u8FvZmLLP%l@dl05IprCyqDB?mA2fc*6UB+49lb zZ8`V9epdo=OeZoiY%zw-w`8DNwTORV_>>3T{r)1-YsGSo0E2s>tix9OBqKFBjg#}G z`pgkCblKMYs!Z)r^(qT_c+}gLhR|gnq!1~Qr|~kt&2@_yswx{i$KEn`8J1W8BGljl zr@GEG#W(s#AKKyuqLp+cl1C}7%`m#-!$15XF{M(M*-fD%+i#mFbP35jlgN3{8#A-dmj&OQtG)!031jTwGMal=&YtPfq2AUWekP9J-JT(p099!L`+yen$ zVH1?kRrhV7(mGKkm_jPP_U@Xd;x=ppk}4WY0Rbr> z0MJM_;$GGxL*P68y%KBqHntF{>X&<{aeI4m6+{TQ%~Zp}v%Pujr)zg5mV;cFKqeA- zQm5`#Sd{B6Rc*4PS-rO(vf>YEdXmOK?>K@`L5}|9q}#t_IE%g+U<-1qw3mr5&v;2A zCQ}BEn9_u;;>n5N#dP0RhCF-_UplC+U(i~Zjh>U5+b8%@p3HK(R*IMQwE!uritb}< zF)AK2?+0@-aE3LYkg`B*&N&m~JWB9>(Z>`aqRwgioU)0w{U1K4?>-#i|ZfhNa9hV)2)(%ch zJMH1twoeZWwkE@I!dz$ma+;9GeACv>Ncupl@+gBSeU_uzfj!$+h&@EACkZG_vwLGA z(?^;rcJu1$5H~xI@6lHIYC-$+b&hF1p`AoAOKqw{t0Fu#X`OGt$)7Q!nmJ=&)xjq@ zHoxT4pcYKSPT5(4yzIuQ^S*N2NJpR4v0?rB-^JuaXNLis?E(l>Jo8mUw(gsFLLOy? zEszHWGaCn|lw$LSwoj{G7Uq(zK0W^VVWu#ms8BMRlF2z%-g`fOXmndgC(na8fc)s` zz$GAoxP+l|+T_S4$r1sLwkV77ew1Gug*`|HiE*?FGLm1q; z^p0A0eqqbmk3?|!CB9DBN1Zof6d7+ zJSn!`VD~tVaqy<*Mw^8dM5v3Bvj2VdVFb=)U3L2eDM3@>n(P z?Rr_=I17+r4fE{>1LBQG0&o97nef67n-aNnVP<{dd6*B!Q344 zZbsAof&jw+;CLeK2d87t9s~YZ5?6Qwf&{NPEBN+)LbjOcZRXNcR&h)x`TtdpI+b!>$E~h0o1L*2OddpR9!Gw~-E^Cj(7i69S<66ak$)AYMv|xG+;uR(`;h zGIV3}?+Qxdjz)s;s}jHY{JPmeo@-tN$H@hxaV@)}K?y~ts~E6H(F|SlsN5oH8g7*h zGiC!8c1doE3U|D}Vul1yPmXuCk*hmyU4MG2ml#V0+(G5I+`L_=3cD$%$I=@*8m-LU-!fn&-sZO1%ls63+w}AiAK`Jv z>`q~ztr&&(gCkFpci+*1Ekdv*MhBCzGfPBj9dM|YEjZk(tWBuz4?MGeq+*)t>Q=z6UXF_w z{QDUT4^JQ8J%hW;d2xGB>Fl4Y-bRT!ttP2GE5jYoI1e(eVK0&V5W+>zludt=nf|UN zi1IV;MK$Fy%$yw<oGeW?JIGjmfGLH$Y;l|T0p1V!N*Jvu zHSAG0WpwPip0vm7%VRq8$2O2>P5b!WBfTz*6dZ4Wd6O9Y(8A;nOuG((y?F`ac_u2( z#~17CoTK)1G<~~Z4jXlout{e&nZbDHyHf(=a?OtaJ(2Q(!g#)Ugw-QQ?A?mN#yN%T zBtJ`sA6Lpg`k>Pi8a7GssiY$eG0Be8LCoQL{GDqi-;j0pLmT!Z)szldvbN7GVcu*S zzb1rEq|M)1qa7rM*I8!<#w7FnQ?{v^? z0`MlS3+`#ZB5$DT4+`7e-Hlp_2G0`*F@STbRJ|!tk3cC~1T%NR-p4s=sTT+RqsMjF zyrp-Jv?CD4Y3N&Zb1gr=%`MFR8;|r)uxQ6*X{OpEhQ~+tu}^n8Wijiy`pSMw0uKNi zSNX^Z1y;WirM0o_x%zft0U2GcLm_2BS`b{Z>g|9VOVr%QF*R?pTpiJsEbj4jLVAyd zTA;x15=f~b0^(e*Vo;Tn;WTJSxpI9LmL($Lxob<^S!k7mGhnnVNnAC*g!$ms0#Q|q zs=25I0<>fUw_&+KU`}5P9wlmjRWdMYh%Np6n?AAHQ;JzG?s(Z9UR`pNh79Nzk~DF+ zX~jy>>f-2bl?drlM8 z3NfIQnrT@pLmv+QA6efWPv!sqe;mh3_RcOj5>Ya;4hhN13dtx*_TJ-=kX_kZQDkPz zIw}#e_dK%au@1*L&iUP^cfH?zf1iK)tHv=t|>-9mMT!;;Vg|svSzWkN7q#t$c4N$Q;tl3EYwef_4q>GO<#I89VhY;`X*hz$n*GZ%f+;uViG z?uLlxD1OIeid}0r9%Ssoc7@vJjZIsZlU9zvYpjhYiOrzD5sq3OC zpf-X;Nb!DLpxqX^zDIK%=46-Z3%i-bac`RIBS5*wcw5Pu>G|kF>TQP$dGRYh#1hwD z{|cbbTOKL>Gb1-;X6?vWLC+KJ_^Ij?KzJ7eZ?^8XNgoYU9^z&>d zsIjX*uOK`#Wu!`>L@y!=XpQcW+mBaRjm|XrB@etLdr}Ob57e7EkE;7a*t7=M#XFL6 za;KHHk-rBNTjp-gS^;ehKNv>K>+_jPQ45J%4><1HyKJ?;T9#~k_23?xD}B&@Wp{%H z($hU+nWR?g!9dsJkgVz(J_Yrdns+m~9V_gQ7Sb`&F4wZZ!k}##j$>O{4{?avCbCZfyW zO$)m7LE=P?$CXHDU_RUD+sYwT;nKI7 zSs_XTv!BuxpJ!7(b~uYfsgzt~mj5(vf2r~`LHwpePs!o2A3zEr@#sxo8HEe8>V||d zBiz0@e&6}p*}!6jsm}I0bN9Mc2(c#jg@;Nu6!Kv&4&P8-UcQ-00WJIO%4OuUn;^jU z;I3r=T3KQtiMQ7&x32eVtB`mCe)9ws^7u%2P`B%Xc}=Qc&O^{FmS^{~Rho}^s`B+H z=1_T);9LRK?{$Vx22!5m)Er8aoPOA8&{7fyt`t@~Vw%gtx~+g3qs8LFR%(2Uny28A6dFYnNQgcUa>Sq=%alFh&8#@1o_qgwve* zVFimnUtL{4aHP6s?FB%bu2SP=e*VGqXC8iuZ-JOc{5%Lx0g|VvyWkdh&FD^Gkc!0N zhoolXvp6GC8wj?Y+V;r*EN+<1ac`-+!8Mqb@Nz)=OqV?4gxhR^t7*+^+AfxxVt(n{ z+fkk|-xSGqmkZa@Q%`;;r`-Z|? z0fR6b@l%pTwK*@xY+(MwBUwf^z+F*~piC64BWTrz}-HS1-XF-IA%?Zs_#F8 zcmUuEZ6Of>YIJOe$&{V;3vIBw7|jSGPeS6cvTMdj96Y~pI-z7InGW;(DhFqaiTTO9@KWvQi9__j0btLZ9 zAa~-Po%^sDFfme4@Yiq}r`BgnYK2eTwCjg9_zC4V{{&_GTm-!qHGVR6JXDjw;}GzF z6lXA{xo1+tQM{9vwb1&sRXPdGDHbEMbnwh}t+%tvcw5p4J4r#hEpDl=A{;Mjc%0)T zsG}v<$^HhdcE)5IJ^iBWK{7?Zn)vb%c!5eIj4 zbT}CGO*u)Od@^LuIC@_2{=AP2-O99NglFudj{!T}0e8wtTQcB@F9QW6$J!0Ye`T+U zXDx84b$!hD#4YzSyZLy~!IIZuFa3%eU zG4eg5?}sZ6Yj29P^-PcXG*8%VzLL$0!oL?c(!oQ+G!kORsa+lsf5YER>PX83R4LgF zgPNQJ#Bo#)MXU%J9k?RWD;c>|as5b5p>xAwau=X5XbERX`_ZHB8_XSNDe`s?n(e>) zGF$G%n6o+W{6A-@4hsIK0*J%jpB#Y*G^B48eQD(CDZR5oBl-P=)r7fH^PLf?!aK6V zwkIM35?l*I6p@;^H}JIDNs-fF*IFN?k?kj(M)QKM%%?dSkf1d$Nly2z(>)oq8z}0H zH?Qa{x&36#W@y04!9zx@x7un@ob$&)V8#f~0n1|jF0kFs4aZ{ND1~QjWHToIY5)LY zrgKDCj@dFCx&-w$QMi=CqD*=`$NqC~2k366pPXl#>Y7A=iQD}f`)+B-pS@LIW_M?9 zlBS_)(vGz!L$#P`?<3Hvonw@B1uJ244y)M?0)z0-hq++sJ0GZ+{oiiH;lFi&wy(C! z0Bv9z^M;`4@)USP)7dhg@K5K&U&|7&-@I0Sk>I+ZH75_xEn>qh9qmc%aA@NEKBsVBgUuK zC=b{w-0oU|)~tAVI zyJ3BAB}%rsjz7qZ?x_XCWe6!_u-{e_3u68Asso0IvwKdxq1lN#%4w>J zi>}P;$JZ>58(ZAjsmSJl6BWUTe`0eGEf3f_yS#H6vx;UJWO7CCK!{)4C}`C$j5gNj|k znb$4QRurEE3tPEe!JzG-a0DmvXePO zSD#Q-qOAjTMm|=aBSnvwHoEbgyVIz@J$hT*legak-hhb}e#%cm2$nR2 zV9A{kc)WT$np=5coPQIskbGMO@Fn2NxPv$@SJZdG6}jV;+%(cH+*RFQ(+DjsJlman zy`D(yN?8MCtjWD3w}Q|jQccb$}BDW%M$zZZnri2+5ls)@@(wQD`jt_GpTKL_^CO&SSCcHbfMX#JXYFI^*947 zPh&S-G=l*C@`E5CU1$m7ao(Q&oSmY7)ZZ#5_fEyYzLsFJwJ%GfErFeRN@7lUbUrL| z$6;gQSNsI91LJvT+$Zb0>g<4g8T{B!U05lfKmoSRH^pB^^8sJ3{8PzVq0NeypMF5k zU3qOqksdq{>AUjm3O~dZx^vS6C$ldgCWszl?xd8-sJ;-kPnISB*-f=L*8XggOx$?u zg%B-QovSjBbj}%sShZv~r?`*6PiiQW;nee<-=+y4}S#}q_BgXIJoSOf$YbE7vXt4;Np zrKzZf6Ny0aES8(-cqmnIGMg&ieYWryBZ0VTB=4<*@auP4NdIk&q(Mt(OLPm|Yl za!0OpC9sA#tk>OsaCSx0;!$5r6naw ztzLBo>#LKaxxsO=yWe%yGilL`A|6E#TK! z+1VRQlo*D?(k0-mlRM+`OMT8kVB*-%ZGv}Aj1u^j!wu*~>L<-T+u?6sX!3C}lQte- zk(6_=iwXsQ0JbRvJDwMnk!c99w~s~uD_4vMB=m~-ft-*|z~$*g4g;pgG~Ap1m@@Fx zWS)8IKSN6`^vVQ8hv^Oc+O(Rt7!U%wVsGP+Y6fyS%GG+v+dIdVfCXPzAV~~li+3m5 ztFQmbE)(#2#Oi@k$1#zUS6ijD_yYsa{+BHZAw+^zAEI3bc(h0qm?|pNf?oS}Km#OG zrOfCKn_-CVO;}DXu|5YE#d8I2o>}vUxYlv&>=+I28WY>a1;uI)HUM_IvpF;Ln4ROT zf!=1rpKihNFUo=R@sD-pT!EOm%%ncl43f;aem^;|A#s3`b6vjeAzO!M-gwc`-Kj~{ zBX)tq64*kJl#TrgW4o%hTY3x$P01nD6a6s2#MmwM$vyX5PU|YngU*wXGK*?f?#Eg$~^OWW3I@of-=XVuu-b%A1Z|nqY_2 z;~jD&=QnB#WGU>;RwFq(I< z34K1fCMwf9F}G%k(&?~2EY&)W*-_z0ReS$;7+I1)zz`)M zpAF{5ZHLPMJhYU z;GE*@hM1NM{G{L94dL$!Y-h6A9K9W=I6AYb`Y=v{(tpyLQz^^Aibea(q()R*TU|-m zozpyr!|-BZ_Dn+$*2|vq2Y@ghHo!-`WjVtU-bab(SJp2*2i-}$UP9^qnF_OIFS~-< zYj^VS!)Wu}vn6!LDIt!HJ1SU-@ce>z8f4cT4R9V@O^Xg9)4`VpjsXm*~@%l^Ux;Rf#Zck`BNXu0Y(!C zj%Z}UAmD00nsOS%Uull)dU(fZgJ$bo>3Oa`8h~Wt)EM?v(ndlTS1p0|E9Pg>=&>58 zghD~%R;YpqZAw;F;M(lx5b_wkVbnd+ER+6A-SYj^1XUgNGn0I~ES|f|5emjyPIW)S z0z8i6)BZt&h(qQxih4HbFYa6~jyeKbc_`QEdLD@9SBGButjw|b^l*oQjDk<7Nig08IK zb`ATVGzK%LP+>9aFM0hr8t+m`uNr?h&8o3Rp$T&ql||K}7GgobFhCViaDH~+F#yC- zt>7T3&_PZ*feTKTyd6vlF~JmEA1f+*>CCE4ex}5N^$4o)YuxX&3T$P0(IS!+kan^J z_p>v#1J8bWELml|S02YAQe-&yVew+kipZr~H-I@yc$=8#rZ-8L<_nDx&Qv3dJDwUX z!)@=h1`~R2M{$J8bM^1O&Gy2oxe1T;K?NA{iv_eYuhpLyc3%xu%z`dVc}Z}%cHGHQ<7P!Q|e?dwnSpL!AUf!B^!?#^Q#W!Ry+7ofwPZ1mZq z(Id0{htmX1W?2cAYWZo_lOtT#+Us-nlP$=CGK|Ri4x0Xh>(|iN9y1 z=9y26A4Y}ViRi9Fxzm{>J`YM>GX1D|$4BY9xJrY{oY2~Z&};B{Zq9Pp!pox`8e#0C z-h~@fohA74(#ws!{7kIe4v6XUX<)9bd)g66Bz%^Y4p0~OF+rY;l$v&7T<3~4y!bv> zR$r#LblZcVgy2lq!ff+>yuR4qCcljQa03x|dTcG7`CHcxh#POtGKt6ymNd_0qF7Wf zBj_KC8{jl!zZ>0neDp19n3sD?HC=|WM3!}cK4zCnu6Uoj*hbV1<#F2BD)@A~y%@VXx+u}Hcn=_s-({PxzmMZ^xJ1SV zoZMY*FarYvO_@z8Lr2ep)%HgIL7rhYa~#X&&V8oYSw zA4m{3{hw1Vb~~26K^xro&e7i9eg^SqK0i}kG3z(!_~E?sjJlSWIWXJqKiHAWTG*SpPcCMD`kEc1gx`R^YkYWz zEN4vEIkj@&e4tC!(_~x`-K$w6CU%X7U2Y z)Y}T5stEyoSsB{H{+xfST3tov~6@lO}2gx#N(rHXiOAHT!dp6FiV8V)B4{L_P_% zmX0rPa^-{1xG6|#uEGo+!v)QAOjRe|jg2ICcXU!|Cr+LMbLHlhJ)ErR*P9*z$NLlt zmYjAUbljq004ZyOco?HJovV7M*Wb2nF8vT2D;3kGi%F)6Kr#TVW>}zTHnUQxoGmD0CY9J`|d%8@}n;_co2q zWr98`R_c@PQbMi}x3bWo4XZj{it6qYj+o*XvNoS4>rF;7WNn;vA*|A!3H}Wh-uk@n z*hV0S+XnX;K;BOoz?&*9_{NnM25s4^^QUt|>R!()^Z6#G3OmL{CU^-IG_M7_a~B+& zCrV;ouC1ljbK(K=ygqAE_-}ewnH2&&t0enS7}I4i0wJgNvCf|P$`|DHku`K`HfDa2=n@DCg8MRi_)vpMR2Mxy4PE2Qe! zD||kNXy=0WeU(43v%md9Hg9Zu#CP%d%C67gk_#pfXs8lf>M=betm(}0fdDKq0{26# z_c?J!Cgo-~*=wswLXkR|W8d+rDdV00`22Ouv=_Hod9bmB!=D$I4r@7DZX7e+0tO!9 zR{0d}A6^K#yRx@ykotO4(WUJsmFvN)d-o-wZ(wcDSUS`8jO-JSAMa4y@MK4fDP`(P zzxQ2})ofiauWKj9{Rm$Yw^?g=?`oO(Vf|T^I+-A+o1#F`>tn59d=FtgVJAV=y;G&` z0GMvtEeil5;e$Ln8-41(UeMl2kYLk%vPl?0+Egg_;g)494o5FsvdeZKP;&&fjw7o{ z|B+e%Z|)8Ts?=>@p|hr!nYXgV=ZjI4Cp#$E>+g^6r7Nd3<>-t=G%B5IyZUI{e{49G zqnIXEB=M@5Ndf1J#l5YWcLG=A4ufF8S{z5Kz-uM?Ni{{%mr);=l0=473h#cIc{K3> zZ-VUw_Ng5^HgWQhs5tQU@qv-YBej9`R$a^|lknX<*+sSVXue8M0#EPBJ6_Liwl*8l z_zoD#!l%WIXJZ$jm?|zUu0LdeP&8IW*(|39&QzKGnem$6--u{ZGtHt#Hro*h)?lu zXGKo-4Hv1WP*VLj;uA6UwGSV*6ro%PRbwR{@tXoCOb=OFTB4ru-|Id!rP5Y6LF*-D zy|t0qDSVPo$ffyoj#CIZV?l3VsPRYye$F^xxv~Z78_fwlCWbwW!nYCR2nx0_+@tg3C_UDMVa2Br=X3hfP}^Cp4Yg=#OK}K zKYVY`V9jEKD!UrCbSX6Xym2T-cg}!n;?;o{mM|zWj0P@D|FO-rQ zKt#ApEh#AX%_f%9!G6`I*K=bSnMIhQ%W5&BOMntzVr*eS;WR;FgM)+k`#+Vze*z&V zkU^I-R|!Nwy<~>eeQ~hJqa2|DdpX15kD=6U73Du;T|VarycBP^n#IZeIJ&H3S9#@oec~poZELqX$DAc>XZyuIqd^GK0Jq~0kI=d zA7gMo8%zmkEdnqMh)tkp?V0I;Tm3`>aU3^~dXw zlhdd3=iygnUgYu#GRhxln}4D?Gokczq?T;RjCk0=fUHy18$lt!-q!%sNxee7No^+N$9d?Es*``)0UJ4SC&FNY0pf z_MlbGdUy$|F}YDvJ9GTCkZbsNKj3DL5;=BGBx8xI;n)=A0d0j6MP7Mi6MQdk@Tux2Qy`oI_&*%EQ0bE?|R>P$rDhcFa8O?JIK zPOpFDa?-L*+Q7RrCg#y5z$l0d>n@+OYo3g>-Z*x&`Jj5|=*UOYaJer6;FAbdtt0O? zrFGUE?!XeUG}G8wMgeTs%+r;3uUU;Nq5EuU{h-g&UOBKhdS`;J=m!~xn*ztv_p@dD zR)tR!P=~5kX)FRsx9)uyuu?0dh%Ht7`PTM@e#Cq!z2ts;O;L)tQ1ipDiWqbGz@o_p z^D=UKR#`S7HAt4vQtD(_SeWyj_av~#tJKlb9>-s5Ykuzx_E1ZNl4)~f=zG$*;-y=T z2ozmFva9az<{2&63fQ?(Q8{IPx@t1LuFcxP-LXVctWh3AwazVTt2)w^*Zn-#eB`bD zSHoAusjOBK5(>uQPGj=ijdOH3jqG?(<5#C{*JQ?Lt~@zow=Ii4Al$Vr!#+Cf-gx)A z`_h(>b@7?*6bYM8%628gGW^rwWoG$mK_eCk`}B&llStfwHf12*{5spmTeNH$4{gCY z@Yuwr*k@%m;T<60bw9z6^WpWi@Bu^qe-g;YAzI+VjgsuZaGA=^G*I{KLy@rIjSpWb zFQNsCp2T;S$VaJtZ<(waRu8y7^X;>YhsWp zM)mKgCeE@K;J4vQSV z&-(Gl5AJCp>K*2-`U|4i;u3p8xo6(isu-38>cY zml1Eo&FBBKJpour?}q&nggpFiGM%m+YX`ng8P+uRnJiMyWcv*_AZ8KAB$w;rfmN8C z<-2EB6TqZO>A~P{*<);wYqZgxQS8E*syOXvGkGxF@s(scud0uv?T)fQ z(DGrwM7lvpitUG~6!*}kZUpBn9PuP`5^nMK@($xI^0Q~axP5qU>L~uF{R_<9&m z({}$$WuD1y-QzMVb3jLPk`~bDJNkw(Dv-6cKUb4uzD= z-w?i0NZ2K}AbT}Zi^uOZ32xmSxJw+6(3j%a!~Tdy-@RxVx6YUw2|V6JX+mSJNclfl zF~SD#eo+lnB=ZpHLl{)E+`sI^-V1Vn!6#Ml_W4aH*Pe(++sNI`M=5L3?X1z0;CJeE zJiX5Mp6JH*=R9W0t(1@>>1y=lP^F=yJil6JxU~I}EpTsBx?rJ5LbCbQ zuLBmmX1MO&!E}khx=+#hCesIB53`IWwqyFtR{AUv7vJ{Q^dn1S0@*^UOmRwctFy&> zd={(J@avBzmu$MbyamRMt_$kfHY<*v)%%&nY4hUDH=$k)$8LHlUG0G3Kv#T~-vQjw z)hXbsNIg?~b-jRw)ir5Q(gfwM+Zk+0haf z+4ER%>T8RnKAoJ-(s&tu&-iZ@A?^J|d z6md=9C4am*v2r=aa&a?~37bc($n#wQ<8UGXL+!RtrRXGSj-2INJ#+3J=}e6nOC}G8 zN~lvCS@rxoq7w$CLg-wx!%V%ymw>~xhUw4cADX*$A}D~{21F$!Y61aHwpdL!QcrsN zl~$s5kk%7HWHkZ43%mOcwlk3RcbKGQ*}K(Fxput)rpE0zH0vY(EyY=blQZ`odG#hD z)~{&r6XkSE(^csqsaMm>2c%xsT2&g_Nab1bTY%fIoNHatDY@C@Ei~v@19|F?szU6SWRS)uDXqNY!48RlAb;S*ijqus; zp;bteR835>3BXML2CewOM<^q3M*ubU`}gnI-oS&(vf=GF|JJB-inGOH_dc1xb|iqR zWgrcNy?1*8)vAlAaiBE%K3Q>5Ygy-#Wf$>FqL|Kvgb&6H?iQC*Z|PN)xZJhH#d#=a z@s9O0oea6Lg}submzNZ{iZ*_okZ$6G*h5YO!dE=7c4=YA9g$y%1xjkVl#|1DShEjM zH3(sS?uRfB3mhW5Wrm} zrY>KpBxM&CC;s5Ie_{o}upN{vdb8x<_$5iiQN49`z`+Zz`&E`yLAim;X&}$HAfKmT zkO2Dgdno95mWMH~h2c4);H=MigT8hyzl|4g;dU7F;p^X>w!fa0zf{^rf?>~ z0w{=F_R}ru{g5i@&xwC%R-!-1x|(k6pSb5_)$f`zyErIvSCs{z`iVvU4x_znFKti!!av6BkRX_=+kEc;*`_rla zB`g4ruCJGT3XVTTrlh3Yj>1>PNIy?sV%Yo*=qaBIOY87_?P04yx6TV?_{~K? zOHEo3|2EA2JAMPYZM!H<{|!s-$r>l5{19icxV`Wf-{<0I>{v&H4FZaCy$B6Ludz{v zRH!!HV#JGP?5(L!Zp#}NlOODgWqjO+yo~+LasPYxH+ht2KjdfCFQr(oovP3?vkFK^5FvPJ4^LD=DpYQi4tUXuY1;erJaBQ79 zHcp(>mKvoD+)bq5SX9siR>(%CL??*D>Snn%p}NfGO4(RY^puLI+j$Pw)NZLb5bKo{s|0L~ z-A3R~;QHMg0bHSgESOM&N&@oF4|8gkPF-nVM=sQ;d}wcS{{!iW-)yQ``D6t#xlh(O zRF0Z@O>0uMz9g)u{P))ptV5lH2(gC8I5i(FDRG5Gp1bgBydKgxJy5gBfK(#D7NzZU zatG}S^z#KL*Do5=K*F7hk(`mbdgI1XoM!8*-};#UzNtEG@Nki#`7)GfV;VlfW^)=` zBaAjK5>gx@wf_D!B!2C6xBK^K4%x|+#?P@5N7tlfWo6xWJD~Wz^cnPfFF($Ixt4!j z9%x^1$on56XZB0Irm^kw-*rd1YVO;(*LbB21@7OPJspo%WO676#~oUMws(zP#+shG+$ns0IC3W z_{kYU>N5<_6=j>*0d}r-?8U+--eXfy2M+opoYL|=I932TMp=&k#tzJ^72OtRJ8BVOvTYPh;@EE=LJLeOk`y?d|Dd9%fWlhON^LnB^6x0LyZqz@imyogJ`$C@Lr9Z4o)ZQz>NCavG$$@e2#r3 z4I=}I5KgV>wl)~_Ja7gLQGju0c1{h%cV&6c`doWWv$>q*=ZLc8J{hBiKXNK?zx2Nr zz!pph;BLU2OaZTv>Pzj(VpSp2&OWNCF<~>NgL!nezhxEgj;&2 zl>z@V#>sykFCnFL?|(j)J3SFr|FFa`n@KbhC2pZB7 z#3>qIn&~mG_Vki=p8_x&CFeD4V7MvgJlk^G7H;(apFxr+7Gc0+1KfI6$@aeF+d7DJ~_-A|H=0?Da#&^Cqb=!=fVz>giW5nw=jWQBS%L^t1EZ@ zCm9;qlG{($@0W3T&l17ownc5pWhfM8Mwn-fLtb7H|IYl)8@QikEc_Le+s60x?&B*m z5kObB5{BD}gGr7l84~vP{N)C~3V;xhBWd%=^j0&KBw3T3-HU`;hqWA3OWW~<8nl-M zfYn-BI0_?g`3$_;&Exw<(G{QM|8)Kq28x9NF-F$>r@_BO)t^T*i-U1bX01<)zC_uE zR@8qEQQ#cm$YbXIUPVO?z7KI$pw@r=-V{V@>dC9Hn==1QBVy_b;#*jR+&f*$AwCl?o&G?2Uk4=*Ej zFK^Yvw*HTO9n!XRBWe++o3)4O!OC9PC=_l_<$M(W8(Akk`zv5?nJifb^rH3N?Hhio zo$=nNmSEz_QFHj|XF!vQEcdqPyZz_4|M_GBH)k)KA9XGRlTJD;3*y1c#?ZWkeaQM* z^`Bf04#Z)ARgrE4rMmlk8E5F=NpaW8xKNd3)-orW$m+kh(W12jQbQ7oi z)=#qbmhkplt}u`FC0sV9sdnb5$E!zX_xlA{4wW&j0*DCm`=1;Sh_sB1xiH@C89Z93;8d)EUk=lPNIZ`o3H`Vd+Ig`=CV}#?PAXvzWk{x96fn z0(rYh<>?PJ>Hd8v@c8=*vm+)>P1k@i2>yMaKw2nihLV6Z;wcdc*E2{8=xNh(FkEe3 zq_pc;ISw&}`?lqKx<4vIa67!xu|P}G$c3MDyg?u^InS?uM6Zzys0QM9ChW>g-ypzA zkOUSfvhTTWq{_>TJ{+kpgwX{@>P5ptiJ1NTO5)8 z8BiLUY_!*AJ$V386^TicK@z0qOPWP#Ea5?}!$_&fQ zOcRKuR^tLX*&CM(ahYftiNg!a=uU|He)2nU2(~iX@Yo|foZp906;o=d%aK09YEW7_ z-yX*;XE#z@?zZ&fQ?2fYX!T8@-$(K5Jo+AkyOM+(944x4B%2NR&avFFJY^9_br5UtzSX5@gmYYm@ z@S$jtqFn18bXQr0IYhQ=+2~ZDB_DRW3d=*B+3q`-*1P$i!GVIG(AMp=vBQ#^_mNxp z(;4Iz#_~&9jZ}}7oW?R;_x8&h?b0N326NJq4~>W^TeI^!o4=G5G{|9ff|`NN5+?ns zL@IWva(*@PXPmVGQ#rgIOY*nnoqNDDy$hd2uMT>wBgzg>YT&BV2U{k1ah1(1j_v0` z@o;6~SUGW=!+j!oa9ko_2^G75?VolPmWk=Pb-h{k=phZga( z88Rp7QzbHkpYG!aug9e^DF63Bi|1#CeAW^CpakO9DTT!p$yhuT8Aq10^cl2O@Zl-2RXr`+zCPj#_FqXs}W2{Qvn2Y{BmNsG45? zB{BF_rVgT$u0 zE8o6|@C>uOK1Ba}!V zx!M$9J1B7#_JSs90cKlucib?T&HqQpLE9YV1?v{gh2NWKEt9FX8;3DePnCL5Z=k)Flp=?-i$<5H4zc z`?2ZZ+p~Y8FYr;m3Vn2(u5Z`Av6#S}zkpQpZ|vNP0DY^I-oa$HXzg+ajQC7%wldRN zfOAL!UwFtuphqqR41v|3He4cQF5;UU9M~lti-k<HSTs^#>-Tf|C2&~#m%6WZAy1jz!Q_-IbpZP z8ht8}UG13lz+N-7+01+RlE)6OT^3px7fn@1|_b7^{bhPet}< z_)77(<^>8-qQ2X(n4faVhm@T0@Z{5HFSWs~EDXtV@7IAMbVUP6;v8^%l3PZ#wOZ-* z*Vk4lRj6OYpAZ_$*`t|tYKmLar&&{5{d+5cst)rQTn`n8>Xi+0zXc6YbTPMgzewFg z23F=+`8=FXXF6b*CDVN$v3|6iy;TSFSYh$qrbhKDcT^U9l zj}3g#zty{k*>s8S+>t|cng#3@Rz`z}njy{*?90mV6_Mkvv=iL9pb0ttHf$7;TxkX1 z-klTGb`2~-Mxx6~+{b-KiFd3XG`p?+6-0PMorB#Q@TY_CH5)En#5WrmHqj;@Fvi1A zeGpO@wuYIPOgRY&02e-U+j7!$LZ#5mS72R3MJS^gfheL5`kQV_n{8}KXaj)V%4b~As zFrQ7yZal}~{ELX@8c#V?2LlM@)g(|;VvcBjEuTJ=`WkOem{DL!+7Lr!U;F!mGm_^~ z+V^T?%bz+8noq9{ybcq16Gzd^fS2`skac)@6|;8X8l6Q19epZ@l^3@1ES!x2XLNA4 z_FI8#x5sq7hXVr83D;_5$sU!*Ye}zyx1wMC?Q{DSgrUx#fM?_Fj@{syA2x2yL^J{S zPPLkQ#O+9E9a^H*USdriL6rGHDt$B!vu~t7^)@_e=(<|SVd!MenX48AP(Z$4WoC9_ zeN;I;hEAr{ZvB^gK*1AWfI~5H0a{Y#2UBjn9`7;3JDrI5leeufemoZol*pDlVTSHP z3#8@6kxsJwUFg9(;)>Xm!{nsFC<7}Xwv_?o=eP)$>vvvj>yw z=YS7{pIOg(u@mJ%G0G^TM@L6>l)?_{_e`(yLxmX%h*D zMJS13@e!}HFR{?GNtq;%=4#zUgfFP^$g|Ax1<`vC&qIPbwGNo}3>ZM?=Evk6r|J&S zi$UD-za)A$kcqu)8)1mG z{FI*zS4{wM6S3;RP-!$0&8!6*;>|%T%HJxZt}cmap#~4vD0Pkx22gBbPo~=2iEMFa zSN<~qRz>jf54?e)>3%j;Gc6C1_YO0C|CDQDt7+bE({$0($tizZ)xn2L?@6_ zR3$`yiwH?E%X*^k*^oQ=z!1GA|E&fXHPR=rIEGq4%0=SGvror2Y%k#d`aPmx5@~7a zdkmPa1d-<`6M%& zp9rn|?C(5SRowEcasXoE$)s`=GvJk9wPt|2VX31T2F}6x3#(&IMqZND*a1muBh9?X zX_HSLo?$y$a;qFx^U1W|YAd%)Gaf|AEHqZ*{PW96FF*&nO-@c?c6t5=K_z@2f$8<^ zY}d|9NRviy7sF$61>@bV$B3*VeDg4DX3qScxVTL~5Go^T?}aG+th- z2`EduJx~ZcSssR;yX%oW&ze|$TF?;>HGHp~Eq?$w&SAD?d#s$$|4F@l*T7}X$7>}7 zRvPwxrPaLO5X-qYiQ7{P^4Ui2GDbq&DJ3Yu`)8zfMi1{>HEq`+uR1bJ4x!#n0D6_M8Zs_# z3mc%u30aK|avL-!XI&?{^%v4OXUr4OzaL*|-HV&M5GPx)SUqYMWw@Ex;%DHx^&FOD zncjYHD@AiYbGx1O(rsKW>Eg}cid)6bqA}!r!G{?x#)c?^k+q_uv%Xh3ha^A^{%wnpRPY({1LqK{NQy>!UjUc8f7x2` zgyLiGpsKlFO75ee2#drn3Glyna)PvUP}e(t6P z(8^W6g23+fzT5gZQQ^L-Yg#^P;QK8FTZAe)*|CKS6(I>8a2aoN+XEkYf2jAF!Zi3! zjS($tF@bu(ypeC>`IZtF;jz`F6A-Y7ZUQBuZxp&q4zHb9cc*!1`T3p9xL9`nWhNVr z!2lf=fCA>;1E&E|yfmrHqB#XnUCu28b*4#eZ{lLL(42#`ui?BO&uZj|d_Fh!Bw8g$ zn@2uezsJz@^XM(T{!CEw+EyG*eaF`FuTN%C zOZg)khBpDobCl(3ud$bhr>EdmuQ^l^Cic|y2m>LM+gsZGYKUAeJE5YUX9}j^JDoojv<}Cm&t+agmp?JE0%d#fo}m_cYogpjn5&egilTvDFz-Df}1i zB4)bXfn$dqb!cCa13DdCgMNehaa&${n5Mw&bxeKfNmHq%e{T_H@WB!H3QgFK2gNpB zP<;xkez-y-Lr(0^P^G!YH~WLut`0=mPXbVN64iv6Nd`s=eUQ;?V((+QU0&B4SF3*{Pm$AVrq;v&)c>VLy_UCe45VEsI@ZWM2TaB# zRU6XaLx0^H=0)Z!$rIu`3*s{Z!W7pU@6aHvX*vUuzME+!B5H}k_gFD)3=f;nI zi1|B!@iO%p;L{!JSEI~vyUByf_{HY=;RuAK##-h!06XFwxYi?xl}oWStJ*P{OcVe~ z_v(y8!+BaLQB`(D(XrL0ReKMn$R)8mU2@$q$Pq; zbZq-$IkP4V(`m}e<)cwnZLrjiA-X0@VY~Gi5-PKX20#Eag!JOw1br%7Rr}`(v@d!u zCo@&wE1SwM=zt~$K!eJ**9GAv!}Cogn9(d0X~BwPkU4gaWh?WVRcE3N?C%_R_D)Vw z(YmJTJ_0~fhItqHPqoIFGQYE2!~?aSRa{vjcDWhy5>oT zGOMFTWfL`aLx-!QL(9r?~D6y9Uhq=af8z!rqg#p zXk%gE-;=@G>MUv7p@P#ni@zP*$YQwA0Dlc21`%pV;p!_F@xI(^eA5&SZ{rU?^Wj}! z6Y%C^eMYilc_~MAwqV`h=I0;WA)MqJ^$IvyJ-O0)*RuLYjTL1TWd|(NbhIZ;nOop( z`4bc=fsxaeI@zc!vvYFFetFRKSMjef2_#oIzzPIxZ4oB0sxKOzX4Wltz#G@LD2Qr5 zm9o~xF;EU*_!O`}IigC{sU%1^$$B@>Fa_H0*>*1Amc^7tnKxcPpr8zZTme`6(0@J| zXfBE;0)lcuv%tqq05V8P2B^)Nhq~qdR|1KCfe>(GeuFaNc)T~zvma>o)FZv;sVD@D zynx%jpd8m<{zI zz44BQcmN85TNhy2plu`Nt$b;sKELSBpW)my@*ZnL{lFaD|7-8c-;zw*wh@(1yH+~o zQd6mwOU~P(B4CS|mX=v+F44&NRvMbQpcpDmU!|BhndzGgrsa}~;RGs*v>~aLX|A9$ zxrCyC3y6ZiciVh3@BH@t1LJY%FM8{e94DY4JQ} zYS0fcOC|N!{@iq*a@H$Qe9ONriBWJrhLhC?o5K2)!=~i)0hGh-mMd~RkqdIGCB(fU zy5*IvHssJ&gxudt>g(3w2{)axskJ_#h96qTc~<{c!`n^f zg+SOfdm8=UI!4%}d%RkXd}yWU1H66h)eDTsQr!qkcZE^zbI#F$k(dn7l7z}@YSv1+ zIcEYw{HJjfg()x7R@zQ&o;LdJ2vi6Fkl?OHM-Ga!%w}co(6=I5LZ>n{9pr~6!z|S$ zq_VfE7##n|{H(t$wPI-D`~L#((@V(MZ>p6Eb8k%4{lIGT;hZ9cg%~HhcbDCd%0RbM zs?uZG1wSL{Z0f+NzDiO?w9~XT^dWptKJ@M~0(@5*az*ZgabU465JN9eFY7vD8Wdz_ zlAIonnlivB;uDXov3sIgoKx2>G6a;@?v0qg;r`RnZ{4wMw2%}(e*c8k`R7sNT@>H} zfUU~mHR~8!4rJTHVlT=v3wz2kx&95Nz?@Tj8)s5E}t{|AFA=d_Y zOTqb{ATx>U``k~NJ2hYk3r#Gn1}|1Xj}jq!9%;{k(?9!WZt1z#{OATvapC-}#$LWi zi2R>~v0v6A<|?Eg)Ye#VyRyr7RJ$N4vFEFfmb1jHF(yZN^rc!ULDen>KWu(D9Z5!P ze(qg(G2HmSqyi2B&W`vo@N=3l?+dXbWn-`1LrY1^_mSilpKLLxQp}@s?=Tqw6Do5Pui*IhPZtaT|GAE&MF$;(4s9Bt5f+vbITElRv3( ze&@3GgY%ltiz;PZXq||TeA+sP9bc(#*G<2ck&zF3W?0$Bxit`EwvZb7jke;810>h3 zb}}!oS_xUbJ^$_PWrSlJ-;v4qq!@|L9uM#ALcMu|+|fni+AqPpu+CtjBrs#Y1jKVU zEc6L$d!2l-MgMi5&7?{Dfxj)qn;mIZudn7I6V$88%05A!PtCQTGSxXKMGh;qXa|fE zJBUmhM!}@e#A?s%bajm+=Ka1WxHZWaj;k#XT{T#;bH9c5zA8txVHEz(EeE*PP9eD9 z<2|evdxmVLj_n@`lp>6@ zy_ZTczm54_lGjPwPaq$dF1HdIks&Mp;%bge$QZnnp${}#&Z3)z95ei@b9;c=kJpY- z$G#RZbgyTi3&d4=3%+gXOSp|g^~^%K1id>re4gTka;7m@WA}bFo`GUbT8-n19VVdO}IkuW(H_iil_S}@$xy(Q*fCcNaD60 zxqsWK5lESLWnKgy^ci@da#k9^aW5)oLzbFxlUVBA&UM~79PF7=rW@Ot`>9(Gju3N{A4%EK0dPuz{=J_LUv|Pe^*x3eq_ExMNjB3?{$+xH^_Y z;e5pH)*~Lo@y=;b=P$Iqp9KR|j(>D-kaI4WeI&&HPFRtbZBMiQ^PwE`pF$Z7#(@UF zP2~&InXDTNx3`4)H2mD8yHl{Jk(|C(VA2vwY}3IRqo*qy9HvN7a!$$hlZqjmb6tZy zp1fLd^be5LmcI`_d3@@A`jLDS!b0qXVvP%y>+DfL86Ie=*TZ)PL??Lk^F};4=dwv; zPRBV>*)f&NE0vtjYHw@vs9l(Dk*g-}ARSciwv!f)E361d_9y<;9b7)PBw$3dh`AZi zAY4)BVh3t>;gR=s)nZW3PT_3bOLDK)eTZT^*m%P!HdC!FvK=Z=_iA>Bg!`SsC|P3u zz+oMr^PUcTebccFK>bqp475+?5RUC{Y7klp^p=Q;ZM+c8Zq6wBtH*5c=QHlp7wZS%6AszeebN>>_2^H7uuK@g%1{vF}DT>U{h`}c+u5ubXcFMH)fZ6-l z!y=qVN>jqgj)3T!mALcM;1!8}PDcMCU6<9?l#euNff${zE=b0d%;TcPFfw`y>zjLg#_WgnwatH|t}Y&WrR32m5W_AWNa`OqIc{ zW{_mX(Ck1psRCgMhJ*hXhcAG1ocb_kuY)%9rlYzq8h$K;X}=5m+8CYpJ4Yw6zLi%S zpu}dkAc_hVv>NfWy9eLsQ-6OzoBl{WAkRi|U;anmJ5dFwz(C9~-A(!Vfw z(E!S5ua;@}(q5GrIc6|PAOSPg{il$s$UBI}tk5xuP-VedGyZd}xqXvWvU_`{;Cf0> z5fN79T(#iq-q$RLb(of0ZA0lfepj^!a2-6 zv{v^7r2J*xmj&XVgZ>Wd=RqwGGe1`-Svll~bz(-y7*N1ooU5J*aY@&5ea5ss6n(a? z`N9l?w~=^1g2wLDVRD5ovqLc^Z#YRDFR+QYV4emH*fzOpzer3>Pudh??f``be>dD3 z)xB}1O6bZpnt=j(m92Fxq0dz89n>B05xx10QDL-YDz&e>h_u@9+RG)Pv4{2IYNiMy z8auH}j+fW*;q%Ymtbq+KI_r4gxGUeYJ>hq~vbe!N3%NntH+Dyh7I70!cu(qE_`Vp; z07NvH4Q2s#9;mKj;>umoviK|H+#CbgGq`D+QxI*$r6&D`yf%-M^{H;6gi4*j3?c9c z8$}NK?0I4%b?c`p2;SvL3*xY`0fe_KIZqPm`M%{DCrPUt{bS|zlhbHBNlUe7zcK}E z$L2zIl+z#Z!thJW!}{G&JAC@Pg`H(}GLM_m;uV}C9Yt(vF+F0Dy7{`k zY&v=ZZf?8^qSD>~2iP#{qQK632aMplZye6Q3X>dctS@JHSz2)zJaqXvFEZlr>9$oY z^&9^4pN`1EJcEw_wi@P{zJqQX470?WZTB*5Y7F!3#xJO^z|Gw@)bFoY5#daTP5OgI zcbKI$Ok(|9g_%#If*$3ga=U0_n%|#}eWwyeW~(19Te+!xF*(rd=LU(nM15;<7Z&oA zrqIw#r7}&_qgCdvS7+!|3?8w7JNRtHQ$~8Yyw(xC+n=- z7SQBo3+)tbg2NJn^=lukNOCkiEsgt~4tCrZ{aSnrHRMk@_?1^whFrEn3mT1NSC9B&c-(JrWu@FUhSNf+(>-_%kX#@LYnzq`^M#XX}(*!_LZCY za24(5Y$WH^=;GY^#0c{Y4{_!GPvm_bd#&6ypUpfwu%|+=UEe^Q+oe$7cXnyF@O67L3%SKO#rdayD^4^vH2hG{w%vp|_*jKf4 z=jb?40UP4S+Mi~(Uz(^cvgVB+r+Rt|;wnFRYcz(i=&Q14Ok=V-tTPw4%v&;ZrxI#w z6&rvLjj#yzBr5~N*7o09CkIE=>EWwo`ceL*@Y=504RB*xY#SY{)p3Gvn9zBL_FCN0 zl^axu8p~su8HpiDNi{%5ojAv1{0?t7*mflF9&Y_x4#)X(jyLl~c+s6*I1G7{zBI;tH*_ z94)o##4$cU4ohj~e#C^E><)3E`d;ftdwTQZpDmp)9)n5^+h%BE?)8LI2A`L!zjTBL zPYE&+#0&jDFc&4Tg}VC}E@4ZGyWbiK2dvn6Mpu!cQT_^6!RG!7)fE>V>?PNFm?vc5 z>A8gcW=5Xm2#LEW_;XgMQ$=Y-#lc|zs2}}2ny_4Kb%D@Vrtu6rOmUe!ph7;;L`XHi zXcDHc;OYbIk44?|A9-=Ml{Xap)^{jb5$Kl?v`CIT`bDXV*x{h+UARtzOd}#US>a%X zOdU`5^_P@lkQxB*B<&RQB?FgJOH2-~rMnXf_{5%~s&OlUM^i30FeOM{`XOXs)3_BU zEAyNr%bz8RJ=Cvw8y=)3p z`K|i!j$l~LqQ)kabHK}7WeyB$x*({t#cQWf98qh&X{R*Y--9)~g)?XCL>&z;v9#hY zTFY?DV&1fPE&*z}6Ki`Y5#(-eVYB;OzZjPSDnN%ArA8D>wODpQT4Jt}ah556JE+G_! z_P0uQ!qDhR94VdpAqajIOl4~>oTaQ8H5yXaTZUOb%cRAkWYV?KSNlTqgSM=Wgf)JP zz=?Q5f5zPEVO!NbOCbqEwP^Ff_O_`gdm67#U{Mp^_bKcq2IoO%zcJb(M5z`cjv1Ck z+!awNRhwjj6CQqu+xC#{UWo^3+h?6ymzq3r?3JV}<|u_9x=MWAm`1AqAnOsJ*@)^4 zr|`FkZlg{Cd!#Chmhn=_ZQe;~-DTUOv>)Tbmh0{z_42vWa|vNUO% z_5KA1xNHBgw0zjUH|s5xg$b4k z@Koa#-AFizrr6h2#$k*41tm7_jp$yL4X*DZcklq!u+>9E0WnhcOFPn7Vh^ao@~tno z@RwY)*+8&|Hpdq)`a=L*Teuw;_B@u;o!a!YaOO@bs-?*gqpm?nRkXl~mKFfF z+OVzE%RlC`M5-+KM_GXZ@9b;=2C(sq+R&Ko_RzZ%5P~kDieK3yzV4BN*{$E%KY;4k z)s?*vacHYN~u+?SoI`e@S2!9Co!cdvz;@N@{yj`0-9^8osR(V7PR-O&gM)x3owqs5oJpIwc zgY`#VzjI$V>YYDrIr8D;0JK<10@ycefw z;;oV(!gUR*xBg%xTl-#d>u(5}#jFrLKo}q0b{IuuZhuO7n++ zo@9)d#`(AT$mbW5g;c;&z>1_2Nk%;L?TIhfeK%PYp>5N<5wdihxw4-qvVsN6t@bol zDFgi~t`B&ZU3ek!#fXVE5Ao$7AwI+@amT_m2SclwQE{cLcv3kwhokq+!S%>Fe_*(Z z75)vhq@YqZqa~Hf$0S?T@nr_%mV%*aT${~4)6|(P@Bq_Q!VC4tZa`7?ra`4?oV+wSr2`TVSUmKS_>V@3%0*S#!+L=3f@oF=4k9U9xv0p1;Fx&}V;X2J~h zcz^}G3|;s8JyEFR*LB*fPUm+?f+ofnBQ5uK%NrwA+RV_~h<6-mw_wU?NGRI!zNTh% z&>ty6x8&gW75gdW)?p->&%?{*brS|k@b|(>&<^nyO55Pi_q*eK)=J*Uunw2cw--p%E!VXuDa? ztZ$HPKJ6$Sh7!UrpxVBLFSnpZOw$(ftvg!Nk1LVfL+FL(u zh1Abu(oCSmgqQ2IrE;Zz2f2DAD%T4XO6tU&)2IB}vV3{^xpz1MYFEPy_09RP2QvmA zIqw<(UaCnCs!mFX$+3sjnV*(O5)y`jW!*wzF-l^K`Bxgap+0Ej z@c^nf{Ic`6I5#9bcE7fwiiP8JZ9dr3FsD~SBiW_`8{UgFt*{$@qj#E)90JYra>Zs3 z$sCTuzOye2GdTO;4@;wgJK@!ij-|c--insluCR}{#q=D6Xz#nL6;`rkc*UzLTR%Y{ zN2YK;Zcz4YY=+|(0_?E=#~3U@I1fIyRiBF zIeWj=id+b|L;kSMs>NMfeB^(={IdrC;NYJy_$L+olL`OdOqgH0OpSa?FTRhwb<|%A Pe7HEdAEg|=c=LY&YVNkY literal 0 HcmV?d00001 diff --git a/packages/veilid_support/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/veilid_support/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..13b35eba55c6dabc3aac36f33d859266c18fa0d0 GIT binary patch literal 5680 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 literal 0 HcmV?d00001 diff --git a/packages/veilid_support/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/veilid_support/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..0a3f5fa40fb3d1e0710331a48de5d256da3f275d GIT binary patch literal 520 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 literal 0 HcmV?d00001 diff --git a/packages/veilid_support/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/veilid_support/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000000000000000000000000000000000000..2f1632cfddf3d9dade342351e627a0a75609fb46 GIT binary patch literal 2218 zcmV;b2vzrqP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuE6iGxuRCodHTWf3-RTMruyW6Fu zQYeUM04eX6D5c0FCjKKPrco1(K`<0SL=crI{PC3-^hZU0kQie$gh-5!7z6SH6Q0J% zqot*`H1q{R5fHFYS}dje@;kG=v$L0(yY0?wY2%*c?A&{2?!D*x?m71{of2gv!$5|C z3>qG_BW}7K_yUcT3A5C6QD<+{aq?x;MAUyAiJn#Jv8_zZtQ{P zTRzbL3U9!qVuZzS$xKU10KiW~Bgdcv1-!uAhQxf3a7q+dU6lj?yoO4Lq4TUN4}h{N z*fIM=SS8|C2$(T>w$`t@3Tka!(r!7W`x z-isCVgQD^mG-MJ;XtJuK3V{Vy72GQ83KRWsHU?e*wrhKk=ApIYeDqLi;JI1e zuvv}5^Dc=k7F7?nm3nIw$NVmU-+R>> zyqOR$-2SDpJ}Pt;^RkJytDVXNTsu|mI1`~G7yw`EJR?VkGfNdqK9^^8P`JdtTV&tX4CNcV4 z&N06nZa??Fw1AgQOUSE2AmPE@WO(Fvo`%m`cDgiv(fAeRA%3AGXUbsGw{7Q`cY;1BI#ac3iN$$Hw z0LT0;xc%=q)me?Y*$xI@GRAw?+}>=9D+KTk??-HJ4=A>`V&vKFS75@MKdSF1JTq{S zc1!^8?YA|t+uKigaq!sT;Z!&0F2=k7F0PIU;F$leJLaw2UI6FL^w}OG&!;+b%ya1c z1n+6-inU<0VM-Y_s5iTElq)ThyF?StVcebpGI znw#+zLx2@ah{$_2jn+@}(zJZ{+}_N9BM;z)0yr|gF-4=Iyu@hI*Lk=-A8f#bAzc9f z`Kd6K--x@t04swJVC3JK1cHY-Hq+=|PN-VO;?^_C#;coU6TDP7Bt`;{JTG;!+jj(` zw5cLQ-(Cz-Tlb`A^w7|R56Ce;Wmr0)$KWOUZ6ai0PhzPeHwdl0H(etP zUV`va_i0s-4#DkNM8lUlqI7>YQLf)(lz9Q3Uw`)nc(z3{m5ZE77Ul$V%m)E}3&8L0 z-XaU|eB~Is08eORPk;=<>!1w)Kf}FOVS2l&9~A+@R#koFJ$Czd%Y(ENTV&A~U(IPI z;UY+gf+&6ioZ=roly<0Yst8ck>(M=S?B-ys3mLdM&)ex!hbt+ol|T6CTS+Sc0jv(& z7ijdvFwBq;0a{%3GGwkDKTeG`b+lyj0jjS1OMkYnepCdoosNY`*zmBIo*981BU%%U z@~$z0V`OVtIbEx5pa|Tct|Lg#ZQf5OYMUMRD>Wdxm5SAqV2}3!ceE-M2 z@O~lQ0OiKQp}o9I;?uxCgYVV?FH|?Riri*U$Zi_`V2eiA>l zdSm6;SEm6#T+SpcE8Ro_f2AwxzI z44hfe^WE3!h@W3RDyA_H440cpmYkv*)6m1XazTqw%=E5Xv7^@^^T7Q2wxr+Z2kVYr + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/veilid_support/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/veilid_support/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..92fb3cd --- /dev/null +++ b/packages/veilid_support/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.example + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2024 com.example. All rights reserved. diff --git a/packages/veilid_support/example/macos/Runner/Configs/Debug.xcconfig b/packages/veilid_support/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/packages/veilid_support/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/veilid_support/example/macos/Runner/Configs/Release.xcconfig b/packages/veilid_support/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/packages/veilid_support/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/veilid_support/example/macos/Runner/Configs/Warnings.xcconfig b/packages/veilid_support/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/packages/veilid_support/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/packages/veilid_support/example/macos/Runner/DebugProfile.entitlements b/packages/veilid_support/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/packages/veilid_support/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/packages/veilid_support/example/macos/Runner/Info.plist b/packages/veilid_support/example/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/packages/veilid_support/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/packages/veilid_support/example/macos/Runner/MainFlutterWindow.swift b/packages/veilid_support/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/packages/veilid_support/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/packages/veilid_support/example/macos/Runner/Release.entitlements b/packages/veilid_support/example/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/packages/veilid_support/example/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/packages/veilid_support/example/macos/RunnerTests/RunnerTests.swift b/packages/veilid_support/example/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..5418c9f --- /dev/null +++ b/packages/veilid_support/example/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import FlutterMacOS +import Cocoa +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/packages/veilid_support/example/pubspec.lock b/packages/veilid_support/example/pubspec.lock new file mode 100644 index 0000000..2903fb7 --- /dev/null +++ b/packages/veilid_support/example/pubspec.lock @@ -0,0 +1,492 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + async_tools: + dependency: transitive + description: + path: "../../async_tools" + relative: true + source: path + version: "1.0.0" + bloc: + dependency: transitive + description: + name: bloc + sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + url: "https://pub.dev" + source: hosted + version: "8.1.4" + bloc_tools: + dependency: transitive + description: + path: "../../bloc_tools" + relative: true + source: path + version: "1.0.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + change_case: + dependency: transitive + description: + name: change_case + sha256: "47c48c36f95f20c6d0ba03efabceff261d05026cca322cc2c4c01c343371b5bb" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + 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" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + 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: transitive + description: + name: fast_immutable_collections + sha256: "38fbc50df5b219dcfb83ebbc3275ec09872530ca1153858fc56fceadb310d037" + url: "https://pub.dev" + source: hosted + version: "10.2.2" + ffi: + dependency: transitive + description: + name: ffi + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + url: "https://pub.dev" + source: hosted + version: "2.4.1" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + globbing: + dependency: transitive + description: + name: globbing + sha256: "4f89cfaf6fa74c9c1740a96259da06bd45411ede56744e28017cc534a12b6e2d" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + loggy: + dependency: transitive + 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: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + url: "https://pub.dev" + source: hosted + version: "0.8.0" + meta: + dependency: transitive + description: + name: meta + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + mutex: + dependency: "direct dev" + description: + path: "../../mutex" + relative: true + source: path + version: "3.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: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + url: "https://pub.dev" + source: hosted + version: "2.2.4" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + 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: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + 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: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + process: + dependency: transitive + description: + name: process + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + 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" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + system_info2: + dependency: transitive + description: + name: system_info2 + sha256: "65206bbef475217008b5827374767550a5420ce70a04d2d7e94d1d2253f3efc9" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + 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_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + veilid: + dependency: transitive + description: + path: "../../../../veilid/veilid-flutter" + relative: true + source: path + version: "0.3.1" + veilid_support: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "1.0.2+0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + win32: + dependency: transitive + description: + name: win32 + sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" + url: "https://pub.dev" + source: hosted + version: "5.5.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" +sdks: + dart: ">=3.3.4 <4.0.0" + flutter: ">=3.19.1" diff --git a/packages/veilid_support/example/pubspec.yaml b/packages/veilid_support/example/pubspec.yaml new file mode 100644 index 0000000..7575a7e --- /dev/null +++ b/packages/veilid_support/example/pubspec.yaml @@ -0,0 +1,90 @@ +name: example +description: "Veilid Support Example" +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: '>=3.3.4 <4.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.6 + veilid_support: + path: ../ + + +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + flutter_lints: ^3.0.1 + mutex: + path: ../../mutex + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/veilid_support/example/test/widget_test.dart b/packages/veilid_support/example/test/widget_test.dart new file mode 100644 index 0000000..092d222 --- /dev/null +++ b/packages/veilid_support/example/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:example/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/packages/veilid_support/example/web/favicon.png b/packages/veilid_support/example/web/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8aaa46ac1ae21512746f852a42ba87e4165dfdd1 GIT binary patch literal 917 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?iy0X7 zltGxWVyS%@P(fs7NJL45ua8x7ey(0(N`6wRUPW#JP&EUCO@$SZnVVXYs8ErclUHn2 zVXFjIVFhG^g!Ppaz)DK8ZIvQ?0~DO|i&7O#^-S~(l1AfjnEK zjFOT9D}DX)@^Za$W4-*MbbUihOG|wNBYh(yU7!lx;>x^|#0uTKVr7USFmqf|i<65o z3raHc^AtelCMM;Vme?vOfh>Xph&xL%(-1c06+^uR^q@XSM&D4+Kp$>4P^%3{)XKjo zGZknv$b36P8?Z_gF{nK@`XI}Z90TzwSQO}0J1!f2c(B=V`5aP@1P1a|PZ!4!3&Gl8 zTYqUsf!gYFyJnXpu0!n&N*SYAX-%d(5gVjrHJWqXQshj@!Zm{!01WsQrH~9=kTxW#6SvuapgMqt>$=j#%eyGrQzr zP{L-3gsMA^$I1&gsBAEL+vxi1*Igl=8#8`5?A-T5=z-sk46WA1IUT)AIZHx1rdUrf zVJrJn<74DDw`j)Ki#gt}mIT-Q`XRa2-jQXQoI%w`nb|XblvzK${ZzlV)m-XcwC(od z71_OEC5Bt9GEXosOXaPTYOia#R4ID2TiU~`zVMl08TV_C%DnU4^+HE>9(CE4D6?Fz oujB08i7adh9xk7*FX66dWH6F5TM;?E2b5PlUHx3vIVCg!0Dx9vYXATM literal 0 HcmV?d00001 diff --git a/packages/veilid_support/example/web/icons/Icon-192.png b/packages/veilid_support/example/web/icons/Icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..b749bfef07473333cf1dd31e9eed89862a5d52aa GIT binary patch literal 5292 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 literal 0 HcmV?d00001 diff --git a/packages/veilid_support/example/web/icons/Icon-512.png b/packages/veilid_support/example/web/icons/Icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..88cfd48dff1169879ba46840804b412fe02fefd6 GIT binary patch literal 8252 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 literal 0 HcmV?d00001 diff --git a/packages/veilid_support/example/web/icons/Icon-maskable-192.png b/packages/veilid_support/example/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000000000000000000000000000000000000..eb9b4d76e525556d5d89141648c724331630325d GIT binary patch literal 5594 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 X>c!)3PgKfL&lX^$Z>Cpu&6)6jvi^Z! literal 0 HcmV?d00001 diff --git a/packages/veilid_support/example/web/icons/Icon-maskable-512.png b/packages/veilid_support/example/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..d69c56691fbdb0b7efa65097c7cc1edac12a6d3e GIT binary patch literal 20998 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 MPgg&ebxsLQ025IeI{*Lx literal 0 HcmV?d00001 diff --git a/packages/veilid_support/example/web/index.html b/packages/veilid_support/example/web/index.html new file mode 100644 index 0000000..45cf2ca --- /dev/null +++ b/packages/veilid_support/example/web/index.html @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + example + + + + + + + + + + diff --git a/packages/veilid_support/example/web/manifest.json b/packages/veilid_support/example/web/manifest.json new file mode 100644 index 0000000..096edf8 --- /dev/null +++ b/packages/veilid_support/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "example", + "short_name": "example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/packages/veilid_support/example/windows/.gitignore b/packages/veilid_support/example/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/packages/veilid_support/example/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/packages/veilid_support/example/windows/CMakeLists.txt b/packages/veilid_support/example/windows/CMakeLists.txt new file mode 100644 index 0000000..d960948 --- /dev/null +++ b/packages/veilid_support/example/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(example LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/packages/veilid_support/example/windows/flutter/CMakeLists.txt b/packages/veilid_support/example/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/packages/veilid_support/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/packages/veilid_support/example/windows/flutter/generated_plugin_registrant.cc b/packages/veilid_support/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..72dbdef --- /dev/null +++ b/packages/veilid_support/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + VeilidPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("VeilidPlugin")); +} diff --git a/packages/veilid_support/example/windows/flutter/generated_plugin_registrant.h b/packages/veilid_support/example/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/packages/veilid_support/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/veilid_support/example/windows/flutter/generated_plugins.cmake b/packages/veilid_support/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..658ec85 --- /dev/null +++ b/packages/veilid_support/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + veilid +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/veilid_support/example/windows/runner/CMakeLists.txt b/packages/veilid_support/example/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/packages/veilid_support/example/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/packages/veilid_support/example/windows/runner/Runner.rc b/packages/veilid_support/example/windows/runner/Runner.rc new file mode 100644 index 0000000..687e6bd --- /dev/null +++ b/packages/veilid_support/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2024 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "example.exe" "\0" + VALUE "ProductName", "example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/packages/veilid_support/example/windows/runner/flutter_window.cpp b/packages/veilid_support/example/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/packages/veilid_support/example/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/packages/veilid_support/example/windows/runner/flutter_window.h b/packages/veilid_support/example/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/packages/veilid_support/example/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/packages/veilid_support/example/windows/runner/main.cpp b/packages/veilid_support/example/windows/runner/main.cpp new file mode 100644 index 0000000..a61bf80 --- /dev/null +++ b/packages/veilid_support/example/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/packages/veilid_support/example/windows/runner/resource.h b/packages/veilid_support/example/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/packages/veilid_support/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/packages/veilid_support/example/windows/runner/resources/app_icon.ico b/packages/veilid_support/example/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c04e20caf6370ebb9253ad831cc31de4a9c965f6 GIT binary patch literal 33772 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 literal 0 HcmV?d00001 diff --git a/packages/veilid_support/example/windows/runner/runner.exe.manifest b/packages/veilid_support/example/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..a42ea76 --- /dev/null +++ b/packages/veilid_support/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/packages/veilid_support/example/windows/runner/utils.cpp b/packages/veilid_support/example/windows/runner/utils.cpp new file mode 100644 index 0000000..b2b0873 --- /dev/null +++ b/packages/veilid_support/example/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length <= 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/packages/veilid_support/example/windows/runner/utils.h b/packages/veilid_support/example/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/packages/veilid_support/example/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/veilid_support/example/windows/runner/win32_window.cpp b/packages/veilid_support/example/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/packages/veilid_support/example/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/packages/veilid_support/example/windows/runner/win32_window.h b/packages/veilid_support/example/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/packages/veilid_support/example/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/packages/veilid_support/run_integration_tests.sh b/packages/veilid_support/run_integration_tests.sh new file mode 100755 index 0000000..9c5b882 --- /dev/null +++ b/packages/veilid_support/run_integration_tests.sh @@ -0,0 +1,4 @@ +#!/bin/bash +pushd example 2>/dev/null +flutter test -r expanded integration_test/app_test.dart $@ +popd 2>/dev/null diff --git a/packages/veilid_support/run_integration_tests_web.sh b/packages/veilid_support/run_integration_tests_web.sh new file mode 100755 index 0000000..c74f9fd --- /dev/null +++ b/packages/veilid_support/run_integration_tests_web.sh @@ -0,0 +1,5 @@ +#!/bin/bash +echo Ensure chromedriver is running on port 4444 and you have compiled veilid-wasm with wasm_build.sh +pushd example 2>/dev/null +flutter drive --driver=test_driver/integration_test.dart --target=integration_test/app_test.dart -d chrome $@ +popd 2>/dev/null diff --git a/packages/veilid_support/run_unit_tests.sh b/packages/veilid_support/run_unit_tests.sh new file mode 100755 index 0000000..388715e --- /dev/null +++ b/packages/veilid_support/run_unit_tests.sh @@ -0,0 +1,2 @@ +#!/bin/bash +flutter test -r expanded $@ From e622b7f949fd2bce27b657bcf3f0f7aabd7c2ad5 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 29 Apr 2024 14:23:01 -0400 Subject: [PATCH 092/270] configure tests --- .../macos/Runner.xcodeproj/project.pbxproj | 35 +++++++++++++++++-- .../macos/Runner/DebugProfile.entitlements | 6 ++++ .../example/macos/Runner/Release.entitlements | 8 +++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/packages/veilid_support/example/macos/Runner.xcodeproj/project.pbxproj b/packages/veilid_support/example/macos/Runner.xcodeproj/project.pbxproj index b6717eb..91980e8 100644 --- a/packages/veilid_support/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/veilid_support/example/macos/Runner.xcodeproj/project.pbxproj @@ -27,6 +27,8 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 4380113B2BE014F40006987E /* libveilid_flutter.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 4380113A2BE014F40006987E /* libveilid_flutter.dylib */; }; + 4380113C2BE014F40006987E /* libveilid_flutter.dylib in Bundle Framework */ = {isa = PBXBuildFile; fileRef = 4380113A2BE014F40006987E /* libveilid_flutter.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 6CFA599ADEA1F061ADEDAE10 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 80431090FA5BD99A5B868F84 /* Pods_Runner.framework */; }; 91F51E973F11EFA10521D360 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C8526B53F6575BAE32CE8D8 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ @@ -55,6 +57,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + 4380113C2BE014F40006987E /* libveilid_flutter.dylib in Bundle Framework */, ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; @@ -79,6 +82,7 @@ 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; 3597F23CF6B8931362B671EB /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 4380113A2BE014F40006987E /* libveilid_flutter.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libveilid_flutter.dylib; path = "../../../../../veilid/target/lipo-darwin/libveilid_flutter.dylib"; sourceTree = ""; }; 4C8526B53F6575BAE32CE8D8 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 80431090FA5BD99A5B868F84 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -103,6 +107,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 4380113B2BE014F40006987E /* libveilid_flutter.dylib in Frameworks */, 6CFA599ADEA1F061ADEDAE10 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -195,13 +200,13 @@ A24D16EEC9CB70E4E4BE3331 /* Pods-RunnerTests.release.xcconfig */, 83A502A4616091D33488BCEE /* Pods-RunnerTests.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + 4380113A2BE014F40006987E /* libveilid_flutter.dylib */, 80431090FA5BD99A5B868F84 /* Pods_Runner.framework */, 4C8526B53F6575BAE32CE8D8 /* Pods_RunnerTests.framework */, ); @@ -270,7 +275,6 @@ 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; LastSwiftMigration = 1100; - ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.Sandbox = { enabled = 1; @@ -572,13 +576,22 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = XP5LBLT7M7; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}\"", + /usr/lib/swift, + "../../../../../veilid/target/lipo-darwin", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.veilid.packages.veilid-support.tests"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -704,13 +717,22 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = XP5LBLT7M7; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}\"", + /usr/lib/swift, + "../../../../../veilid/target/lipo-darwin", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.veilid.packages.veilid-support.tests"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -724,13 +746,22 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = XP5LBLT7M7; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}\"", + /usr/lib/swift, + "../../../../../veilid/target/lipo-darwin", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.veilid.packages.veilid-support.tests"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; diff --git a/packages/veilid_support/example/macos/Runner/DebugProfile.entitlements b/packages/veilid_support/example/macos/Runner/DebugProfile.entitlements index dddb8a3..1871fce 100644 --- a/packages/veilid_support/example/macos/Runner/DebugProfile.entitlements +++ b/packages/veilid_support/example/macos/Runner/DebugProfile.entitlements @@ -6,7 +6,13 @@ com.apple.security.cs.allow-jit + com.apple.security.network.client + com.apple.security.network.server + keychain-access-groups + + $(AppIdentifierPrefix)org.veilid.packages.veilid-support.tests + diff --git a/packages/veilid_support/example/macos/Runner/Release.entitlements b/packages/veilid_support/example/macos/Runner/Release.entitlements index 852fa1a..bc8f1bd 100644 --- a/packages/veilid_support/example/macos/Runner/Release.entitlements +++ b/packages/veilid_support/example/macos/Runner/Release.entitlements @@ -4,5 +4,13 @@ com.apple.security.app-sandbox + com.apple.security.network.client + + com.apple.security.network.server + + keychain-access-groups + + $(AppIdentifierPrefix)org.veilid.packages.veilid-support.tests + From 25a6a00fcfd6ddbe91507599589a962ad52d0ca5 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 1 May 2024 20:58:25 -0400 Subject: [PATCH 093/270] refactor, use external libraries, and add integration test for veilid_support --- .../account_repository.dart | 2 +- lib/chat/cubits/active_chat_cubit.dart | 3 +- .../active_conversations_bloc_map_cubit.dart | 2 +- ...ve_single_contact_chat_bloc_map_cubit.dart | 2 +- lib/chat_list/cubits/chat_list_cubit.dart | 2 +- .../cubits/contact_invitation_list_cubit.dart | 10 +- .../cubits/contact_request_inbox_cubit.dart | 2 +- .../cubits/invitation_generator_cubit.dart | 2 +- .../cubits/waiting_invitation_cubit.dart | 2 +- .../waiting_invitations_bloc_map_cubit.dart | 2 +- .../models/valid_contact_invitation.dart | 4 +- lib/contacts/cubits/conversation_cubit.dart | 8 +- .../home_account_ready_shell.dart | 2 +- lib/settings/preferences_cubit.dart | 2 +- lib/theme/views/widget_helpers.dart | 2 +- lib/tick.dart | 15 +- .../cubit/connection_state_cubit.dart | 2 +- packages/async_tools/.gitignore | 7 - packages/async_tools/analysis_options.yaml | 15 - packages/async_tools/build.bat | 2 - packages/async_tools/build.sh | 3 - .../example/async_tools_example.dart | 6 - packages/async_tools/lib/async_tools.dart | 11 - .../async_tools/lib/src/async_tag_lock.dart | 64 --- packages/async_tools/lib/src/async_value.dart | 209 -------- .../lib/src/async_value.freezed.dart | 480 ----------------- .../async_tools/lib/src/delayed_wait_set.dart | 18 - .../async_tools/lib/src/serial_future.dart | 57 -- .../async_tools/lib/src/single_future.dart | 45 -- .../lib/src/single_state_processor.dart | 67 --- .../lib/src/single_stateless_processor.dart | 48 -- packages/async_tools/lib/src/wait_set.dart | 18 - packages/async_tools/pubspec.yaml | 19 - .../async_tools/test/async_tools_test.dart | 16 - 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 | 48 -- .../bloc_tools/lib/src/bloc_busy_wrapper.dart | 78 --- .../bloc_tools/lib/src/bloc_map_cubit.dart | 127 ----- .../lib/src/bloc_tools_extension.dart | 12 - packages/bloc_tools/lib/src/future_cubit.dart | 24 - .../lib/src/state_map_follower.dart | 125 ----- .../lib/src/stream_wrapper_cubit.dart | 25 - .../bloc_tools/lib/src/transformer_cubit.dart | 21 - packages/bloc_tools/pubspec.yaml | 24 - packages/bloc_tools/test/bloc_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 ------------ .../example/analysis_options.yaml | 39 +- .../example/integration_test/app_test.dart | 50 +- .../example/integration_test/fixtures.dart | 152 ------ .../fixtures/dht_record_pool_fixture.dart | 36 ++ .../integration_test/fixtures/fixtures.dart | 1 + .../test_dht_record_pool.dart | 196 ++++++- .../test_dht_short_array.dart | 85 ++- .../ios/Runner.xcodeproj/project.pbxproj | 40 +- .../example/ios/Runner/Info.plist | 8 +- packages/veilid_support/example/pubspec.lock | 56 +- packages/veilid_support/example/pubspec.yaml | 73 +-- .../src/dht_record/dht_record.dart | 1 + .../src/dht_record/dht_record_pool.dart | 59 ++- .../src/dht_short_array/dht_short_array.dart | 105 ++-- .../dht_short_array_cubit.dart | 2 +- .../dht_short_array/dht_short_array_head.dart | 33 +- .../lib/src/async_table_db_backed_cubit.dart | 1 - packages/veilid_support/lib/src/identity.dart | 12 +- .../lib/src/persistent_queue.dart | 1 - packages/veilid_support/pubspec.lock | 27 +- packages/veilid_support/pubspec.yaml | 8 +- pubspec.lock | 19 +- pubspec.yaml | 14 +- 84 files changed, 626 insertions(+), 3835 deletions(-) delete mode 100644 packages/async_tools/.gitignore delete mode 100644 packages/async_tools/analysis_options.yaml delete mode 100644 packages/async_tools/build.bat delete mode 100755 packages/async_tools/build.sh delete mode 100644 packages/async_tools/example/async_tools_example.dart delete mode 100644 packages/async_tools/lib/async_tools.dart delete mode 100644 packages/async_tools/lib/src/async_tag_lock.dart delete mode 100644 packages/async_tools/lib/src/async_value.dart delete mode 100644 packages/async_tools/lib/src/async_value.freezed.dart delete mode 100644 packages/async_tools/lib/src/delayed_wait_set.dart delete mode 100644 packages/async_tools/lib/src/serial_future.dart delete mode 100644 packages/async_tools/lib/src/single_future.dart delete mode 100644 packages/async_tools/lib/src/single_state_processor.dart delete mode 100644 packages/async_tools/lib/src/single_stateless_processor.dart delete mode 100644 packages/async_tools/lib/src/wait_set.dart delete mode 100644 packages/async_tools/pubspec.yaml delete mode 100644 packages/async_tools/test/async_tools_test.dart delete mode 100644 packages/bloc_tools/.gitignore delete mode 100644 packages/bloc_tools/analysis_options.yaml delete mode 100644 packages/bloc_tools/example/bloc_tools_example.dart delete mode 100644 packages/bloc_tools/lib/bloc_tools.dart delete mode 100644 packages/bloc_tools/lib/src/async_transformer_cubit.dart delete mode 100644 packages/bloc_tools/lib/src/bloc_busy_wrapper.dart delete mode 100644 packages/bloc_tools/lib/src/bloc_map_cubit.dart delete mode 100644 packages/bloc_tools/lib/src/bloc_tools_extension.dart delete mode 100644 packages/bloc_tools/lib/src/future_cubit.dart delete mode 100644 packages/bloc_tools/lib/src/state_map_follower.dart delete mode 100644 packages/bloc_tools/lib/src/stream_wrapper_cubit.dart delete mode 100644 packages/bloc_tools/lib/src/transformer_cubit.dart delete mode 100644 packages/bloc_tools/pubspec.yaml delete mode 100644 packages/bloc_tools/test/bloc_tools_test.dart delete mode 100644 packages/mutex/.gitignore delete mode 100644 packages/mutex/CHANGELOG.md delete mode 100644 packages/mutex/LICENSE delete mode 100644 packages/mutex/README.md delete mode 100644 packages/mutex/analysis_options.yaml delete mode 100644 packages/mutex/example/example.dart delete mode 100644 packages/mutex/lib/mutex.dart delete mode 100644 packages/mutex/lib/src/mutex.dart delete mode 100644 packages/mutex/lib/src/read_write_mutex.dart delete mode 100644 packages/mutex/pubspec.yaml delete mode 100644 packages/mutex/test/mutex_multiple_read_test.dart delete mode 100644 packages/mutex/test/mutex_readwrite_test.dart delete mode 100644 packages/mutex/test/mutex_test.dart delete mode 100644 packages/veilid_support/example/integration_test/fixtures.dart create mode 100644 packages/veilid_support/example/integration_test/fixtures/dht_record_pool_fixture.dart create mode 100644 packages/veilid_support/example/integration_test/fixtures/fixtures.dart diff --git a/lib/account_manager/repository/account_repository/account_repository.dart b/lib/account_manager/repository/account_repository/account_repository.dart index 777c79e..4691d52 100644 --- a/lib/account_manager/repository/account_repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository/account_repository.dart @@ -400,7 +400,7 @@ class AccountRepository { // Record not yet open, do it final pool = DHTRecordPool.instance; - final record = await pool.openOwned( + final record = await pool.openRecordOwned( userLogin.accountRecordInfo.accountRecord, debugName: 'AccountRepository::openAccountRecord::AccountRecord', parent: localAccount.identityMaster.identityRecordKey); diff --git a/lib/chat/cubits/active_chat_cubit.dart b/lib/chat/cubits/active_chat_cubit.dart index b57076e..e47caec 100644 --- a/lib/chat/cubits/active_chat_cubit.dart +++ b/lib/chat/cubits/active_chat_cubit.dart @@ -1,8 +1,7 @@ -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 { +class ActiveChatCubit extends Cubit { ActiveChatCubit(super.initialState); void setActiveChat(TypedKey? activeChatRemoteConversationRecordKey) { 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 ff903ca..b221208 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,5 @@ import 'package:async_tools/async_tools.dart'; -import 'package:bloc_tools/bloc_tools.dart'; +import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; import 'package:veilid_support/veilid_support.dart'; 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 index 19ad150..d9ecb67 100644 --- 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 @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:async_tools/async_tools.dart'; -import 'package:bloc_tools/bloc_tools.dart'; +import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index 4a0818a..5023d51 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:bloc_tools/bloc_tools.dart'; +import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:veilid_support/veilid_support.dart'; diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index a24005a..8e133b1 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:bloc_tools/bloc_tools.dart'; +import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; @@ -85,7 +85,7 @@ class ContactInvitationListCubit // to and it will be eventually encrypted with the DH of the contact's // identity key late final Uint8List signedContactInvitationBytes; - await (await pool.create( + await (await pool.createRecord( debugName: 'ContactInvitationListCubit::createInvitation::' 'LocalConversation', parent: _activeAccountInfo.accountRecordKey, @@ -114,7 +114,7 @@ class ContactInvitationListCubit // 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( + await (await pool.createRecord( debugName: 'ContactInvitationListCubit::createInvitation::' 'ContactRequestInbox', parent: _activeAccountInfo.accountRecordKey, @@ -198,7 +198,7 @@ class ContactInvitationListCubit if (success && deletedItem != null) { // Delete the contact request inbox final contactRequestInbox = deletedItem.contactRequestInbox.toVeilid(); - await (await pool.openOwned(contactRequestInbox, + await (await pool.openRecordOwned(contactRequestInbox, debugName: 'ContactInvitationListCubit::deleteInvitation::' 'ContactRequestInbox', parent: accountRecordKey)) @@ -250,7 +250,7 @@ class ContactInvitationListCubit contactRequestInboxKey) != -1; - await (await pool.openRead(contactRequestInboxKey, + await (await pool.openRecordRead(contactRequestInboxKey, debugName: 'ContactInvitationListCubit::validateInvitation::' 'ContactRequestInbox', parent: _activeAccountInfo.accountRecordKey)) diff --git a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart index a8de4f8..a376881 100644 --- a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart +++ b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart @@ -34,7 +34,7 @@ class ContactRequestInboxCubit contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(); final writer = TypedKeyPair( kind: recordKey.kind, key: writerKey, secret: writerSecret); - return pool.openRead(recordKey, + return pool.openRecordRead(recordKey, debugName: 'ContactRequestInboxCubit::_open::' 'ContactRequestInbox', crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer), diff --git a/lib/contact_invitation/cubits/invitation_generator_cubit.dart b/lib/contact_invitation/cubits/invitation_generator_cubit.dart index c6f7258..cd785fa 100644 --- a/lib/contact_invitation/cubits/invitation_generator_cubit.dart +++ b/lib/contact_invitation/cubits/invitation_generator_cubit.dart @@ -1,6 +1,6 @@ import 'dart:typed_data'; -import 'package:bloc_tools/bloc_tools.dart'; +import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; class InvitationGeneratorCubit extends FutureCubit { InvitationGeneratorCubit(super.fut); diff --git a/lib/contact_invitation/cubits/waiting_invitation_cubit.dart b/lib/contact_invitation/cubits/waiting_invitation_cubit.dart index 07c2d63..f8c639f 100644 --- a/lib/contact_invitation/cubits/waiting_invitation_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitation_cubit.dart @@ -1,7 +1,7 @@ import 'dart:typed_data'; import 'package:async_tools/async_tools.dart'; -import 'package:bloc_tools/bloc_tools.dart'; +import 'package:bloc_advanced_tools/bloc_advanced_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 7c06bf7..968e108 100644 --- a/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart @@ -1,5 +1,5 @@ import 'package:async_tools/async_tools.dart'; -import 'package:bloc_tools/bloc_tools.dart'; +import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; diff --git a/lib/contact_invitation/models/valid_contact_invitation.dart b/lib/contact_invitation/models/valid_contact_invitation.dart index 639af7f..12aacef 100644 --- a/lib/contact_invitation/models/valid_contact_invitation.dart +++ b/lib/contact_invitation/models/valid_contact_invitation.dart @@ -37,7 +37,7 @@ class ValidContactInvitation { _activeAccountInfo.localAccount.identityMaster.identityPublicKey; final accountRecordKey = _activeAccountInfo.accountRecordKey; - return (await pool.openWrite(_contactRequestInboxKey, _writer, + return (await pool.openRecordWrite(_contactRequestInboxKey, _writer, debugName: 'ValidContactInvitation::accept::' 'ContactRequestInbox', parent: accountRecordKey)) @@ -100,7 +100,7 @@ class ValidContactInvitation { final accountRecordKey = _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - return (await pool.openWrite(_contactRequestInboxKey, _writer, + return (await pool.openRecordWrite(_contactRequestInboxKey, _writer, debugName: 'ValidContactInvitation::reject::' 'ContactRequestInbox', parent: accountRecordKey)) diff --git a/lib/contacts/cubits/conversation_cubit.dart b/lib/contacts/cubits/conversation_cubit.dart index 7e23a99..6edb533 100644 --- a/lib/contacts/cubits/conversation_cubit.dart +++ b/lib/contacts/cubits/conversation_cubit.dart @@ -48,7 +48,7 @@ class ConversationCubit extends Cubit> { final pool = DHTRecordPool.instance; final crypto = await _cachedConversationCrypto(); final writer = _activeAccountInfo.conversationWriter; - final record = await pool.openWrite( + final record = await pool.openRecordWrite( _localConversationRecordKey!, writer, debugName: 'ConversationCubit::LocalConversation', parent: accountRecordKey, @@ -67,7 +67,7 @@ class ConversationCubit extends Cubit> { // Open remote record key if it is specified final pool = DHTRecordPool.instance; final crypto = await _cachedConversationCrypto(); - final record = await pool.openRead(_remoteConversationRecordKey, + final record = await pool.openRecordRead(_remoteConversationRecordKey, debugName: 'ConversationCubit::RemoteConversation', parent: accountRecordKey, crypto: crypto); @@ -226,14 +226,14 @@ class ConversationCubit extends Cubit> { // Open with SMPL scheme for identity writer late final DHTRecord localConversationRecord; if (existingConversationRecordKey != null) { - localConversationRecord = await pool.openWrite( + localConversationRecord = await pool.openRecordWrite( existingConversationRecordKey, writer, debugName: 'ConversationCubit::initLocalConversation::LocalConversation', parent: accountRecordKey, crypto: crypto); } else { - localConversationRecord = await pool.create( + localConversationRecord = await pool.createRecord( debugName: 'ConversationCubit::initLocalConversation::LocalConversation', parent: accountRecordKey, 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 4ff44d2..bf98bba 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,8 @@ import 'package:async_tools/async_tools.dart'; +import 'package:bloc_advanced_tools/bloc_advanced_tools.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'; diff --git a/lib/settings/preferences_cubit.dart b/lib/settings/preferences_cubit.dart index 6cfd249..f96d755 100644 --- a/lib/settings/preferences_cubit.dart +++ b/lib/settings/preferences_cubit.dart @@ -1,4 +1,4 @@ -import 'package:bloc_tools/bloc_tools.dart'; +import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'settings.dart'; diff --git a/lib/theme/views/widget_helpers.dart b/lib/theme/views/widget_helpers.dart index 7cef561..6edca24 100644 --- a/lib/theme/views/widget_helpers.dart +++ b/lib/theme/views/widget_helpers.dart @@ -1,6 +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:bloc_advanced_tools/bloc_advanced_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'; diff --git a/lib/tick.dart b/lib/tick.dart index 197a1d6..8ec3db7 100644 --- a/lib/tick.dart +++ b/lib/tick.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:async_tools/async_tools.dart'; import 'package:flutter/material.dart'; import 'package:veilid_support/veilid_support.dart'; @@ -16,15 +17,12 @@ class BackgroundTicker extends StatefulWidget { class BackgroundTickerState extends State { Timer? _tickTimer; - bool _inTick = false; @override void initState() { super.initState(); _tickTimer = Timer.periodic(const Duration(seconds: 1), (timer) { - if (!_inTick) { - unawaited(_onTick()); - } + singleFuture(this, _onTick); }); } @@ -50,12 +48,7 @@ class BackgroundTickerState extends State { return; } - _inTick = true; - try { - // Tick DHT record pool - unawaited(DHTRecordPool.instance.tick()); - } finally { - _inTick = false; - } + // Tick DHT record pool + await DHTRecordPool.instance.tick(); } } diff --git a/lib/veilid_processor/cubit/connection_state_cubit.dart b/lib/veilid_processor/cubit/connection_state_cubit.dart index 8bf18f8..0bbfdc2 100644 --- a/lib/veilid_processor/cubit/connection_state_cubit.dart +++ b/lib/veilid_processor/cubit/connection_state_cubit.dart @@ -1,4 +1,4 @@ -import 'package:bloc_tools/bloc_tools.dart'; +import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import '../models/models.dart'; import '../repository/processor_repository.dart'; diff --git a/packages/async_tools/.gitignore b/packages/async_tools/.gitignore deleted file mode 100644 index 3cceda5..0000000 --- a/packages/async_tools/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# 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 deleted file mode 100644 index e1620f7..0000000 --- a/packages/async_tools/analysis_options.yaml +++ /dev/null @@ -1,15 +0,0 @@ -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/build.bat b/packages/async_tools/build.bat deleted file mode 100644 index 0e2e698..0000000 --- a/packages/async_tools/build.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -dart run build_runner build --delete-conflicting-outputs diff --git a/packages/async_tools/build.sh b/packages/async_tools/build.sh deleted file mode 100755 index 2a76503..0000000 --- a/packages/async_tools/build.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -set -e -dart run build_runner build --delete-conflicting-outputs diff --git a/packages/async_tools/example/async_tools_example.dart b/packages/async_tools/example/async_tools_example.dart deleted file mode 100644 index 33a41ab..0000000 --- a/packages/async_tools/example/async_tools_example.dart +++ /dev/null @@ -1,6 +0,0 @@ -// 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 deleted file mode 100644 index 6438ff2..0000000 --- a/packages/async_tools/lib/async_tools.dart +++ /dev/null @@ -1,11 +0,0 @@ -/// Async Tools -library; - -export 'src/async_tag_lock.dart'; -export 'src/async_value.dart'; -export 'src/delayed_wait_set.dart'; -export 'src/serial_future.dart'; -export 'src/single_future.dart'; -export 'src/single_state_processor.dart'; -export 'src/single_stateless_processor.dart'; -export 'src/wait_set.dart'; diff --git a/packages/async_tools/lib/src/async_tag_lock.dart b/packages/async_tools/lib/src/async_tag_lock.dart deleted file mode 100644 index a0f6117..0000000 --- a/packages/async_tools/lib/src/async_tag_lock.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:mutex/mutex.dart'; - -class _AsyncTagLockEntry { - _AsyncTagLockEntry() - : mutex = Mutex.locked(), - waitingCount = 0; - // - Mutex mutex; - int waitingCount; -} - -class AsyncTagLock { - AsyncTagLock() - : _tableLock = Mutex(), - _locks = {}; - - Future lockTag(T tag) async { - await _tableLock.protect(() async { - final lockEntry = _locks[tag]; - if (lockEntry != null) { - lockEntry.waitingCount++; - await lockEntry.mutex.acquire(); - lockEntry.waitingCount--; - } else { - _locks[tag] = _AsyncTagLockEntry(); - } - }); - } - - 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) { - // 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(); - } - } - - 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; -} diff --git a/packages/async_tools/lib/src/async_value.dart b/packages/async_tools/lib/src/async_value.dart deleted file mode 100644 index cc3bd9b..0000000 --- a/packages/async_tools/lib/src/async_value.dart +++ /dev/null @@ -1,209 +0,0 @@ -// 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 [asData] 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 [asData] 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 asData => map( - data: (data) => data, - loading: (_) => null, - error: (_) => null, - ); - - bool get isData => asData != null; - - /// Check if this is loading - AsyncLoading? get asLoading => map( - data: (_) => null, - loading: (loading) => loading, - error: (_) => null, - ); - - bool get isLoading => asLoading != null; - - /// Check if this is an error - AsyncError? get asError => map( - data: (_) => null, - loading: (_) => null, - error: (e) => e, - ); - - bool get isError => asError != 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, - ); - - /// 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/async_tools/lib/src/async_value.freezed.dart b/packages/async_tools/lib/src/async_value.freezed.dart deleted file mode 100644 index b6911e2..0000000 --- a/packages/async_tools/lib/src/async_value.freezed.dart +++ /dev/null @@ -1,480 +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 '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#adding-getters-and-methods-to-our-models'); - -/// @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/packages/async_tools/lib/src/delayed_wait_set.dart b/packages/async_tools/lib/src/delayed_wait_set.dart deleted file mode 100644 index 75223dc..0000000 --- a/packages/async_tools/lib/src/delayed_wait_set.dart +++ /dev/null @@ -1,18 +0,0 @@ -class DelayedWaitSet { - DelayedWaitSet(); - - void add(Future Function() closure) { - _closures.add(closure); - } - - Future call() async { - final futures = _closures.map((c) => c()).toList(); - _closures = []; - if (futures.isEmpty) { - return; - } - await futures.wait; - } - - List Function()> _closures = []; -} diff --git a/packages/async_tools/lib/src/serial_future.dart b/packages/async_tools/lib/src/serial_future.dart deleted file mode 100644 index 17225b7..0000000 --- a/packages/async_tools/lib/src/serial_future.dart +++ /dev/null @@ -1,57 +0,0 @@ -// 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_future.dart b/packages/async_tools/lib/src/single_future.dart deleted file mode 100644 index 11f6005..0000000 --- a/packages/async_tools/lib/src/single_future.dart +++ /dev/null @@ -1,45 +0,0 @@ -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); - } - }()); -} - -Future singleFuturePause(Object tag) async => _keys.lockTag(tag); -void singleFutureResume(Object tag) => _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 deleted file mode 100644 index 098238c..0000000 --- a/packages/async_tools/lib/src/single_state_processor.dart +++ /dev/null @@ -1,67 +0,0 @@ -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, 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 newClosure = closure; - var done = false; - while (!done) { - await newClosure(newState); - - // See if there's another state change to process - final nextState = _nextState; - final nextClosure = _nextClosure; - _nextState = null; - _nextClosure = null; - if (nextState != null) { - newState = nextState; - newClosure = nextClosure!; - } else { - done = true; - } - } - }, onBusy: () { - // Keep this state until we process again - _nextState = newInputState; - _nextClosure = closure; - }); - } - - Future pause() => singleFuturePause(this); - Future resume() async { - // Process any next state before resuming the singlefuture - try { - final nextState = _nextState; - final nextClosure = _nextClosure; - _nextState = null; - _nextClosure = null; - if (nextState != null) { - await nextClosure!(nextState); - } - } finally { - singleFutureResume(this); - } - } - - State? _nextState; - Future Function(State)? _nextClosure; -} diff --git a/packages/async_tools/lib/src/single_stateless_processor.dart b/packages/async_tools/lib/src/single_stateless_processor.dart deleted file mode 100644 index 7cb7ff0..0000000 --- a/packages/async_tools/lib/src/single_stateless_processor.dart +++ /dev/null @@ -1,48 +0,0 @@ -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/async_tools/lib/src/wait_set.dart b/packages/async_tools/lib/src/wait_set.dart deleted file mode 100644 index ae79ba9..0000000 --- a/packages/async_tools/lib/src/wait_set.dart +++ /dev/null @@ -1,18 +0,0 @@ -class WaitSet { - WaitSet(); - - void add(Future Function() closure) { - _futures.add(Future.delayed(Duration.zero, closure)); - } - - Future call() async { - final futures = _futures; - _futures = []; - if (futures.isEmpty) { - return; - } - await futures.wait; - } - - List> _futures = []; -} diff --git a/packages/async_tools/pubspec.yaml b/packages/async_tools/pubspec.yaml deleted file mode 100644 index 42d7a71..0000000 --- a/packages/async_tools/pubspec.yaml +++ /dev/null @@ -1,19 +0,0 @@ -name: async_tools -description: Useful data structures and tools for async/Future code -version: 1.0.0 -publish_to: none - -environment: - sdk: '>=3.2.0 <4.0.0' - -# Add regular dependencies here. -dependencies: - freezed_annotation: ^2.4.1 - mutex: - path: ../mutex - -dev_dependencies: - build_runner: ^2.4.8 - freezed: ^2.4.7 - lint_hard: ^4.0.0 - test: ^1.25.2 diff --git a/packages/async_tools/test/async_tools_test.dart b/packages/async_tools/test/async_tools_test.dart deleted file mode 100644 index 0d54797..0000000 --- a/packages/async_tools/test/async_tools_test.dart +++ /dev/null @@ -1,16 +0,0 @@ -// 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/bloc_tools/.gitignore b/packages/bloc_tools/.gitignore deleted file mode 100644 index 3cceda5..0000000 --- a/packages/bloc_tools/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# 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 deleted file mode 100644 index e1620f7..0000000 --- a/packages/bloc_tools/analysis_options.yaml +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index 25e6326..0000000 --- a/packages/bloc_tools/example/bloc_tools_example.dart +++ /dev/null @@ -1,6 +0,0 @@ -// 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 deleted file mode 100644 index 507ae58..0000000 --- a/packages/bloc_tools/lib/bloc_tools.dart +++ /dev/null @@ -1,11 +0,0 @@ -/// 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_map_follower.dart'; -export 'src/stream_wrapper_cubit.dart'; -export 'src/transformer_cubit.dart'; diff --git a/packages/bloc_tools/lib/src/async_transformer_cubit.dart b/packages/bloc_tools/lib/src/async_transformer_cubit.dart deleted file mode 100644 index f31659d..0000000 --- a/packages/bloc_tools/lib/src/async_transformer_cubit.dart +++ /dev/null @@ -1,48 +0,0 @@ -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()) { - _asyncTransform(input.state); - _subscription = input.stream.listen(_asyncTransform); - } - - void _asyncTransform(AsyncValue newInputState) { - _singleStateProcessor.updateState(newInputState, (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.asData!.value); - emit(transformedState); - } - } on Exception catch (e, st) { - emit(AsyncValue.error(e, st)); - } - }); - } - - @override - Future close() async { - await _subscription.cancel(); - await input.close(); - await super.close(); - } - - Cubit> input; - final SingleStateProcessor> _singleStateProcessor = - SingleStateProcessor(); - Future> Function(S) transform; - late final StreamSubscription> _subscription; -} diff --git a/packages/bloc_tools/lib/src/bloc_busy_wrapper.dart b/packages/bloc_tools/lib/src/bloc_busy_wrapper.dart deleted file mode 100644 index b6811f2..0000000 --- a/packages/bloc_tools/lib/src/bloc_busy_wrapper.dart +++ /dev/null @@ -1,78 +0,0 @@ -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 busyValue(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 - 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; - }); - - 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 and - // turn off the busy state - final finalState = changedState; - if (finalState != null && finalState != state.state) { - emit(BlocBusyState(finalState)); - } else { - emit(BlocBusyState(state.state)); - } - }); - void changeState(S state) { - if (_mutex.isLocked) { - changedState = state; - } else { - emit(BlocBusyState(state)); - } - } - - bool get isBusy => _mutex.isLocked; - - final Mutex _mutex = Mutex(); - S? changedState; -} diff --git a/packages/bloc_tools/lib/src/bloc_map_cubit.dart b/packages/bloc_tools/lib/src/bloc_map_cubit.dart deleted file mode 100644 index b99ae2b..0000000 --- a/packages/bloc_tools/lib/src/bloc_map_cubit.dart +++ /dev/null @@ -1,127 +0,0 @@ -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:meta/meta.dart'; - -import 'state_map_follower.dart'; - -typedef BlocMapState = IMap; - -class _ItemEntry { - _ItemEntry({required this.bloc, required this.subscription}); - final B bloc; - 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 -// V = 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> - with StateMapFollowable, K, V> { - 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(); - } - - @protected - @override - // ignore: unnecessary_overrides - void emit(BlocMapState state) { - super.emit(state); - } - - 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 addState(K key, V 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) { - 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); - }); - - /// StateMapFollowable ///////////////////////// - @override - IMap getStateMap(BlocMapState s) => s; - - final Map> _entries; - final AsyncTagLock _tagLock; -} diff --git a/packages/bloc_tools/lib/src/bloc_tools_extension.dart b/packages/bloc_tools/lib/src/bloc_tools_extension.dart deleted file mode 100644 index 44940da..0000000 --- a/packages/bloc_tools/lib/src/bloc_tools_extension.dart +++ /dev/null @@ -1,12 +0,0 @@ -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/packages/bloc_tools/lib/src/future_cubit.dart b/packages/bloc_tools/lib/src/future_cubit.dart deleted file mode 100644 index 39be126..0000000 --- a/packages/bloc_tools/lib/src/future_cubit.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'dart:async'; - -import 'package:async_tools/async_tools.dart'; -import 'package:bloc/bloc.dart'; - -abstract class FutureCubit extends Cubit> { - FutureCubit(Future fut) : super(const AsyncValue.loading()) { - _initWait.add(() async => fut.then((value) { - emit(AsyncValue.data(value)); - // ignore: avoid_types_on_closure_parameters - }, onError: (Object e, StackTrace stackTrace) { - emit(AsyncValue.error(e, stackTrace)); - })); - } - FutureCubit.value(State state) : super(AsyncValue.data(state)); - - @override - Future close() async { - await _initWait(); - await super.close(); - } - - final WaitSet _initWait = WaitSet(); -} diff --git a/packages/bloc_tools/lib/src/state_map_follower.dart b/packages/bloc_tools/lib/src/state_map_follower.dart deleted file mode 100644 index 4843f39..0000000 --- a/packages/bloc_tools/lib/src/state_map_follower.dart +++ /dev/null @@ -1,125 +0,0 @@ -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:meta/meta.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 -mixin StateMapFollower on Closable { - void follow(StateMapFollowable followable) { - assert(_following == null, 'can only follow one followable at a time'); - _following = followable; - _lastInputStateMap = IMap(); - _subscription = followable.registerFollower(this); - } - - Future unfollow() async { - await _subscription?.cancel(); - _subscription = null; - _following?.unregisterFollower(this); - _following = null; - } - - @override - @mustCallSuper - Future close() async { - await unfollow(); - await super.close(); - } - - Future removeFromState(K key); - Future updateState(K key, V value); - - void _updateFollow(IMap newInputState) { - final following = _following; - if (following == null) { - return; - } - _singleStateProcessor.updateState(newInputState, (newStateMap) async { - 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; - }); - } - - StateMapFollowable? _following; - late IMap _lastInputStateMap; - late StreamSubscription>? _subscription; - final SingleStateProcessor> _singleStateProcessor = - SingleStateProcessor(); -} - -/// Interface that allows a StateMapFollower to follow some other class's -/// state changes -abstract mixin class StateMapFollowable - implements StateStreamable { - IMap getStateMap(S state); - - StreamSubscription> registerFollower( - StateMapFollower follower) { - final stateMapTransformer = StreamTransformer>.fromHandlers( - handleData: (d, s) => s.add(getStateMap(d))); - - if (_followers.isEmpty) { - // start transforming stream - _transformedStream = stream.transform(stateMapTransformer); - } - _followers.add(follower); - follower._updateFollow(getStateMap(state)); - return _transformedStream!.listen((s) => follower._updateFollow(s)); - } - - void unregisterFollower(StateMapFollower follower) { - _followers.remove(follower); - if (_followers.isEmpty) { - // stop transforming stream - _transformedStream = null; - } - } - - Future syncFollowers(Future Function() closure) async { - // pause all followers - await _followers.map((f) => f._singleStateProcessor.pause()).wait; - - // run closure - final out = await closure(); - - // resume all followers and wait for current state map to be updated - final resumeState = getStateMap(state); - await _followers.map((f) async { - // Ensure the latest state has been updated - try { - f._updateFollow(resumeState); - } finally { - // Resume processing of the follower - await f._singleStateProcessor.resume(); - } - }).wait; - - return out; - } - - Stream>? _transformedStream; - final List> _followers = []; -} diff --git a/packages/bloc_tools/lib/src/stream_wrapper_cubit.dart b/packages/bloc_tools/lib/src/stream_wrapper_cubit.dart deleted file mode 100644 index 732695b..0000000 --- a/packages/bloc_tools/lib/src/stream_wrapper_cubit.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'dart:async'; - -import 'package:async_tools/async_tools.dart'; -import 'package:bloc/bloc.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/packages/bloc_tools/lib/src/transformer_cubit.dart b/packages/bloc_tools/lib/src/transformer_cubit.dart deleted file mode 100644 index e9aa9b6..0000000 --- a/packages/bloc_tools/lib/src/transformer_cubit.dart +++ /dev/null @@ -1,21 +0,0 @@ -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/packages/bloc_tools/pubspec.yaml b/packages/bloc_tools/pubspec.yaml deleted file mode 100644 index a51fa35..0000000 --- a/packages/bloc_tools/pubspec.yaml +++ /dev/null @@ -1,24 +0,0 @@ -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.11.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 deleted file mode 100644 index 5a81be4..0000000 --- a/packages/bloc_tools/test/bloc_tools_test.dart +++ /dev/null @@ -1,16 +0,0 @@ -// 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/.gitignore b/packages/mutex/.gitignore deleted file mode 100644 index 2ca4cae..0000000 --- a/packages/mutex/.gitignore +++ /dev/null @@ -1,16 +0,0 @@ -# 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 deleted file mode 100644 index 9115310..0000000 --- a/packages/mutex/CHANGELOG.md +++ /dev/null @@ -1,50 +0,0 @@ -## 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 deleted file mode 100644 index eb40cc8..0000000 --- a/packages/mutex/LICENSE +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 1df6b41..0000000 --- a/packages/mutex/README.md +++ /dev/null @@ -1,191 +0,0 @@ -# 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 deleted file mode 100644 index e1620f7..0000000 --- a/packages/mutex/analysis_options.yaml +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index c13b007..0000000 --- a/packages/mutex/example/example.dart +++ /dev/null @@ -1,114 +0,0 @@ -// 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 deleted file mode 100644 index ba224d1..0000000 --- a/packages/mutex/lib/mutex.dart +++ /dev/null @@ -1,11 +0,0 @@ -// 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 deleted file mode 100644 index 1c9e9ec..0000000 --- a/packages/mutex/lib/src/mutex.dart +++ /dev/null @@ -1,89 +0,0 @@ -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 deleted file mode 100644 index 8a5c12e..0000000 --- a/packages/mutex/lib/src/read_write_mutex.dart +++ /dev/null @@ -1,304 +0,0 @@ -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 deleted file mode 100644 index 753ae25..0000000 --- a/packages/mutex/pubspec.yaml +++ /dev/null @@ -1,12 +0,0 @@ -name: mutex -description: Mutual exclusion with implementation of normal and read-write mutex -version: 3.1.0 -publish_to: none - -environment: - sdk: '>=3.2.0 <4.0.0' - -dev_dependencies: - lint_hard: ^4.0.0 - pana: ^0.22.2 - test: ^1.25.2 diff --git a/packages/mutex/test/mutex_multiple_read_test.dart b/packages/mutex/test/mutex_multiple_read_test.dart deleted file mode 100644 index 5ed8345..0000000 --- a/packages/mutex/test/mutex_multiple_read_test.dart +++ /dev/null @@ -1,102 +0,0 @@ -// 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 deleted file mode 100644 index 310caa1..0000000 --- a/packages/mutex/test/mutex_readwrite_test.dart +++ /dev/null @@ -1,486 +0,0 @@ -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 deleted file mode 100644 index 0db5f52..0000000 --- a/packages/mutex/test/mutex_test.dart +++ /dev/null @@ -1,341 +0,0 @@ -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/example/analysis_options.yaml b/packages/veilid_support/example/analysis_options.yaml index 0d29021..04953d6 100644 --- a/packages/veilid_support/example/analysis_options.yaml +++ b/packages/veilid_support/example/analysis_options.yaml @@ -1,28 +1,15 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml - +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: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at https://dart.dev/lints. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options + unawaited_futures: true + avoid_positional_boolean_parameters: false diff --git a/packages/veilid_support/example/integration_test/app_test.dart b/packages/veilid_support/example/integration_test/app_test.dart index 542ce54..0b411e0 100644 --- a/packages/veilid_support/example/integration_test/app_test.dart +++ b/packages/veilid_support/example/integration_test/app_test.dart @@ -1,19 +1,30 @@ -@Timeout(Duration(seconds: 60)) +@Timeout(Duration(seconds: 120)) + +library veilid_support_integration_test; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:veilid_test/veilid_test.dart'; -import 'fixtures.dart'; +import 'fixtures/fixtures.dart'; import 'test_dht_record_pool.dart'; import 'test_dht_short_array.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - final fixture = DefaultFixture(); + final veilidFixture = + DefaultVeilidFixture(programName: 'veilid_support integration test'); + final updateProcessorFixture = + UpdateProcessorFixture(veilidFixture: veilidFixture); + final tickerFixture = + TickerFixture(updateProcessorFixture: updateProcessorFixture); + final dhtRecordPoolFixture = DHTRecordPoolFixture( + tickerFixture: tickerFixture, + updateProcessorFixture: updateProcessorFixture); group('Started Tests', () { - setUpAll(fixture.setUp); - tearDownAll(fixture.tearDown); + setUpAll(veilidFixture.setUp); + tearDownAll(veilidFixture.tearDown); // group('Crypto Tests', () { // test('best cryptosystem', testBestCryptoSystem); @@ -23,15 +34,32 @@ void main() { // }); group('Attached Tests', () { - setUpAll(fixture.attach); - tearDownAll(fixture.detach); + setUpAll(veilidFixture.attach); + tearDownAll(veilidFixture.detach); group('DHT Support Tests', () { - group('DHTRecordPool Tests', () { - test('create pool', testDHTRecordPoolCreate); - }); + setUpAll(updateProcessorFixture.setUp); + setUpAll(tickerFixture.setUp); + tearDownAll(tickerFixture.tearDown); + tearDownAll(updateProcessorFixture.tearDown); + + test('create pool', testDHTRecordPoolCreate); + + // group('DHTRecordPool Tests', () { + // setUpAll(dhtRecordPoolFixture.setUp); + // tearDownAll(dhtRecordPoolFixture.tearDown); + + // test('create/delete record', testDHTRecordCreateDelete); + // test('record scopes', testDHTRecordScopes); + // test('create/delete deep record', testDHTRecordDeepCreateDelete); + // }); + group('DHTShortArray Tests', () { - test('create shortarray', testDHTShortArrayCreate); + setUpAll(dhtRecordPoolFixture.setUp); + tearDownAll(dhtRecordPoolFixture.tearDown); + + // test('create shortarray', testDHTShortArrayCreateDelete); + test('add shortarray', testDHTShortArrayAdd); }); }); }); diff --git a/packages/veilid_support/example/integration_test/fixtures.dart b/packages/veilid_support/example/integration_test/fixtures.dart deleted file mode 100644 index ef69e81..0000000 --- a/packages/veilid_support/example/integration_test/fixtures.dart +++ /dev/null @@ -1,152 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:mutex/mutex.dart'; -import 'package:veilid/veilid.dart'; - -class DefaultFixture { - DefaultFixture(); - - StreamSubscription? _veilidUpdateSubscription; - Stream? _veilidUpdateStream; - final StreamController _updateStreamController = - StreamController.broadcast(); - - static final _fixtureMutex = Mutex(); - - Future setUp() async { - await _fixtureMutex.acquire(); - - assert(_veilidUpdateStream == null, 'should not set up fixture twice'); - - 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(); - - final Map platformConfigJson; - if (kIsWeb) { - final platformConfig = VeilidWASMConfig( - logging: VeilidWASMConfigLogging( - performance: VeilidWASMConfigLoggingPerformance( - enabled: true, - level: VeilidConfigLogLevel.debug, - logsInTimings: true, - logsInConsole: false, - ignoreLogTargets: ignoreLogTargets, - ), - api: VeilidWASMConfigLoggingApi( - enabled: true, - level: VeilidConfigLogLevel.info, - ignoreLogTargets: ignoreLogTargets, - ))); - platformConfigJson = platformConfig.toJson(); - } else { - final platformConfig = VeilidFFIConfig( - logging: VeilidFFIConfigLogging( - terminal: VeilidFFIConfigLoggingTerminal( - enabled: false, - level: VeilidConfigLogLevel.debug, - ignoreLogTargets: ignoreLogTargets, - ), - otlp: VeilidFFIConfigLoggingOtlp( - enabled: false, - level: VeilidConfigLogLevel.trace, - grpcEndpoint: 'localhost:4317', - serviceName: 'Veilid Tests', - ignoreLogTargets: ignoreLogTargets, - ), - api: VeilidFFIConfigLoggingApi( - enabled: true, - // level: VeilidConfigLogLevel.debug, - level: VeilidConfigLogLevel.info, - ignoreLogTargets: ignoreLogTargets, - ))); - platformConfigJson = platformConfig.toJson(); - } - Veilid.instance.initializeVeilidCore(platformConfigJson); - - var config = await getDefaultVeilidConfig( - isWeb: kIsWeb, - programName: 'Veilid Tests', - // 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'), - ); - - config = - config.copyWith(tableStore: config.tableStore.copyWith(delete: true)); - config = config.copyWith( - protectedStore: config.protectedStore.copyWith(delete: true)); - config = - config.copyWith(blockStore: config.blockStore.copyWith(delete: true)); - - final us = - _veilidUpdateStream = await Veilid.instance.startupVeilidCore(config); - - _veilidUpdateSubscription = us.listen((update) { - if (update is VeilidLog) { - // print(update.message); - } else if (update is VeilidUpdateAttachment) { - } else if (update is VeilidUpdateConfig) { - } else if (update is VeilidUpdateNetwork) { - } else if (update is VeilidAppMessage) { - } else if (update is VeilidAppCall) { - } else if (update is VeilidUpdateValueChange) { - } else if (update is VeilidUpdateRouteChange) { - } else { - throw Exception('unexpected update: $update'); - } - _updateStreamController.sink.add(update); - }); - } - - Stream get updateStream => _updateStreamController.stream; - - Future attach() async { - await Veilid.instance.attach(); - - // Wait for attached state - while (true) { - final state = await Veilid.instance.getVeilidState(); - var done = false; - if (state.attachment.publicInternetReady) { - switch (state.attachment.state) { - case AttachmentState.detached: - break; - case AttachmentState.attaching: - break; - case AttachmentState.detaching: - break; - default: - done = true; - break; - } - } - if (done) { - break; - } - await Future.delayed(const Duration(seconds: 1)); - } - } - - Future detach() async { - await Veilid.instance.detach(); - } - - Future tearDown() async { - assert(_veilidUpdateStream != null, 'should not tearDown without setUp'); - - final cancelFut = _veilidUpdateSubscription?.cancel(); - await Veilid.instance.shutdownVeilidCore(); - await cancelFut; - - _veilidUpdateSubscription = null; - _veilidUpdateStream = null; - - _fixtureMutex.release(); - } -} diff --git a/packages/veilid_support/example/integration_test/fixtures/dht_record_pool_fixture.dart b/packages/veilid_support/example/integration_test/fixtures/dht_record_pool_fixture.dart new file mode 100644 index 0000000..216d00f --- /dev/null +++ b/packages/veilid_support/example/integration_test/fixtures/dht_record_pool_fixture.dart @@ -0,0 +1,36 @@ +import 'dart:async'; + +import 'package:async_tools/async_tools.dart'; +import 'package:veilid_support/veilid_support.dart'; +import 'package:veilid_test/veilid_test.dart'; + +class DHTRecordPoolFixture implements TickerFixtureTickable { + DHTRecordPoolFixture( + {required this.tickerFixture, required this.updateProcessorFixture}); + + static final _fixtureMutex = Mutex(); + UpdateProcessorFixture updateProcessorFixture; + TickerFixture tickerFixture; + + Future setUp() async { + await _fixtureMutex.acquire(); + await DHTRecordPool.init(); + tickerFixture.register(this); + } + + Future tearDown() async { + assert(_fixtureMutex.isLocked, 'should not tearDown without setUp'); + tickerFixture.unregister(this); + await DHTRecordPool.close(); + _fixtureMutex.release(); + } + + @override + Future onTick() async { + if (!updateProcessorFixture + .processorConnectionState.isPublicInternetReady) { + return; + } + await DHTRecordPool.instance.tick(); + } +} diff --git a/packages/veilid_support/example/integration_test/fixtures/fixtures.dart b/packages/veilid_support/example/integration_test/fixtures/fixtures.dart new file mode 100644 index 0000000..95e89a0 --- /dev/null +++ b/packages/veilid_support/example/integration_test/fixtures/fixtures.dart @@ -0,0 +1 @@ +export 'dht_record_pool_fixture.dart'; diff --git a/packages/veilid_support/example/integration_test/test_dht_record_pool.dart b/packages/veilid_support/example/integration_test/test_dht_record_pool.dart index 56d398e..2f05d0b 100644 --- a/packages/veilid_support/example/integration_test/test_dht_record_pool.dart +++ b/packages/veilid_support/example/integration_test/test_dht_record_pool.dart @@ -1,9 +1,201 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:veilid_support/veilid_support.dart'; Future testDHTRecordPoolCreate() async { - // final cs = await Veilid.instance.bestCryptoSystem(); - // expect(await cs.defaultSaltLength(), equals(16)); + await DHTRecordPool.init(logger: debugPrintSynchronously); + final pool = DHTRecordPool.instance; + await pool.tick(); + await DHTRecordPool.close(); +} + +Future testDHTRecordCreateDelete() async { + final pool = DHTRecordPool.instance; + + // Close before delete + { + final rec = await pool.createRecord(debugName: 'test_create_delete 1'); + expect(rec.isOpen, isTrue); + await rec.close(); + expect(rec.isOpen, isFalse); + await pool.deleteRecord(rec.key); + // Set should fail + await expectLater(() async => rec.tryWriteBytes(utf8.encode('test')), + throwsA(isA())); + } + + // Close after delete + { + final rec2 = await pool.createRecord(debugName: 'test_create_delete 2'); + expect(rec2.isOpen, isTrue); + await pool.deleteRecord(rec2.key); + expect(rec2.isOpen, isTrue); + await rec2.close(); + expect(rec2.isOpen, isFalse); + // Set should fail + await expectLater(() async => rec2.tryWriteBytes(utf8.encode('test')), + throwsA(isA())); + } + + // Close after delete multiple + // Okay to request delete multiple times before close + { + final rec3 = await pool.createRecord(debugName: 'test_create_delete 3'); + await pool.deleteRecord(rec3.key); + await pool.deleteRecord(rec3.key); + // Set should succeed still + await rec3.tryWriteBytes(utf8.encode('test')); + await rec3.close(); + await rec3.close(); + // Set should fail + await expectLater(() async => rec3.tryWriteBytes(utf8.encode('test')), + throwsA(isA())); + // Delete already delete should fail + await expectLater(() async => pool.deleteRecord(rec3.key), + throwsA(isA())); + } +} + +Future testDHTRecordScopes() async { + final pool = DHTRecordPool.instance; + + // Delete scope with exception should propagate exception + { + final rec = await pool.createRecord(debugName: 'test_scope 1'); + await expectLater( + () async => rec.deleteScope((recd) async { + throw Exception(); + }), + throwsA(isA())); + // Set should fail + await expectLater(() async => rec.tryWriteBytes(utf8.encode('test')), + throwsA(isA())); + } + + // Delete scope without exception + { + final rec2 = await pool.createRecord(debugName: 'test_scope 2'); + try { + await rec2.deleteScope((rec2d) async { + // + }); + } on Exception { + assert(false, 'should not throw'); + } + await rec2.close(); + await pool.deleteRecord(rec2.key); + } + + // Close scope without exception + { + final rec3 = await pool.createRecord(debugName: 'test_scope 3'); + try { + await rec3.scope((rec3d) async { + // + }); + } on Exception { + assert(false, 'should not throw'); + } + // Set should fail because scope closed it + await expectLater(() async => rec3.tryWriteBytes(utf8.encode('test')), + throwsA(isA())); + await pool.deleteRecord(rec3.key); + } +} + +Future testDHTRecordGetSet() async { + final pool = DHTRecordPool.instance; + final valdata = utf8.encode('test'); + + // Test get without set + { + final rec = await pool.createRecord(debugName: 'test_get_set 1'); + final val = await rec.get(); + await pool.deleteRecord(rec.key); + expect(val, isNull); + } + + // Test set then get + { + final rec2 = await pool.createRecord(debugName: 'test_get_set 2'); + expect(await rec2.tryWriteBytes(valdata), isNull); + expect(await rec2.get(), equals(valdata)); + // Invalid subkey should throw + await expectLater( + () async => rec2.get(subkey: 1), throwsA(isA())); + await pool.deleteRecord(rec2.key); + } + + // Test set then delete then open then get + { + final rec3 = await pool.createRecord(debugName: 'test_get_set 3'); + expect(await rec3.tryWriteBytes(valdata), isNull); + expect(await rec3.get(), equals(valdata)); + await rec3.close(); + await pool.deleteRecord(rec3.key); + final rec4 = + await pool.openRecordRead(rec3.key, debugName: 'test_get_set 4'); + expect(await rec4.get(), equals(valdata)); + await rec4.close(); + await pool.deleteRecord(rec4.key); + } +} + +Future testDHTRecordDeepCreateDelete() async { + final pool = DHTRecordPool.instance; + const numChildren = 20; + const numIterations = 10; + + // Make root record + final recroot = await pool.createRecord(debugName: 'test_deep_create_delete'); + + for (var d = 0; d < numIterations; d++) { + // Make child set 1 + var parent = recroot; + final children = []; + for (var n = 0; n < numChildren; n++) { + final child = + await pool.createRecord(debugName: 'deep $n', parent: parent.key); + children.add(child); + parent = child; + } + + // Make child set 2 + final children2 = []; + parent = recroot; + for (var n = 0; n < numChildren; n++) { + final child = + await pool.createRecord(debugName: 'deep2 $n ', parent: parent.key); + children2.add(child); + parent = child; + } + // Should fail to delete root + await expectLater( + () async => pool.deleteRecord(recroot.key), throwsA(isA())); + + // Close child set 1 + await children.map((c) => c.close()).wait; + + // Delete child set 1 in reverse order + for (var n = numChildren - 1; n >= 0; n--) { + await pool.deleteRecord(children[n].key); + } + + // Should fail to delete root + await expectLater( + () async => pool.deleteRecord(recroot.key), throwsA(isA())); + + // Close child set 1 + await children2.map((c) => c.close()).wait; + + // Delete child set 2 in reverse order + for (var n = numChildren - 1; n >= 0; n--) { + await pool.deleteRecord(children2[n].key); + } + } + + // Should be able to delete root now + await pool.deleteRecord(recroot.key); } diff --git a/packages/veilid_support/example/integration_test/test_dht_short_array.dart b/packages/veilid_support/example/integration_test/test_dht_short_array.dart index 4a3c627..80497e3 100644 --- a/packages/veilid_support/example/integration_test/test_dht_short_array.dart +++ b/packages/veilid_support/example/integration_test/test_dht_short_array.dart @@ -3,7 +3,86 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:veilid_support/veilid_support.dart'; -Future testDHTShortArrayCreate() async { - // final cs = await Veilid.instance.bestCryptoSystem(); - // expect(await cs.defaultSaltLength(), equals(16)); +Future testDHTShortArrayCreateDelete() async { + // Close before delete + { + final arr = await DHTShortArray.create(debugName: 'sa_create_delete 1'); + expect(await arr.operate((r) async => r.length), isZero); + expect(arr.isOpen, isTrue); + await arr.close(); + expect(arr.isOpen, isFalse); + await arr.delete(); + // Operate should fail + await expectLater(() async => arr.operate((r) async => r.length), + throwsA(isA())); + } + + // Close after delete + { + final arr = await DHTShortArray.create(debugName: 'sa_create_delete 2'); + await arr.delete(); + // Operate should still succeed because things aren't closed + expect(await arr.operate((r) async => r.length), isZero); + await arr.close(); + // Operate should fail + await expectLater(() async => arr.operate((r) async => r.length), + throwsA(isA())); + } + + // Close after delete multiple + // Okay to request delete multiple times before close + { + final arr = await DHTShortArray.create(debugName: 'sa_create_delete 3'); + await arr.delete(); + await arr.delete(); + // Operate should still succeed because things aren't closed + expect(await arr.operate((r) async => r.length), isZero); + await arr.close(); + await arr.close(); + // Operate should fail + await expectLater(() async => arr.operate((r) async => r.length), + throwsA(isA())); + } +} + +Future testDHTShortArrayAdd() async { + final arr = await DHTShortArray.create(debugName: 'sa_add 1'); + + final dataset = + Iterable.generate(256).map((n) => utf8.encode('elem $n')).toList(); + + print('adding'); + { + final (res, ok) = await arr.operateWrite((w) async { + for (var n = 0; n < dataset.length; n++) { + print('add $n'); + final success = await w.tryAddItem(dataset[n]); + expect(success, isTrue); + } + }); + expect(res, isNull); + expect(ok, isTrue); + } + + print('get all'); + { + final dataset2 = await arr.operate((r) async => r.getAllItems()); + expect(dataset2, equals(dataset)); + } + + print('clear'); + { + final (res, ok) = await arr.operateWrite((w) async => w.tryClear()); + expect(res, isTrue); + expect(ok, isTrue); + } + + print('get all'); + { + final dataset3 = await arr.operate((r) async => r.getAllItems()); + expect(dataset3, isEmpty); + } + + await arr.delete(); + await arr.close(); } diff --git a/packages/veilid_support/example/ios/Runner.xcodeproj/project.pbxproj b/packages/veilid_support/example/ios/Runner.xcodeproj/project.pbxproj index c3b1948..e9fe777 100644 --- a/packages/veilid_support/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/veilid_support/example/ios/Runner.xcodeproj/project.pbxproj @@ -45,6 +45,7 @@ 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 4380113E2BE01E850006987E /* libveilid_flutter.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libveilid_flutter.a; path = "../../../../../veilid/target/lipo-ios/libveilid_flutter.a"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -76,6 +77,14 @@ path = RunnerTests; sourceTree = ""; }; + 4380113D2BE01E850006987E /* Frameworks */ = { + isa = PBXGroup; + children = ( + 4380113E2BE01E850006987E /* libveilid_flutter.a */, + ); + name = Frameworks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -94,6 +103,7 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, + 4380113D2BE01E850006987E /* Frameworks */, ); sourceTree = ""; }; @@ -361,16 +371,22 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = W2TA5TB8Q5; + DEVELOPMENT_TEAM = XP5LBLT7M7; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Veilid Support Tests"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "org.veilid.packages.veilid-support.tests"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -541,16 +557,22 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = W2TA5TB8Q5; + DEVELOPMENT_TEAM = XP5LBLT7M7; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Veilid Support Tests"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "org.veilid.packages.veilid-support.tests"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -564,16 +586,22 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = W2TA5TB8Q5; + DEVELOPMENT_TEAM = XP5LBLT7M7; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Veilid Support Tests"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "org.veilid.packages.veilid-support.tests"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/packages/veilid_support/example/ios/Runner/Info.plist b/packages/veilid_support/example/ios/Runner/Info.plist index 5458fc4..f15383a 100644 --- a/packages/veilid_support/example/ios/Runner/Info.plist +++ b/packages/veilid_support/example/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -24,6 +26,8 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + UIApplicationSupportsIndirectInputEvents + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -41,9 +45,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - diff --git a/packages/veilid_support/example/pubspec.lock b/packages/veilid_support/example/pubspec.lock index 2903fb7..a31cdd2 100644 --- a/packages/veilid_support/example/pubspec.lock +++ b/packages/veilid_support/example/pubspec.lock @@ -10,12 +10,13 @@ packages: source: hosted version: "2.11.0" async_tools: - dependency: transitive + dependency: "direct dev" description: - path: "../../async_tools" - relative: true - source: path - version: "1.0.0" + name: async_tools + sha256: "972f68ab663724d86260a31e363c1355ff493308441b872bf4e7b8adc67c832c" + url: "https://pub.dev" + source: hosted + version: "0.1.0" bloc: dependency: transitive description: @@ -24,13 +25,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.4" - bloc_tools: + bloc_advanced_tools: dependency: transitive description: - path: "../../bloc_tools" - relative: true - source: path - version: "1.0.0" + name: bloc_advanced_tools + sha256: bc0e1d5c26ae7df011464ab6abc2134dcfb668952acc87359abc7457cab091dd + url: "https://pub.dev" + source: hosted + version: "0.1.0" boolean_selector: dependency: transitive description: @@ -145,14 +147,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" flutter_test: dependency: "direct dev" description: flutter @@ -221,14 +215,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" - lints: - dependency: transitive + lint_hard: + dependency: "direct dev" description: - name: lints - sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + name: lint_hard + sha256: "44d15ec309b1a8e1aff99069df9dcb1597f49d5f588f32811ca28fb7b38c32fe" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "4.0.0" loggy: dependency: transitive description: @@ -261,13 +255,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.11.0" - mutex: - dependency: "direct dev" - description: - path: "../../mutex" - relative: true - source: path - version: "3.1.0" path: dependency: transitive description: @@ -447,7 +434,7 @@ packages: path: "../../../../veilid/veilid-flutter" relative: true source: path - version: "0.3.1" + version: "0.3.2" veilid_support: dependency: "direct main" description: @@ -455,6 +442,13 @@ packages: relative: true source: path version: "1.0.2+0" + veilid_test: + dependency: "direct dev" + description: + path: "../../../../veilid/veilid-flutter/packages/veilid_test" + relative: true + source: path + version: "0.1.0" vm_service: dependency: transitive description: diff --git a/packages/veilid_support/example/pubspec.yaml b/packages/veilid_support/example/pubspec.yaml index 7575a7e..8860e2d 100644 --- a/packages/veilid_support/example/pubspec.yaml +++ b/packages/veilid_support/example/pubspec.yaml @@ -1,90 +1,27 @@ name: example description: "Veilid Support Example" -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. version: 1.0.0+1 environment: sdk: '>=3.3.4 <4.0.0' -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. dependencies: + cupertino_icons: ^1.0.6 flutter: sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.6 veilid_support: path: ../ - dev_dependencies: + async_tools: ^0.1.0 flutter_test: sdk: flutter integration_test: sdk: flutter - flutter_lints: ^3.0.1 - mutex: - path: ../../mutex + lint_hard: ^4.0.0 + veilid_test: + path: ../../../../veilid/veilid-flutter/packages/veilid_test -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages 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 7d29292..ccb09f8 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 @@ -57,6 +57,7 @@ class DHTRecord { DHTRecordCrypto get crypto => _crypto; OwnedDHTRecordPointer get ownedDHTRecordPointer => OwnedDHTRecordPointer(recordKey: key, owner: ownerKeyPair!); + bool get isOpen => _open; Future close() async { if (!_open) { 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 608131c..a64f461 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 @@ -2,10 +2,10 @@ 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'; @@ -179,6 +179,13 @@ class DHTRecordPool with TableDBBackedJson { _singleton = globalPool; } + static Future close() async { + if (_singleton != null) { + _singleton!._routingContext.close(); + _singleton = null; + } + } + Veilid get veilid => _veilid; void log(String message) { @@ -191,8 +198,9 @@ class DHTRecordPool with TableDBBackedJson { required DHTSchema schema, KeyPair? writer, TypedKey? parent}) async { - assert(_mutex.isLocked, 'should be locked here'); - + if (!_mutex.isLocked) { + throw StateError('should be locked here'); + } // Create the record final recordDescriptor = await dhtctx.createDHTRecord(schema); @@ -225,8 +233,9 @@ class DHTRecordPool with TableDBBackedJson { required TypedKey recordKey, KeyPair? writer, TypedKey? parent}) async { - assert(_mutex.isLocked, 'should be locked here'); - + if (!_mutex.isLocked) { + throw StateError('should be locked here'); + } log('openDHTRecord: debugName=$debugName key=$recordKey'); // If we are opening a key that already exists @@ -303,8 +312,9 @@ class DHTRecordPool with TableDBBackedJson { // Collect all dependencies (including the record itself) // in reverse (bottom-up/delete order) List _collectChildrenInner(TypedKey recordKey) { - assert(_mutex.isLocked, 'should be locked here'); - + if (!_mutex.isLocked) { + throw StateError('should be locked here'); + } final allDeps = []; final currentDeps = [recordKey]; while (currentDeps.isNotEmpty) { @@ -318,16 +328,18 @@ class DHTRecordPool with TableDBBackedJson { return allDeps.reversedView; } - void _debugPrintChildren(TypedKey recordKey, {List? allDeps}) { + String _debugChildren(TypedKey recordKey, {List? allDeps}) { allDeps ??= _collectChildrenInner(recordKey); // ignore: avoid_print - print('Parent: $recordKey (${_state.debugNames[recordKey.toString()]})'); + var out = + 'Parent: $recordKey (${_state.debugNames[recordKey.toString()]})\n'; for (final dep in allDeps) { if (dep != recordKey) { // ignore: avoid_print - print(' Child: $dep (${_state.debugNames[dep.toString()]})'); + out += ' Child: $dep (${_state.debugNames[dep.toString()]})\n'; } } + return out; } Future _deleteRecordInner(TypedKey recordKey) async { @@ -343,8 +355,8 @@ class DHTRecordPool with TableDBBackedJson { final allDeps = _collectChildrenInner(recordKey); if (allDeps.singleOrNull != recordKey) { - _debugPrintChildren(recordKey, allDeps: allDeps); - assert(false, 'must delete children first'); + final dbgstr = _debugChildren(recordKey, allDeps: allDeps); + throw StateError('must delete children first: $dbgstr'); } final ori = _opened[recordKey]; @@ -359,7 +371,9 @@ class DHTRecordPool with TableDBBackedJson { } void _validateParentInner(TypedKey? parent, TypedKey child) { - assert(_mutex.isLocked, 'should be locked here'); + if (!_mutex.isLocked) { + throw StateError('should be locked here'); + } final childJson = child.toJson(); final existingParent = _state.parentByChild[childJson]; @@ -379,7 +393,9 @@ class DHTRecordPool with TableDBBackedJson { Future _addDependencyInner(TypedKey? parent, TypedKey child, {required String debugName}) async { - assert(_mutex.isLocked, 'should be locked here'); + if (!_mutex.isLocked) { + throw StateError('should be locked here'); + } if (parent == null) { if (_state.rootRecords.contains(child)) { // Dependency already added @@ -404,8 +420,9 @@ class DHTRecordPool with TableDBBackedJson { } Future _removeDependenciesInner(List childList) async { - assert(_mutex.isLocked, 'should be locked here'); - + if (!_mutex.isLocked) { + throw StateError('should be locked here'); + } var state = _state; for (final child in childList) { @@ -442,7 +459,7 @@ class DHTRecordPool with TableDBBackedJson { /////////////////////////////////////////////////////////////////////// /// Create a root DHTRecord that has no dependent records - Future create({ + Future createRecord({ required String debugName, VeilidRoutingContext? routingContext, TypedKey? parent, @@ -479,7 +496,7 @@ class DHTRecordPool with TableDBBackedJson { }); /// Open a DHTRecord readonly - Future openRead(TypedKey recordKey, + Future openRecordRead(TypedKey recordKey, {required String debugName, VeilidRoutingContext? routingContext, TypedKey? parent, @@ -508,7 +525,7 @@ class DHTRecordPool with TableDBBackedJson { }); /// Open a DHTRecord writable - Future openWrite( + Future openRecordWrite( TypedKey recordKey, KeyPair writer, { required String debugName, @@ -548,7 +565,7 @@ class DHTRecordPool with TableDBBackedJson { /// This is primarily used for backing up private content on to the DHT /// to synchronizing it between devices. Because it is 'owned', the correct /// parent must be specified. - Future openOwned( + Future openRecordOwned( OwnedDHTRecordPointer ownedDHTRecordPointer, { required String debugName, required TypedKey parent, @@ -556,7 +573,7 @@ class DHTRecordPool with TableDBBackedJson { int defaultSubkey = 0, DHTRecordCrypto? crypto, }) => - openWrite( + openRecordWrite( ownedDHTRecordPointer.recordKey, ownedDHTRecordPointer.owner, debugName: debugName, 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 de17b62..f15987a 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,8 +1,8 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:async_tools/async_tools.dart'; import 'package:collection/collection.dart'; -import 'package:mutex/mutex.dart'; import 'package:protobuf/protobuf.dart'; import '../../../veilid_support.dart'; @@ -43,7 +43,7 @@ class DHTShortArray { final schema = DHTSchema.smpl( oCnt: 0, members: [DHTSchemaMember(mKey: smplWriter.key, mCnt: stride + 1)]); - dhtRecord = await pool.create( + dhtRecord = await pool.createRecord( debugName: debugName, parent: parent, routingContext: routingContext, @@ -52,7 +52,7 @@ class DHTShortArray { writer: smplWriter); } else { final schema = DHTSchema.dflt(oCnt: stride + 1); - dhtRecord = await pool.create( + dhtRecord = await pool.createRecord( debugName: debugName, parent: parent, routingContext: routingContext, @@ -80,7 +80,7 @@ class DHTShortArray { VeilidRoutingContext? routingContext, TypedKey? parent, DHTRecordCrypto? crypto}) async { - final dhtRecord = await DHTRecordPool.instance.openRead(headRecordKey, + final dhtRecord = await DHTRecordPool.instance.openRecordRead(headRecordKey, debugName: debugName, parent: parent, routingContext: routingContext, @@ -103,7 +103,7 @@ class DHTShortArray { TypedKey? parent, DHTRecordCrypto? crypto, }) async { - final dhtRecord = await DHTRecordPool.instance.openWrite( + final dhtRecord = await DHTRecordPool.instance.openRecordWrite( headRecordKey, writer, debugName: debugName, parent: parent, @@ -144,21 +144,31 @@ class DHTShortArray { /// Get the record pointer foir this shortarray OwnedDHTRecordPointer get recordPointer => _head.recordPointer; + /// Check if the shortarray is open + bool get isOpen => _head.isOpen; + /// Free all resources for the DHTShortArray Future close() async { + if (!isOpen) { + return; + } await _watchController?.close(); + _watchController = null; await _head.close(); } /// Free all resources for the DHTShortArray and delete it from the DHT + /// Will wait until the short array is closed to delete it Future delete() async { - await close(); - await DHTRecordPool.instance.deleteRecord(recordKey); + 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 { + if (!isOpen) { + throw StateError('short array is not open"'); + } try { return await scopeFunction(this); } finally { @@ -171,6 +181,10 @@ class DHTShortArray { /// uncaught exception is thrown Future deleteScope( Future Function(DHTShortArray) scopeFunction) async { + if (!isOpen) { + throw StateError('short array is not open"'); + } + try { final out = await scopeFunction(this); await close(); @@ -182,11 +196,16 @@ class DHTShortArray { } /// Runs a closure allowing read-only access to the shortarray - Future operate(Future Function(DHTShortArrayRead) closure) async => - _head.operate((head) async { - final reader = _DHTShortArrayRead._(head); - return closure(reader); - }); + Future operate(Future Function(DHTShortArrayRead) closure) async { + if (!isOpen) { + throw StateError('short array is not open"'); + } + + return _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 @@ -206,38 +225,48 @@ class DHTShortArray { /// 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 Function(DHTShortArrayWrite) closure, + {Duration? timeout}) async { + if (!isOpen) { + throw StateError('short array is not open"'); + } + + return _head.operateWriteEventual((head) async { + final writer = _DHTShortArrayWrite._(head); + return closure(writer); + }, timeout: timeout); + } /// Listen to and any all changes to the structure of this short array /// regardless of where the changes are coming from 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; - })); - }); + ) { + if (!isOpen) { + throw StateError('short array is not open"'); + } - // Start watching head record - await _head.watch(); - } - // Return subscription - return _watchController!.stream.listen((_) => onChanged()); - }); + return _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 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 cdce828..3375e9f 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 @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:async_tools/async_tools.dart'; import 'package:bloc/bloc.dart'; -import 'package:bloc_tools/bloc_tools.dart'; +import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:equatable/equatable.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:meta/meta.dart'; 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 f6f3a5a..6da7791 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 @@ -50,13 +50,30 @@ class _DHTShortArrayHead { TypedKey get recordKey => _headRecord.key; OwnedDHTRecordPointer get recordPointer => _headRecord.ownedDHTRecordPointer; int get length => _index.length; + bool get isOpen => _headRecord.isOpen; Future close() async { - final futures = >[_headRecord.close()]; - for (final lr in _linkedRecords) { - futures.add(lr.close()); - } - await Future.wait(futures); + await _headMutex.protect(() async { + if (!isOpen) { + return; + } + final futures = >[_headRecord.close()]; + for (final lr in _linkedRecords) { + futures.add(lr.close()); + } + await Future.wait(futures); + }); + } + + Future delete() async { + await _headMutex.protect(() async { + final pool = DHTRecordPool.instance; + final futures = >[pool.deleteRecord(_headRecord.key)]; + for (final lr in _linkedRecords) { + futures.add(pool.deleteRecord(lr.key)); + } + await Future.wait(futures); + }); } Future operate(Future Function(_DHTShortArrayHead) closure) async => @@ -270,7 +287,7 @@ class _DHTShortArrayHead { final schema = DHTSchema.smpl( oCnt: 0, members: [DHTSchemaMember(mKey: smplWriter.key, mCnt: _stride)]); - final dhtRecord = await pool.create( + final dhtRecord = await pool.createRecord( debugName: '${_headRecord.debugName}_linked_$recordNumber', parent: parent, routingContext: routingContext, @@ -292,14 +309,14 @@ class _DHTShortArrayHead { TypedKey recordKey, int recordNumber) async { final writer = _headRecord.writer; return (writer != null) - ? await DHTRecordPool.instance.openWrite( + ? await DHTRecordPool.instance.openRecordWrite( recordKey, writer, debugName: '${_headRecord.debugName}_linked_$recordNumber', parent: _headRecord.key, routingContext: _headRecord.routingContext, ) - : await DHTRecordPool.instance.openRead( + : await DHTRecordPool.instance.openRecordRead( recordKey, debugName: '${_headRecord.debugName}_linked_$recordNumber', parent: _headRecord.key, 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 da278d4..5d0d84f 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 @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:async_tools/async_tools.dart'; import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; -import 'package:mutex/mutex.dart'; import 'table_db.dart'; diff --git a/packages/veilid_support/lib/src/identity.dart b/packages/veilid_support/lib/src/identity.dart index 9d26a7d..baae797 100644 --- a/packages/veilid_support/lib/src/identity.dart +++ b/packages/veilid_support/lib/src/identity.dart @@ -134,7 +134,7 @@ extension IdentityMasterExtension on IdentityMaster { identityRecordKey.kind, identitySecret); late final List accountRecordInfo; - await (await pool.openRead(identityRecordKey, + await (await pool.openRecordRead(identityRecordKey, debugName: 'IdentityMaster::readAccountsFromIdentity::IdentityRecord', parent: masterRecordKey, @@ -168,14 +168,14 @@ extension IdentityMasterExtension on IdentityMaster { // Open identity key for writing veilidLoggy.debug('Opening identity record'); - return (await pool.openWrite( + return (await pool.openRecordWrite( identityRecordKey, identityWriter(identitySecret), debugName: 'IdentityMaster::addAccountToIdentity::IdentityRecord', parent: masterRecordKey)) .scope((identityRec) async { // Create new account to insert into identity veilidLoggy.debug('Creating new account'); - return (await pool.create( + return (await pool.createRecord( debugName: 'IdentityMaster::addAccountToIdentity::AccountRecord', parent: identityRec.key)) .deleteScope((accountRec) async { @@ -231,14 +231,14 @@ class IdentityMasterWithSecrets { // IdentityMaster DHT record is public/unencrypted veilidLoggy.debug('Creating master identity record'); - return (await pool.create( + return (await pool.createRecord( debugName: 'IdentityMasterWithSecrets::create::IdentityMasterRecord', crypto: const DHTRecordCryptoPublic())) .deleteScope((masterRec) async { veilidLoggy.debug('Creating identity record'); // Identity record is private - return (await pool.create( + return (await pool.createRecord( debugName: 'IdentityMasterWithSecrets::create::IdentityRecord', parent: masterRec.key)) .scope((identityRec) async { @@ -296,7 +296,7 @@ Future openIdentityMaster( final pool = DHTRecordPool.instance; // IdentityMaster DHT record is public/unencrypted - return (await pool.openRead(identityMasterRecordKey, + return (await pool.openRecordRead(identityMasterRecordKey, debugName: 'IdentityMaster::openIdentityMaster::IdentityMasterRecord')) .deleteScope((masterRec) async { diff --git a/packages/veilid_support/lib/src/persistent_queue.dart b/packages/veilid_support/lib/src/persistent_queue.dart index fb76a10..63f0cfb 100644 --- a/packages/veilid_support/lib/src/persistent_queue.dart +++ b/packages/veilid_support/lib/src/persistent_queue.dart @@ -3,7 +3,6 @@ import 'dart:typed_data'; import 'package:async_tools/async_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:mutex/mutex.dart'; import 'package:protobuf/protobuf.dart'; import 'table_db.dart'; diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index 9dcb93e..5cfd348 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -36,10 +36,11 @@ packages: async_tools: dependency: "direct main" description: - path: "../async_tools" - relative: true - source: path - version: "1.0.0" + name: async_tools + sha256: "972f68ab663724d86260a31e363c1355ff493308441b872bf4e7b8adc67c832c" + url: "https://pub.dev" + source: hosted + version: "0.1.0" bloc: dependency: "direct main" description: @@ -48,13 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.3" - bloc_tools: + bloc_advanced_tools: dependency: "direct main" description: - path: "../bloc_tools" - relative: true - source: path - version: "1.0.0" + name: bloc_advanced_tools + sha256: bc0e1d5c26ae7df011464ab6abc2134dcfb668952acc87359abc7457cab091dd + url: "https://pub.dev" + source: hosted + version: "0.1.0" boolean_selector: dependency: transitive description: @@ -409,13 +411,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - mutex: - dependency: "direct main" - description: - path: "../mutex" - relative: true - source: path - version: "3.1.0" node_preamble: dependency: transitive description: diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index 0d2d439..99eb44e 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -7,11 +7,9 @@ environment: sdk: '>=3.2.0 <4.0.0' dependencies: - async_tools: - path: ../async_tools + async_tools: ^0.1.0 bloc: ^8.1.3 - bloc_tools: - path: ../bloc_tools + bloc_advanced_tools: ^0.1.0 collection: ^1.18.0 equatable: ^2.0.5 fast_immutable_collections: ^10.1.1 @@ -19,8 +17,6 @@ dependencies: json_annotation: ^4.8.1 loggy: ^2.0.3 meta: ^1.11.0 - mutex: - path: ../mutex protobuf: ^3.1.0 veilid: diff --git a/pubspec.lock b/pubspec.lock index 6d53fc0..09d4fb0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -60,10 +60,10 @@ packages: async_tools: dependency: "direct main" description: - path: "packages/async_tools" + path: "../dart_async_tools" relative: true source: path - version: "1.0.0" + version: "0.1.0" awesome_extensions: dependency: "direct main" description: @@ -96,13 +96,13 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.4" - bloc_tools: + bloc_advanced_tools: dependency: "direct main" description: - path: "packages/bloc_tools" + path: "../bloc_advanced_tools" relative: true source: path - version: "1.0.0" + version: "0.1.0" blurry_modal_progress_hud: dependency: "direct main" description: @@ -830,13 +830,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.9.1" - mutex: - dependency: "direct main" - description: - path: "packages/mutex" - relative: true - source: path - version: "3.1.0" nested: dependency: transitive description: @@ -1504,7 +1497,7 @@ packages: path: "../veilid/veilid-flutter" relative: true source: path - version: "0.3.1" + version: "0.3.2" veilid_support: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index f8ac76c..cd42f00 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,14 +11,12 @@ dependencies: animated_theme_switcher: ^2.0.10 ansicolor: ^2.0.2 archive: ^3.4.10 - async_tools: - path: packages/async_tools + async_tools: ^0.1.0 awesome_extensions: ^2.0.14 badges: ^3.1.2 basic_utils: ^5.7.0 bloc: ^8.1.4 - bloc_tools: - path: packages/bloc_tools + bloc_advanced_tools: ^0.1.0 blurry_modal_progress_hud: ^1.1.1 change_case: ^2.0.1 charcode: ^1.3.1 @@ -55,8 +53,6 @@ dependencies: meta: ^1.11.0 mobile_scanner: ^4.0.1 motion_toast: ^2.9.1 - mutex: - path: packages/mutex pasteboard: ^0.2.0 path: ^1.9.0 path_provider: ^2.1.3 @@ -87,6 +83,12 @@ dependencies: xterm: ^4.0.0 zxing2: ^0.2.3 +dependency_overrides: + async_tools: + path: ../dart_async_tools + bloc_advanced_tools: + path: ../bloc_advanced_tools + dev_dependencies: build_runner: ^2.4.9 freezed: ^2.5.2 From 627066dd27cdab7e644385adb664c2d0d8143927 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 7 May 2024 10:03:41 -0500 Subject: [PATCH 094/270] use dev version of async and bloc tools --- .../cubits/single_contact_messages_cubit.dart | 2 +- lib/contacts/cubits/conversation_cubit.dart | 4 ++-- .../src/dht_record/dht_record_cubit.dart | 2 +- .../dht_short_array/dht_short_array_cubit.dart | 2 +- .../lib/src/async_table_db_backed_cubit.dart | 2 +- .../veilid_support/lib/src/persistent_queue.dart | 2 +- packages/veilid_support/lib/src/table_db.dart | 2 +- packages/veilid_support/pubspec.lock | 16 +++++++--------- packages/veilid_support/pubspec.yaml | 6 ++++++ 9 files changed, 21 insertions(+), 17 deletions(-) diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 6605ab6..de281fe 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -391,7 +391,7 @@ class SingleContactMessagesCubit extends Cubit { _renderState(); } - final WaitSet _initWait = WaitSet(); + final WaitSet _initWait = WaitSet(); final ActiveAccountInfo _activeAccountInfo; final TypedKey _remoteIdentityPublicKey; final TypedKey _localConversationRecordKey; diff --git a/lib/contacts/cubits/conversation_cubit.dart b/lib/contacts/cubits/conversation_cubit.dart index 6edb533..253dbba 100644 --- a/lib/contacts/cubits/conversation_cubit.dart +++ b/lib/contacts/cubits/conversation_cubit.dart @@ -159,7 +159,7 @@ class ConversationCubit extends Cubit> { final localConversationCubit = _localConversationCubit; final remoteConversationCubit = _remoteConversationCubit; - final deleteSet = DelayedWaitSet(); + final deleteSet = DelayedWaitSet(); if (localConversationCubit != null) { final data = localConversationCubit.state.asData; @@ -351,5 +351,5 @@ class ConversationCubit extends Cubit> { localConversation: null, remoteConversation: null); // DHTRecordCrypto? _conversationCrypto; - final WaitSet _initWait = WaitSet(); + final WaitSet _initWait = WaitSet(); } 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 5cfa721..15919f9 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 @@ -112,7 +112,7 @@ class DHTRecordCubit extends Cubit> { DHTRecord get record => _record; @protected - final WaitSet initWait = WaitSet(); + final WaitSet initWait = WaitSet(); StreamSubscription? _subscription; late DHTRecord _record; 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 3375e9f..7465715 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 @@ -121,7 +121,7 @@ class DHTShortArrayCubit extends Cubit> return _shortArray.operateWriteEventual(closure, timeout: timeout); } - final WaitSet _initWait = WaitSet(); + final WaitSet _initWait = WaitSet(); late final DHTShortArray _shortArray; final T Function(List data) _decodeElement; StreamSubscription? _subscription; 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 5d0d84f..710eec4 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 @@ -42,6 +42,6 @@ abstract class AsyncTableDBBackedCubit extends Cubit> } } - final WaitSet _initWait = WaitSet(); + final WaitSet _initWait = WaitSet(); final Mutex _mutex = Mutex(); } diff --git a/packages/veilid_support/lib/src/persistent_queue.dart b/packages/veilid_support/lib/src/persistent_queue.dart index 63f0cfb..f0cf17a 100644 --- a/packages/veilid_support/lib/src/persistent_queue.dart +++ b/packages/veilid_support/lib/src/persistent_queue.dart @@ -185,7 +185,7 @@ class PersistentQueue final String _key; final T Function(Uint8List) _fromBuffer; final bool _deleteOnClose; - final WaitSet _initWait = WaitSet(); + final WaitSet _initWait = WaitSet(); final Mutex _queueMutex = Mutex(); IList _queue = IList.empty(); final StreamController> _syncAddController = StreamController(); diff --git a/packages/veilid_support/lib/src/table_db.dart b/packages/veilid_support/lib/src/table_db.dart index d0fb72e..773309b 100644 --- a/packages/veilid_support/lib/src/table_db.dart +++ b/packages/veilid_support/lib/src/table_db.dart @@ -172,7 +172,7 @@ class TableDBValue extends TableDBBackedJson { final T? Function(Object? obj) _valueFromJson; final Object? Function(T? obj) _valueToJson; final StreamController _streamController; - final WaitSet _initWait = WaitSet(); + final WaitSet _initWait = WaitSet(); ////////////////////////////////////////////////////////////// /// AsyncTableDBBacked diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index 5cfd348..08ab7e2 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -36,10 +36,9 @@ packages: async_tools: dependency: "direct main" description: - name: async_tools - sha256: "972f68ab663724d86260a31e363c1355ff493308441b872bf4e7b8adc67c832c" - url: "https://pub.dev" - source: hosted + path: "../../../dart_async_tools" + relative: true + source: path version: "0.1.0" bloc: dependency: "direct main" @@ -52,10 +51,9 @@ packages: bloc_advanced_tools: dependency: "direct main" description: - name: bloc_advanced_tools - sha256: bc0e1d5c26ae7df011464ab6abc2134dcfb668952acc87359abc7457cab091dd - url: "https://pub.dev" - source: hosted + path: "../../../bloc_advanced_tools" + relative: true + source: path version: "0.1.0" boolean_selector: dependency: transitive @@ -718,7 +716,7 @@ packages: path: "../../../veilid/veilid-flutter" relative: true source: path - version: "0.3.1" + version: "0.3.2" vm_service: dependency: transitive description: diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index 99eb44e..b4232e7 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -23,6 +23,12 @@ dependencies: # veilid: ^0.0.1 path: ../../../veilid/veilid-flutter +dependency_overrides: + async_tools: + path: ../../../dart_async_tools + bloc_advanced_tools: + path: ../../../bloc_advanced_tools + dev_dependencies: build_runner: ^2.4.8 freezed: ^2.4.7 From ab4f05a34758bcc7aebe4c25b301eb617862587c Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 9 May 2024 10:54:52 -0500 Subject: [PATCH 095/270] fix build --- build.bat | 6 +- build.sh | 4 - lib/settings/models/preferences.g.dart | 2 +- .../example/integration_test/app_test.dart | 31 ++-- .../test_dht_short_array.dart | 169 ++++++++++-------- .../veilid_support/example/macos/Podfile.lock | 2 +- packages/veilid_support/example/pubspec.lock | 12 +- packages/veilid_support/example/pubspec.yaml | 4 +- .../lib/dht_support/proto/dht.proto | 15 +- .../src/dht_record/dht_record_pool.g.dart | 36 ++-- .../veilid_support/lib/src/identity.g.dart | 34 ++-- packages/veilid_support/pubspec.lock | 82 ++++----- packages/veilid_support/pubspec.yaml | 22 +-- pubspec.lock | 12 +- pubspec.yaml | 4 +- 15 files changed, 215 insertions(+), 220 deletions(-) diff --git a/build.bat b/build.bat index e40afe0..b7e2e95 100644 --- a/build.bat +++ b/build.bat @@ -1,13 +1,11 @@ @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 +dart run build_runner build --delete-conflicting-outputs + 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 f723071..1b618ef 100755 --- a/build.sh +++ b/build.sh @@ -1,10 +1,6 @@ #!/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 diff --git a/lib/settings/models/preferences.g.dart b/lib/settings/models/preferences.g.dart index af010d6..23cd5bb 100644 --- a/lib/settings/models/preferences.g.dart +++ b/lib/settings/models/preferences.g.dart @@ -8,7 +8,7 @@ part of 'preferences.dart'; _$LockPreferenceImpl _$$LockPreferenceImplFromJson(Map json) => _$LockPreferenceImpl( - inactivityLockSecs: json['inactivity_lock_secs'] as int, + inactivityLockSecs: (json['inactivity_lock_secs'] as num).toInt(), lockWhenSwitching: json['lock_when_switching'] as bool, lockWithSystemLock: json['lock_with_system_lock'] as bool, ); diff --git a/packages/veilid_support/example/integration_test/app_test.dart b/packages/veilid_support/example/integration_test/app_test.dart index 0b411e0..4a5b1e1 100644 --- a/packages/veilid_support/example/integration_test/app_test.dart +++ b/packages/veilid_support/example/integration_test/app_test.dart @@ -1,4 +1,4 @@ -@Timeout(Duration(seconds: 120)) +@Timeout(Duration(seconds: 240)) library veilid_support_integration_test; @@ -26,13 +26,6 @@ void main() { setUpAll(veilidFixture.setUp); tearDownAll(veilidFixture.tearDown); - // group('Crypto Tests', () { - // test('best cryptosystem', testBestCryptoSystem); - // test('get cryptosystem', testGetCryptoSystem); - // test('get cryptosystem invalid', testGetCryptoSystemInvalid); - // test('hash and verify password', testHashAndVerifyPassword); - // }); - group('Attached Tests', () { setUpAll(veilidFixture.attach); tearDownAll(veilidFixture.detach); @@ -45,21 +38,25 @@ void main() { test('create pool', testDHTRecordPoolCreate); - // group('DHTRecordPool Tests', () { - // setUpAll(dhtRecordPoolFixture.setUp); - // tearDownAll(dhtRecordPoolFixture.tearDown); + group('DHTRecordPool Tests', () { + setUpAll(dhtRecordPoolFixture.setUp); + tearDownAll(dhtRecordPoolFixture.tearDown); - // test('create/delete record', testDHTRecordCreateDelete); - // test('record scopes', testDHTRecordScopes); - // test('create/delete deep record', testDHTRecordDeepCreateDelete); - // }); + test('create/delete record', testDHTRecordCreateDelete); + test('record scopes', testDHTRecordScopes); + test('create/delete deep record', testDHTRecordDeepCreateDelete); + }); group('DHTShortArray Tests', () { setUpAll(dhtRecordPoolFixture.setUp); tearDownAll(dhtRecordPoolFixture.tearDown); - // test('create shortarray', testDHTShortArrayCreateDelete); - test('add shortarray', testDHTShortArrayAdd); + for (final stride in [256, 64, 32, 16, 8, 4, 2, 1]) { + test('create shortarray stride=$stride', + makeTestDHTShortArrayCreateDelete(stride: stride)); + test('add shortarray stride=$stride', + makeTestDHTShortArrayAdd(stride: 256)); + } }); }); }); diff --git a/packages/veilid_support/example/integration_test/test_dht_short_array.dart b/packages/veilid_support/example/integration_test/test_dht_short_array.dart index 80497e3..ad3e22a 100644 --- a/packages/veilid_support/example/integration_test/test_dht_short_array.dart +++ b/packages/veilid_support/example/integration_test/test_dht_short_array.dart @@ -3,86 +3,99 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:veilid_support/veilid_support.dart'; -Future testDHTShortArrayCreateDelete() async { - // Close before delete - { - final arr = await DHTShortArray.create(debugName: 'sa_create_delete 1'); - expect(await arr.operate((r) async => r.length), isZero); - expect(arr.isOpen, isTrue); - await arr.close(); - expect(arr.isOpen, isFalse); - await arr.delete(); - // Operate should fail - await expectLater(() async => arr.operate((r) async => r.length), - throwsA(isA())); - } - - // Close after delete - { - final arr = await DHTShortArray.create(debugName: 'sa_create_delete 2'); - await arr.delete(); - // Operate should still succeed because things aren't closed - expect(await arr.operate((r) async => r.length), isZero); - await arr.close(); - // Operate should fail - await expectLater(() async => arr.operate((r) async => r.length), - throwsA(isA())); - } - - // Close after delete multiple - // Okay to request delete multiple times before close - { - final arr = await DHTShortArray.create(debugName: 'sa_create_delete 3'); - await arr.delete(); - await arr.delete(); - // Operate should still succeed because things aren't closed - expect(await arr.operate((r) async => r.length), isZero); - await arr.close(); - await arr.close(); - // Operate should fail - await expectLater(() async => arr.operate((r) async => r.length), - throwsA(isA())); - } -} - -Future testDHTShortArrayAdd() async { - final arr = await DHTShortArray.create(debugName: 'sa_add 1'); - - final dataset = - Iterable.generate(256).map((n) => utf8.encode('elem $n')).toList(); - - print('adding'); - { - final (res, ok) = await arr.operateWrite((w) async { - for (var n = 0; n < dataset.length; n++) { - print('add $n'); - final success = await w.tryAddItem(dataset[n]); - expect(success, isTrue); +Future Function() makeTestDHTShortArrayCreateDelete( + {required int stride}) => + () async { + // Close before delete + { + final arr = await DHTShortArray.create( + debugName: 'sa_create_delete 1 stride $stride', stride: stride); + expect(await arr.operate((r) async => r.length), isZero); + expect(arr.isOpen, isTrue); + await arr.close(); + expect(arr.isOpen, isFalse); + await arr.delete(); + // Operate should fail + await expectLater(() async => arr.operate((r) async => r.length), + throwsA(isA())); } - }); - expect(res, isNull); - expect(ok, isTrue); - } - print('get all'); - { - final dataset2 = await arr.operate((r) async => r.getAllItems()); - expect(dataset2, equals(dataset)); - } + // Close after delete + { + final arr = await DHTShortArray.create( + debugName: 'sa_create_delete 2 stride $stride', stride: stride); + await arr.delete(); + // Operate should still succeed because things aren't closed + expect(await arr.operate((r) async => r.length), isZero); + await arr.close(); + // Operate should fail + await expectLater(() async => arr.operate((r) async => r.length), + throwsA(isA())); + } - print('clear'); - { - final (res, ok) = await arr.operateWrite((w) async => w.tryClear()); - expect(res, isTrue); - expect(ok, isTrue); - } + // Close after delete multiple + // Okay to request delete multiple times before close + { + final arr = await DHTShortArray.create( + debugName: 'sa_create_delete 3 stride $stride', stride: stride); + await arr.delete(); + await arr.delete(); + // Operate should still succeed because things aren't closed + expect(await arr.operate((r) async => r.length), isZero); + await arr.close(); + await arr.close(); + // Operate should fail + await expectLater(() async => arr.operate((r) async => r.length), + throwsA(isA())); + } + }; - print('get all'); - { - final dataset3 = await arr.operate((r) async => r.getAllItems()); - expect(dataset3, isEmpty); - } +Future Function() makeTestDHTShortArrayAdd({required int stride}) => + () async { + final startTime = DateTime.now(); - await arr.delete(); - await arr.close(); -} + final arr = await DHTShortArray.create( + debugName: 'sa_add 1 stride $stride', stride: stride); + + final dataset = Iterable.generate(256) + .map((n) => utf8.encode('elem $n')) + .toList(); + + print('adding\n'); + { + final (res, ok) = await arr.operateWrite((w) async { + for (var n = 0; n < dataset.length; n++) { + print('$n '); + final success = await w.tryAddItem(dataset[n]); + expect(success, isTrue); + } + }); + expect(res, isNull); + expect(ok, isTrue); + } + + //print('get all\n'); + { + final dataset2 = await arr.operate((r) async => r.getAllItems()); + expect(dataset2, equals(dataset)); + } + + //print('clear\n'); + { + final (res, ok) = await arr.operateWrite((w) async => w.tryClear()); + expect(res, isTrue); + expect(ok, isTrue); + } + + //print('get all\n'); + { + final dataset3 = await arr.operate((r) async => r.getAllItems()); + expect(dataset3, isEmpty); + } + + await arr.delete(); + await arr.close(); + + final endTime = DateTime.now(); + print('Duration: ${endTime.difference(startTime)}'); + }; diff --git a/packages/veilid_support/example/macos/Podfile.lock b/packages/veilid_support/example/macos/Podfile.lock index ac31a59..6a58494 100644 --- a/packages/veilid_support/example/macos/Podfile.lock +++ b/packages/veilid_support/example/macos/Podfile.lock @@ -21,7 +21,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 veilid: a54f57b7bcf0e4e072fe99272d76ca126b2026d0 PODFILE CHECKSUM: 16208599a12443d53889ba2270a4985981cfb204 diff --git a/packages/veilid_support/example/pubspec.lock b/packages/veilid_support/example/pubspec.lock index a31cdd2..a3fee79 100644 --- a/packages/veilid_support/example/pubspec.lock +++ b/packages/veilid_support/example/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: "direct dev" description: name: async_tools - sha256: "972f68ab663724d86260a31e363c1355ff493308441b872bf4e7b8adc67c832c" + sha256: e783ac6ed5645c86da34240389bb3a000fc5e3ae6589c6a482eb24ece7217681 url: "https://pub.dev" source: hosted - version: "0.1.0" + version: "0.1.1" bloc: dependency: transitive description: @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: bloc_advanced_tools - sha256: bc0e1d5c26ae7df011464ab6abc2134dcfb668952acc87359abc7457cab091dd + sha256: "09f8a121d950575f1f2980c8b10df46b2ac6c72c8cbe48cc145871e5882ed430" url: "https://pub.dev" source: hosted - version: "0.1.0" + version: "0.1.1" boolean_selector: dependency: transitive description: @@ -283,10 +283,10 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" path_provider_linux: dependency: transitive description: diff --git a/packages/veilid_support/example/pubspec.yaml b/packages/veilid_support/example/pubspec.yaml index 8860e2d..1c73078 100644 --- a/packages/veilid_support/example/pubspec.yaml +++ b/packages/veilid_support/example/pubspec.yaml @@ -7,14 +7,14 @@ environment: sdk: '>=3.3.4 <4.0.0' dependencies: - cupertino_icons: ^1.0.6 + cupertino_icons: ^1.0.8 flutter: sdk: flutter veilid_support: path: ../ dev_dependencies: - async_tools: ^0.1.0 + async_tools: ^0.1.1 flutter_test: sdk: flutter integration_test: diff --git a/packages/veilid_support/lib/dht_support/proto/dht.proto b/packages/veilid_support/lib/dht_support/proto/dht.proto index 087cc9c..023c3cf 100644 --- a/packages/veilid_support/lib/dht_support/proto/dht.proto +++ b/packages/veilid_support/lib/dht_support/proto/dht.proto @@ -36,7 +36,7 @@ message DHTShortArray { // Uses the same writer as this DHTList with SMPL schema repeated veilid.TypedKey keys = 1; - // Item position index (uint8[256]) + // Item position index (uint8[256./]) // Actual item location is: // idx = index[n] + 1 (offset for header at idx 0) // key = idx / stride @@ -50,16 +50,11 @@ message DHTShortArray { // calculated through iteration } -// DHTLog - represents an appendable/truncatable log collection of individual elements -// Header in subkey 0 of first key follows this structure -// -// stride = descriptor subkey count on first key - 1 -// Subkeys 1..=stride on the first key are individual elements -// Subkeys 0..stride on the 'keys' keys are also individual elements -// -// Keys must use writable schema in order to make this list mutable +// DHTLog - represents a long ring buffer of elements utilizing a multi-level +// indirection table of DHTShortArrays. + message DHTLog { - // Other keys to concatenate + // Keys to concatenate repeated veilid.TypedKey keys = 1; // Back link to another DHTLog further back veilid.TypedKey back = 2; diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.g.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.g.dart index fadd8b8..12b3a1e 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.g.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.g.dart @@ -9,27 +9,27 @@ part of 'dht_record_pool.dart'; _$DHTRecordPoolAllocationsImpl _$$DHTRecordPoolAllocationsImplFromJson( Map json) => _$DHTRecordPoolAllocationsImpl( - childrenByParent: json['childrenByParent'] == null + childrenByParent: json['children_by_parent'] == null ? const IMapConst>({}) : IMap>>.fromJson( - json['childrenByParent'] as Map, + json['children_by_parent'] as Map, (value) => value as String, (value) => ISet>.fromJson(value, (value) => Typed.fromJson(value))), - parentByChild: json['parentByChild'] == null + parentByChild: json['parent_by_child'] == null ? const IMapConst({}) : IMap>.fromJson( - json['parentByChild'] as Map, + json['parent_by_child'] as Map, (value) => value as String, (value) => Typed.fromJson(value)), - rootRecords: json['rootRecords'] == null + rootRecords: json['root_records'] == null ? const ISetConst({}) - : ISet>.fromJson(json['rootRecords'], + : ISet>.fromJson(json['root_records'], (value) => Typed.fromJson(value)), - debugNames: json['debugNames'] == null + debugNames: json['debug_names'] == null ? const IMapConst({}) : IMap.fromJson( - json['debugNames'] as Map, + json['debug_names'] as Map, (value) => value as String, (value) => value as String), ); @@ -37,20 +37,20 @@ _$DHTRecordPoolAllocationsImpl _$$DHTRecordPoolAllocationsImplFromJson( Map _$$DHTRecordPoolAllocationsImplToJson( _$DHTRecordPoolAllocationsImpl instance) => { - 'childrenByParent': instance.childrenByParent.toJson( + 'children_by_parent': instance.childrenByParent.toJson( (value) => value, (value) => value.toJson( - (value) => value, + (value) => value.toJson(), ), ), - 'parentByChild': instance.parentByChild.toJson( - (value) => value, + 'parent_by_child': instance.parentByChild.toJson( (value) => value, + (value) => value.toJson(), ), - 'rootRecords': instance.rootRecords.toJson( - (value) => value, + 'root_records': instance.rootRecords.toJson( + (value) => value.toJson(), ), - 'debugNames': instance.debugNames.toJson( + 'debug_names': instance.debugNames.toJson( (value) => value, (value) => value, ), @@ -59,13 +59,13 @@ Map _$$DHTRecordPoolAllocationsImplToJson( _$OwnedDHTRecordPointerImpl _$$OwnedDHTRecordPointerImplFromJson( Map json) => _$OwnedDHTRecordPointerImpl( - recordKey: Typed.fromJson(json['recordKey']), + recordKey: Typed.fromJson(json['record_key']), owner: KeyPair.fromJson(json['owner']), ); Map _$$OwnedDHTRecordPointerImplToJson( _$OwnedDHTRecordPointerImpl instance) => { - 'recordKey': instance.recordKey, - 'owner': instance.owner, + 'record_key': instance.recordKey.toJson(), + 'owner': instance.owner.toJson(), }; diff --git a/packages/veilid_support/lib/src/identity.g.dart b/packages/veilid_support/lib/src/identity.g.dart index 7d3687e..616477a 100644 --- a/packages/veilid_support/lib/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['accountRecord']), + accountRecord: OwnedDHTRecordPointer.fromJson(json['account_record']), ); Map _$$AccountRecordInfoImplToJson( _$AccountRecordInfoImpl instance) => { - 'accountRecord': instance.accountRecord, + 'account_record': instance.accountRecord.toJson(), }; _$IdentityImpl _$$IdentityImplFromJson(Map json) => _$IdentityImpl( accountRecords: IMap>.fromJson( - json['accountRecords'] as Map, + json['account_records'] 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) => { - 'accountRecords': instance.accountRecords.toJson( + 'account_records': instance.accountRecords.toJson( (value) => value, (value) => value.toJson( - (value) => value, + (value) => value.toJson(), ), ), }; @@ -40,24 +40,24 @@ Map _$$IdentityImplToJson(_$IdentityImpl instance) => _$IdentityMasterImpl _$$IdentityMasterImplFromJson(Map json) => _$IdentityMasterImpl( identityRecordKey: - Typed.fromJson(json['identityRecordKey']), + Typed.fromJson(json['identity_record_key']), identityPublicKey: - FixedEncodedString43.fromJson(json['identityPublicKey']), + FixedEncodedString43.fromJson(json['identity_public_key']), masterRecordKey: - Typed.fromJson(json['masterRecordKey']), - masterPublicKey: FixedEncodedString43.fromJson(json['masterPublicKey']), + Typed.fromJson(json['master_record_key']), + masterPublicKey: FixedEncodedString43.fromJson(json['master_public_key']), identitySignature: - FixedEncodedString86.fromJson(json['identitySignature']), - masterSignature: FixedEncodedString86.fromJson(json['masterSignature']), + FixedEncodedString86.fromJson(json['identity_signature']), + masterSignature: FixedEncodedString86.fromJson(json['master_signature']), ); Map _$$IdentityMasterImplToJson( _$IdentityMasterImpl instance) => { - 'identityRecordKey': instance.identityRecordKey, - 'identityPublicKey': instance.identityPublicKey, - 'masterRecordKey': instance.masterRecordKey, - 'masterPublicKey': instance.masterPublicKey, - 'identitySignature': instance.identitySignature, - 'masterSignature': instance.masterSignature, + '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(), }; diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index 08ab7e2..cf16648 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.0" async: dependency: transitive description: @@ -36,25 +36,27 @@ packages: async_tools: dependency: "direct main" description: - path: "../../../dart_async_tools" - relative: true - source: path - version: "0.1.0" + name: async_tools + sha256: e783ac6ed5645c86da34240389bb3a000fc5e3ae6589c6a482eb24ece7217681 + url: "https://pub.dev" + source: hosted + version: "0.1.1" bloc: 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_advanced_tools: dependency: "direct main" description: - path: "../../../bloc_advanced_tools" - relative: true - source: path - version: "0.1.0" + name: bloc_advanced_tools + sha256: "09f8a121d950575f1f2980c8b10df46b2ac6c72c8cbe48cc145871e5882ed430" + url: "https://pub.dev" + source: hosted + version: "0.1.1" boolean_selector: dependency: transitive description: @@ -99,10 +101,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: @@ -123,10 +125,10 @@ packages: dependency: transitive description: name: built_value - sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.9.1" + version: "8.9.2" change_case: dependency: transitive description: @@ -187,10 +189,10 @@ packages: dependency: transitive description: name: coverage - sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" + sha256: "3945034e86ea203af7a056d98e98e42a5518fff200d6e8e6647e1886b07e936e" url: "https://pub.dev" source: hosted - version: "1.7.2" + version: "1.8.0" crypto: dependency: transitive description: @@ -219,10 +221,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: @@ -261,10 +263,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "57247f692f35f068cae297549a46a9a097100685c6780fe67177503eea5ed4e5" + sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1 url: "https://pub.dev" source: hosted - version: "2.4.7" + version: "2.5.2" freezed_annotation: dependency: "direct main" description: @@ -277,10 +279,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: @@ -341,18 +343,18 @@ packages: dependency: "direct main" description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b url: "https://pub.dev" source: hosted - version: "6.7.1" + version: "6.8.0" lint_hard: dependency: "direct dev" description: @@ -437,26 +439,26 @@ packages: dependency: transitive description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.4" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" path_provider_linux: dependency: transitive description: @@ -721,10 +723,10 @@ packages: dependency: transitive description: name: vm_service - sha256: a75f83f14ad81d5fe4b3319710b90dec37da0e22612326b696c9e1b8f34bbf48 + sha256: "7475cb4dd713d57b6f7464c0e13f06da0d535d8b2067e188962a59bac2cf280b" url: "https://pub.dev" source: hosted - version: "14.2.0" + version: "14.2.2" watcher: dependency: transitive description: @@ -745,10 +747,10 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.4.5" webkit_inspection_protocol: dependency: transitive description: @@ -761,10 +763,10 @@ packages: dependency: transitive description: name: win32 - sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480" + sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" url: "https://pub.dev" source: hosted - version: "5.3.0" + version: "5.5.0" xdg_directories: dependency: transitive description: diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index b4232e7..a7baeed 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -7,14 +7,14 @@ environment: sdk: '>=3.2.0 <4.0.0' dependencies: - async_tools: ^0.1.0 - bloc: ^8.1.3 - bloc_advanced_tools: ^0.1.0 + async_tools: ^0.1.1 + bloc: ^8.1.4 + bloc_advanced_tools: ^0.1.1 collection: ^1.18.0 equatable: ^2.0.5 - fast_immutable_collections: ^10.1.1 + fast_immutable_collections: ^10.2.2 freezed_annotation: ^2.4.1 - json_annotation: ^4.8.1 + json_annotation: ^4.9.0 loggy: ^2.0.3 meta: ^1.11.0 @@ -23,15 +23,9 @@ dependencies: # veilid: ^0.0.1 path: ../../../veilid/veilid-flutter -dependency_overrides: - async_tools: - path: ../../../dart_async_tools - bloc_advanced_tools: - path: ../../../bloc_advanced_tools - dev_dependencies: - build_runner: ^2.4.8 - freezed: ^2.4.7 - json_serializable: ^6.7.1 + build_runner: ^2.4.9 + freezed: ^2.5.2 + json_serializable: ^6.8.0 lint_hard: ^4.0.0 test: ^1.25.2 diff --git a/pubspec.lock b/pubspec.lock index 09d4fb0..21e4f2e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -63,7 +63,7 @@ packages: path: "../dart_async_tools" relative: true source: path - version: "0.1.0" + version: "0.1.1" awesome_extensions: dependency: "direct main" description: @@ -102,7 +102,7 @@ packages: path: "../bloc_advanced_tools" relative: true source: path - version: "0.1.0" + version: "0.1.1" blurry_modal_progress_hud: dependency: "direct main" description: @@ -738,18 +738,18 @@ packages: dependency: "direct main" description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b url: "https://pub.dev" source: hosted - version: "6.7.1" + version: "6.8.0" linkify: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index cd42f00..821c214 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,12 +11,12 @@ dependencies: animated_theme_switcher: ^2.0.10 ansicolor: ^2.0.2 archive: ^3.4.10 - async_tools: ^0.1.0 + async_tools: ^0.1.1 awesome_extensions: ^2.0.14 badges: ^3.1.2 basic_utils: ^5.7.0 bloc: ^8.1.4 - bloc_advanced_tools: ^0.1.0 + bloc_advanced_tools: ^0.1.1 blurry_modal_progress_hud: ^1.1.1 change_case: ^2.0.1 charcode: ^1.3.1 From c4d25fecb0e226b87ed8042bae1fb4e5ac6d70d2 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 13 May 2024 10:03:25 -0400 Subject: [PATCH 096/270] progress on dht_log --- .../cubits/single_contact_messages_cubit.dart | 4 +- lib/chat_list/cubits/chat_list_cubit.dart | 16 +- .../cubits/contact_invitation_list_cubit.dart | 10 +- lib/contacts/cubits/contact_list_cubit.dart | 10 +- lib/contacts/cubits/conversation_cubit.dart | 2 +- packages/veilid_support/build.yaml | 10 + .../test_dht_short_array.dart | 18 +- .../lib/dht_support/dht_support.dart | 2 + .../lib/dht_support/proto/dht.proto | 26 +- .../lib/dht_support/src/dht_log/barrel.dart | 2 + .../lib/dht_support/src/dht_log/dht_log.dart | 273 +++++++++ .../src/dht_log/dht_log_append.dart | 42 ++ .../src/dht_log/dht_log_cubit.dart | 119 ++++ .../dht_support/src/dht_log/dht_log_read.dart | 103 ++++ .../src/dht_log/dht_log_spine.dart | 527 ++++++++++++++++++ .../dht_record/default_dht_record_cubit.dart | 3 +- .../src/dht_record/dht_record.dart | 188 ++++--- .../src/dht_record/dht_record_cubit.dart | 2 +- .../src/dht_short_array/dht_short_array.dart | 88 ++- .../dht_short_array_cubit.dart | 34 +- .../dht_short_array/dht_short_array_head.dart | 21 +- .../dht_short_array/dht_short_array_read.dart | 99 +--- .../dht_short_array_write.dart | 128 +---- .../src/interfaces/dht_append_truncate.dart | 44 ++ .../src/interfaces/dht_openable.dart | 49 ++ .../src/interfaces/dht_random_read.dart | 63 +++ .../src/interfaces/dht_random_write.dart | 104 ++++ .../src/interfaces/exceptions.dart | 5 + .../src/interfaces/interfaces.dart | 4 + packages/veilid_support/lib/proto/dht.pb.dart | 124 ++--- .../veilid_support/lib/proto/dht.pbjson.dart | 32 +- packages/veilid_support/lib/src/identity.dart | 4 +- packages/veilid_support/lib/src/output.dart | 33 ++ .../veilid_support/lib/veilid_support.dart | 1 + pubspec.lock | 56 +- pubspec.yaml | 10 +- 36 files changed, 1754 insertions(+), 502 deletions(-) create mode 100644 packages/veilid_support/build.yaml create mode 100644 packages/veilid_support/lib/dht_support/src/dht_log/barrel.dart create mode 100644 packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart create mode 100644 packages/veilid_support/lib/dht_support/src/dht_log/dht_log_append.dart create mode 100644 packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart create mode 100644 packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart create mode 100644 packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart create mode 100644 packages/veilid_support/lib/dht_support/src/interfaces/dht_append_truncate.dart create mode 100644 packages/veilid_support/lib/dht_support/src/interfaces/dht_openable.dart create mode 100644 packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart create mode 100644 packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart create mode 100644 packages/veilid_support/lib/dht_support/src/interfaces/exceptions.dart create mode 100644 packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart create mode 100644 packages/veilid_support/lib/src/output.dart diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index de281fe..72c820e 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -227,7 +227,7 @@ class SingleContactMessagesCubit extends Cubit { } Future _reconcileMessagesInner( - {required DHTShortArrayWrite reconciledMessagesWriter, + {required DHTRandomReadWrite reconciledMessagesWriter, required IList messages}) async { // Ensure remoteMessages is sorted by timestamp final newMessages = messages @@ -236,7 +236,7 @@ class SingleContactMessagesCubit extends Cubit { // Existing messages will always be sorted by timestamp so merging is easy final existingMessages = await reconciledMessagesWriter - .getAllItemsProtobuf(proto.Message.fromBuffer); + .getItemRangeProtobuf(proto.Message.fromBuffer, 0); if (existingMessages == null) { throw Exception( 'Could not load existing reconciled messages at this time'); diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index 5023d51..9204a0a 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -92,31 +92,29 @@ class ChatListCubit extends DHTShortArrayCubit // Remove Chat from account's list // if this fails, don't keep retrying, user can try again later - final (deletedItem, success) = + final deletedItem = // Ensure followers get their changes before we return await syncFollowers(() => operateWrite((writer) async { if (activeChatCubit.state == remoteConversationRecordKey) { activeChatCubit.setActiveChat(null); } for (var i = 0; i < writer.length; i++) { - final cbuf = await writer.getItem(i); - if (cbuf == null) { + final c = + await writer.getItemProtobuf(proto.Chat.fromBuffer, i); + if (c == null) { throw Exception('Failed to get chat'); } - final c = proto.Chat.fromBuffer(cbuf); if (c.remoteConversationRecordKey == remoteConversationKey) { // Found the right chat - if (await writer.tryRemoveItem(i) != null) { - return c; - } - return null; + await writer.removeItem(i); + return c; } } return null; })); // Since followers are synced, we can safetly remove the reconciled // chat record now - if (success && deletedItem != null) { + if (deletedItem != null) { try { await DHTRecordPool.instance.deleteRecord( 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 8e133b1..afe91c0 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -177,7 +177,7 @@ class ContactInvitationListCubit _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; // Remove ContactInvitationRecord from account's list - final (deletedItem, success) = await operateWrite((writer) async { + final deletedItem = await operateWrite((writer) async { for (var i = 0; i < writer.length; i++) { final item = await writer.getItemProtobuf( proto.ContactInvitationRecord.fromBuffer, i); @@ -186,16 +186,14 @@ class ContactInvitationListCubit } if (item.contactRequestInbox.recordKey.toVeilid() == contactRequestInboxRecordKey) { - if (await writer.tryRemoveItem(i) != null) { - return item; - } - return null; + await writer.removeItem(i); + return item; } } return null; }); - if (success && deletedItem != null) { + if (deletedItem != null) { // Delete the contact request inbox final contactRequestInbox = deletedItem.contactRequestInbox.toVeilid(); await (await pool.openRecordOwned(contactRequestInbox, diff --git a/lib/contacts/cubits/contact_list_cubit.dart b/lib/contacts/cubits/contact_list_cubit.dart index e30c8ed..a139b89 100644 --- a/lib/contacts/cubits/contact_list_cubit.dart +++ b/lib/contacts/cubits/contact_list_cubit.dart @@ -70,7 +70,7 @@ class ContactListCubit extends DHTShortArrayCubit { contact.remoteConversationRecordKey.toVeilid(); // Remove Contact from account's list - final (deletedItem, success) = await operateWrite((writer) async { + final deletedItem = await operateWrite((writer) async { for (var i = 0; i < writer.length; i++) { final item = await writer.getItemProtobuf(proto.Contact.fromBuffer, i); if (item == null) { @@ -78,16 +78,14 @@ class ContactListCubit extends DHTShortArrayCubit { } if (item.remoteConversationRecordKey == contact.remoteConversationRecordKey) { - if (await writer.tryRemoveItem(i) != null) { - return item; - } - return null; + await writer.removeItem(i); + return item; } } return null; }); - if (success && deletedItem != null) { + if (deletedItem != null) { try { // Make a conversation cubit to manipulate the conversation final conversationCubit = ConversationCubit( diff --git a/lib/contacts/cubits/conversation_cubit.dart b/lib/contacts/cubits/conversation_cubit.dart index 253dbba..b4d8ee9 100644 --- a/lib/contacts/cubits/conversation_cubit.dart +++ b/lib/contacts/cubits/conversation_cubit.dart @@ -295,7 +295,7 @@ class ConversationCubit extends Cubit> { debugName: 'ConversationCubit::initLocalMessages::LocalMessages', parent: localConversationKey, crypto: crypto, - smplWriter: writer)) + writer: writer)) .deleteScope((messages) async => await callback(messages)); } diff --git a/packages/veilid_support/build.yaml b/packages/veilid_support/build.yaml new file mode 100644 index 0000000..84fde8c --- /dev/null +++ b/packages/veilid_support/build.yaml @@ -0,0 +1,10 @@ +targets: + $default: + sources: + exclude: + - example/** + builders: + json_serializable: + options: + explicit_to_json: true + field_rename: snake diff --git a/packages/veilid_support/example/integration_test/test_dht_short_array.dart b/packages/veilid_support/example/integration_test/test_dht_short_array.dart index ad3e22a..c2fcc2b 100644 --- a/packages/veilid_support/example/integration_test/test_dht_short_array.dart +++ b/packages/veilid_support/example/integration_test/test_dht_short_array.dart @@ -63,7 +63,7 @@ Future Function() makeTestDHTShortArrayAdd({required int stride}) => print('adding\n'); { - final (res, ok) = await arr.operateWrite((w) async { + final res = await arr.operateWrite((w) async { for (var n = 0; n < dataset.length; n++) { print('$n '); final success = await w.tryAddItem(dataset[n]); @@ -71,26 +71,28 @@ Future Function() makeTestDHTShortArrayAdd({required int stride}) => } }); expect(res, isNull); - expect(ok, isTrue); } //print('get all\n'); { - final dataset2 = await arr.operate((r) async => r.getAllItems()); + final dataset2 = await arr.operate((r) async => r.getItemRange(0)); expect(dataset2, equals(dataset)); } + { + final dataset3 = + await arr.operate((r) async => r.getItemRange(64, length: 128)); + expect(dataset3, equals(dataset.sublist(64, 64 + 128))); + } //print('clear\n'); { - final (res, ok) = await arr.operateWrite((w) async => w.tryClear()); - expect(res, isTrue); - expect(ok, isTrue); + await arr.operateWrite((w) async => w.clear()); } //print('get all\n'); { - final dataset3 = await arr.operate((r) async => r.getAllItems()); - expect(dataset3, isEmpty); + final dataset4 = await arr.operate((r) async => r.getItemRange(0)); + expect(dataset4, isEmpty); } await arr.delete(); diff --git a/packages/veilid_support/lib/dht_support/dht_support.dart b/packages/veilid_support/lib/dht_support/dht_support.dart index 869a267..cc2a8be 100644 --- a/packages/veilid_support/lib/dht_support/dht_support.dart +++ b/packages/veilid_support/lib/dht_support/dht_support.dart @@ -2,5 +2,7 @@ library dht_support; +export 'src/dht_log/barrel.dart'; export 'src/dht_record/barrel.dart'; export 'src/dht_short_array/barrel.dart'; +export 'src/interfaces/interfaces.dart'; diff --git a/packages/veilid_support/lib/dht_support/proto/dht.proto b/packages/veilid_support/lib/dht_support/proto/dht.proto index 023c3cf..6796753 100644 --- a/packages/veilid_support/lib/dht_support/proto/dht.proto +++ b/packages/veilid_support/lib/dht_support/proto/dht.proto @@ -23,6 +23,18 @@ message DHTData { uint32 size = 4; } + +// DHTLog - represents a ring buffer of many elements with append/truncate semantics +// Header in subkey 0 of first key follows this structure +message DHTLog { + // Position of the start of the log (oldest items) + uint32 head = 1; + // Position of the end of the log (newest items) + uint32 tail = 2; + // Stride of each segment of the dhtlog + uint32 stride = 3; +} + // DHTShortArray - represents a re-orderable collection of up to 256 individual elements // Header in subkey 0 of first key follows this structure // @@ -50,20 +62,6 @@ message DHTShortArray { // calculated through iteration } -// DHTLog - represents a long ring buffer of elements utilizing a multi-level -// indirection table of DHTShortArrays. - -message DHTLog { - // Keys to concatenate - repeated veilid.TypedKey keys = 1; - // Back link to another DHTLog further back - veilid.TypedKey back = 2; - // Count of subkeys in all keys in this DHTLog - repeated uint32 subkey_counts = 3; - // Total count of subkeys in all keys in this DHTLog including all backlogs - uint32 total_subkeys = 4; -} - // DataReference // Pointer to data somewhere in Veilid // Abstraction over DHTData and BlockStore diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/barrel.dart b/packages/veilid_support/lib/dht_support/src/dht_log/barrel.dart new file mode 100644 index 0000000..18686f2 --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/dht_log/barrel.dart @@ -0,0 +1,2 @@ +export 'dht_array.dart'; +export 'dht_array_cubit.dart'; diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart new file mode 100644 index 0000000..a132bdb --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart @@ -0,0 +1,273 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:async_tools/async_tools.dart'; +import 'package:collection/collection.dart'; +import 'package:equatable/equatable.dart'; + +import '../../../veilid_support.dart'; +import '../../proto/proto.dart' as proto; +import '../interfaces/dht_append_truncate.dart'; + +part 'dht_log_spine.dart'; +part 'dht_log_read.dart'; +part 'dht_log_append.dart'; + +/////////////////////////////////////////////////////////////////////// + +/// DHTLog is a ring-buffer queue like data structure with the following +/// operations: +/// * Add elements to the tail +/// * Remove elements from the head +/// The structure has a 'spine' record that acts as an indirection table of +/// DHTShortArray record pointers spread over its subkeys. +/// Subkey 0 of the DHTLog is a head subkey that contains housekeeping data: +/// * The head and tail position of the log +/// - subkeyIdx = pos / recordsPerSubkey +/// - recordIdx = pos % recordsPerSubkey +class DHTLog implements DHTOpenable { + //////////////////////////////////////////////////////////////// + // Constructors + + DHTLog._({required _DHTLogSpine spine}) : _spine = spine { + _spine.onUpdatedSpine = () { + _watchController?.sink.add(null); + }; + } + + /// Create a DHTLog + static Future create( + {required String debugName, + int stride = DHTShortArray.maxElements, + VeilidRoutingContext? routingContext, + TypedKey? parent, + DHTRecordCrypto? crypto, + KeyPair? writer}) async { + assert(stride <= DHTShortArray.maxElements, 'stride too long'); + final pool = DHTRecordPool.instance; + + late final DHTRecord spineRecord; + if (writer != null) { + final schema = DHTSchema.smpl( + oCnt: 0, + members: [DHTSchemaMember(mKey: writer.key, mCnt: spineSubkeys + 1)]); + spineRecord = await pool.createRecord( + debugName: debugName, + parent: parent, + routingContext: routingContext, + schema: schema, + crypto: crypto, + writer: writer); + } else { + const schema = DHTSchema.dflt(oCnt: spineSubkeys + 1); + spineRecord = await pool.createRecord( + debugName: debugName, + parent: parent, + routingContext: routingContext, + schema: schema, + crypto: crypto); + } + + try { + final spine = await _DHTLogSpine.create( + spineRecord: spineRecord, segmentStride: stride); + return DHTLog._(spine: spine); + } on Exception catch (_) { + await spineRecord.close(); + await spineRecord.delete(); + rethrow; + } + } + + static Future openRead(TypedKey logRecordKey, + {required String debugName, + VeilidRoutingContext? routingContext, + TypedKey? parent, + DHTRecordCrypto? crypto}) async { + final spineRecord = await DHTRecordPool.instance.openRecordRead( + logRecordKey, + debugName: debugName, + parent: parent, + routingContext: routingContext, + crypto: crypto); + try { + final spine = await _DHTLogSpine.load(spineRecord: spineRecord); + final dhtLog = DHTLog._(spine: spine); + return dhtLog; + } on Exception catch (_) { + await spineRecord.close(); + rethrow; + } + } + + static Future openWrite( + TypedKey logRecordKey, + KeyPair writer, { + required String debugName, + VeilidRoutingContext? routingContext, + TypedKey? parent, + DHTRecordCrypto? crypto, + }) async { + final spineRecord = await DHTRecordPool.instance.openRecordWrite( + logRecordKey, writer, + debugName: debugName, + parent: parent, + routingContext: routingContext, + crypto: crypto); + try { + final spine = await _DHTLogSpine.load(spineRecord: spineRecord); + final dhtLog = DHTLog._(spine: spine); + return dhtLog; + } on Exception catch (_) { + await spineRecord.close(); + rethrow; + } + } + + static Future openOwned( + OwnedDHTRecordPointer ownedLogRecordPointer, { + required String debugName, + required TypedKey parent, + VeilidRoutingContext? routingContext, + DHTRecordCrypto? crypto, + }) => + openWrite( + ownedLogRecordPointer.recordKey, + ownedLogRecordPointer.owner, + debugName: debugName, + routingContext: routingContext, + parent: parent, + crypto: crypto, + ); + + //////////////////////////////////////////////////////////////////////////// + // DHTOpenable + + /// Check if the DHTLog is open + @override + bool get isOpen => _spine.isOpen; + + /// Free all resources for the DHTLog + @override + Future close() async { + if (!isOpen) { + return; + } + await _watchController?.close(); + _watchController = null; + await _spine.close(); + } + + /// Free all resources for the DHTLog and delete it from the DHT + /// Will wait until the short array is closed to delete it + @override + Future delete() async { + await _spine.delete(); + } + + //////////////////////////////////////////////////////////////////////////// + // Public API + + /// Get the record key for this log + TypedKey get recordKey => _spine.recordKey; + + /// Get the record pointer foir this log + OwnedDHTRecordPointer get recordPointer => _spine.recordPointer; + + /// Runs a closure allowing read-only access to the log + Future operate(Future Function(DHTRandomRead) closure) async { + if (!isOpen) { + throw StateError('log is not open"'); + } + + return _spine.operate((spine) async { + final reader = _DHTLogRead._(spine); + return closure(reader); + }); + } + + /// Runs a closure allowing append/truncate access to the log + /// Makes only one attempt to consistently write the changes to the DHT + /// Returns result of the closure if the write could be performed + /// Throws DHTOperateException if the write could not be performed + /// at this time + Future operateAppend( + Future Function(DHTAppendTruncateRandomRead) closure) async { + if (!isOpen) { + throw StateError('log is not open"'); + } + + return _spine.operateAppend((spine) async { + final writer = _DHTLogAppend._(spine); + return closure(writer); + }); + } + + /// Runs a closure allowing append/truncate access to the log + /// 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 operateAppendEventual( + Future Function(DHTAppendTruncateRandomRead) closure, + {Duration? timeout}) async { + if (!isOpen) { + throw StateError('log is not open"'); + } + + return _spine.operateAppendEventual((spine) async { + final writer = _DHTLogAppend._(spine); + return closure(writer); + }, timeout: timeout); + } + + /// Listen to and any all changes to the structure of this log + /// regardless of where the changes are coming from + Future> listen( + void Function() onChanged, + ) { + if (!isOpen) { + throw StateError('log is not open"'); + } + + return _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 _spine.cancelWatch(); + _watchController = null; + })); + }); + + // Start watching head subkey of the spine + await _spine.watch(); + } + // Return subscription + return _watchController!.stream.listen((_) => onChanged()); + }); + } + + //////////////////////////////////////////////////////////////// + // Fields + + // 56 subkeys * 512 segments * 36 bytes per typedkey = + // 1032192 bytes per record + // 512*36 = 18432 bytes per subkey + // 28672 shortarrays * 256 elements = 7340032 elements + static const spineSubkeys = 56; + static const segmentsPerSubkey = 512; + + // Internal representation refreshed from spine record + final _DHTLogSpine _spine; + + // 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_log/dht_log_append.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_append.dart new file mode 100644 index 0000000..6a172a7 --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_append.dart @@ -0,0 +1,42 @@ +part of 'dht_log.dart'; + +//////////////////////////////////////////////////////////////////////////// +// Append/truncate implementation + +class _DHTLogAppend extends _DHTLogRead implements DHTAppendTruncateRandomRead { + _DHTLogAppend._(super.spine) : super._(); + + @override + Future tryAppendItem(Uint8List value) async { + // Allocate empty index at the end of the list + final endPos = _spine.length; + _spine.allocateTail(1); + final lookup = await _spine.lookupPosition(endPos); + if (lookup == null) { + throw StateError("can't write to dht log"); + } + // Write item to the segment + return lookup.shortArray + .operateWrite((write) async => write.tryWriteItem(lookup.pos, value)); + } + + @override + Future truncate(int count) async { + final len = _spine.length; + if (count > len) { + count = len; + } + if (count == 0) { + return; + } + if (count < 0) { + throw StateError('can not remove negative items'); + } + _spine.releaseHead(count); + } + + @override + Future clear() async { + _spine.releaseHead(_spine.length); + } +} diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart new file mode 100644 index 0000000..a7d5333 --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart @@ -0,0 +1,119 @@ +import 'dart:async'; + +import 'package:async_tools/async_tools.dart'; +import 'package:bloc/bloc.dart'; +import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; +import 'package:equatable/equatable.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:meta/meta.dart'; + +import '../../../veilid_support.dart'; + +// xxx paginate and remember to paginate watches (could use short array cubit as a subcubit here?) + +// @immutable +// class DHTArrayElementState extends Equatable { +// const DHTArrayElementState( +// {required this.value, required this.isOffline}); +// final T value; +// final bool isOffline; + +// @override +// List get props => [value, isOffline]; +// } + +// typedef DHTArrayState = AsyncValue>>; +// typedef DHTArrayBusyState = BlocBusyState>; + +// class DHTArrayCubit extends Cubit> +// with BlocBusyWrapper> { +// DHTArrayCubit({ +// required Future Function() open, +// required T Function(List data) decodeElement, +// }) : _decodeElement = decodeElement, +// super(const BlocBusyState(AsyncValue.loading())) { +// _initWait.add(() async { +// // Open DHT record +// _array = await open(); +// _wantsCloseRecord = true; + +// // Make initial state update +// await _refreshNoWait(); +// _subscription = await _array.listen(_update); +// }); +// } + +// Future refresh({bool forceRefresh = false}) async { +// await _initWait(); +// await _refreshNoWait(forceRefresh: forceRefresh); +// } + +// Future _refreshNoWait({bool forceRefresh = false}) async => +// busy((emit) async => _refreshInner(emit, forceRefresh: forceRefresh)); + +// Future _refreshInner(void Function(DHTShortArrayState) emit, +// {bool forceRefresh = false}) async { +// try { +// final newState = await _shortArray.operate((reader) async { +// final offlinePositions = await reader.getOfflinePositions(); +// final allItems = (await reader.getAllItems(forceRefresh: forceRefresh)) +// ?.indexed +// .map((x) => DHTShortArrayElementState( +// value: _decodeElement(x.$2), +// isOffline: offlinePositions.contains(x.$1))) +// .toIList(); +// return allItems; +// }); +// if (newState != null) { +// 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. Only called after init future has run +// // so we dont have to wait for that here. +// _sspUpdate.busyUpdate>( +// busy, (emit) async => _refreshInner(emit)); +// } + +// @override +// Future close() async { +// await _initWait(); +// await _subscription?.cancel(); +// _subscription = null; +// if (_wantsCloseRecord) { +// await _shortArray.close(); +// } +// await super.close(); +// } + +// Future operate(Future Function(DHTShortArrayRead) closure) async { +// await _initWait(); +// return _shortArray.operate(closure); +// } + +// Future<(R?, bool)> operateWrite( +// Future Function(DHTShortArrayWrite) closure) async { +// await _initWait(); +// return _shortArray.operateWrite(closure); +// } + +// Future operateWriteEventual( +// Future Function(DHTShortArrayWrite) closure, +// {Duration? timeout}) async { +// await _initWait(); +// return _shortArray.operateWriteEventual(closure, timeout: timeout); +// } + +// final WaitSet _initWait = WaitSet(); +// late final DHTShortArray _shortArray; +// final T Function(List data) _decodeElement; +// StreamSubscription? _subscription; +// bool _wantsCloseRecord = false; +// final _sspUpdate = SingleStatelessProcessor(); +// } diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart new file mode 100644 index 0000000..0919412 --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart @@ -0,0 +1,103 @@ +part of 'dht_log.dart'; + +//////////////////////////////////////////////////////////////////////////// +// Reader-only implementation + +class _DHTLogRead implements DHTRandomRead { + _DHTLogRead._(_DHTLogSpine spine) : _spine = spine; + + @override + int get length => _spine.length; + + @override + Future getItem(int pos, {bool forceRefresh = false}) async { + if (pos < 0 || pos >= length) { + throw IndexError.withLength(pos, length); + } + final lookup = await _spine.lookupPosition(pos); + if (lookup == null) { + return null; + } + + return lookup.shortArray.operate( + (read) => read.getItem(lookup.pos, forceRefresh: forceRefresh)); + } + + (int, int) _clampStartLen(int start, int? len) { + len ??= _spine.length; + if (start < 0) { + throw IndexError.withLength(start, _spine.length); + } + if (start > _spine.length) { + throw IndexError.withLength(start, _spine.length); + } + if ((len + start) > _spine.length) { + len = _spine.length - start; + } + return (start, len); + } + + @override + Future?> getItemRange(int start, + {int? length, bool forceRefresh = false}) async { + final out = []; + (start, length) = _clampStartLen(start, length); + + final chunks = Iterable.generate(length).slices(maxDHTConcurrency).map( + (chunk) => chunk + .map((pos) => getItem(pos + start, forceRefresh: forceRefresh))); + + for (final chunk in chunks) { + final elems = await chunk.wait; + if (elems.contains(null)) { + return null; + } + out.addAll(elems.cast()); + } + + return out; + } + + @override + Future> getOfflinePositions() async { + final positionOffline = {}; + + // Iterate positions backward from most recent + for (var pos = _spine.length - 1; pos >= 0; pos--) { + final lookup = await _spine.lookupPosition(pos); + if (lookup == null) { + throw StateError('Unable to look up position'); + } + + // Check each segment for offline positions + var foundOffline = false; + await lookup.shortArray.operate((read) async { + final segmentOffline = await read.getOfflinePositions(); + + // For each shortarray segment go through their segment positions + // in reverse order and see if they are offline + for (var segmentPos = lookup.pos; + segmentPos >= 0 && pos >= 0; + segmentPos--, pos--) { + // If the position in the segment is offline, then + // mark the position in the log as offline + if (segmentOffline.contains(segmentPos)) { + positionOffline.add(pos); + foundOffline = true; + } + } + }); + + // If we found nothing offline in this segment then we can stop + if (!foundOffline) { + break; + } + } + + return positionOffline; + } + + //////////////////////////////////////////////////////////////////////////// + // Fields + final _DHTLogSpine _spine; +} diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart new file mode 100644 index 0000000..76a3f0c --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -0,0 +1,527 @@ +part of 'dht_log.dart'; + +class DHTLogPositionLookup { + const DHTLogPositionLookup({required this.shortArray, required this.pos}); + final DHTShortArray shortArray; + final int pos; +} + +class _DHTLogSegmentLookup extends Equatable { + const _DHTLogSegmentLookup({required this.subkey, required this.segment}); + final int subkey; + final int segment; + + @override + List get props => [subkey, segment]; +} + +class _DHTLogSpine { + _DHTLogSpine._( + {required DHTRecord spineRecord, + required int head, + required int tail, + required int stride}) + : _spineRecord = spineRecord, + _head = head, + _tail = tail, + _segmentStride = stride, + _spineCache = []; + + // Create a new spine record and push it to the network + static Future<_DHTLogSpine> create( + {required DHTRecord spineRecord, required int segmentStride}) async { + // Construct new spinehead + final spine = _DHTLogSpine._( + spineRecord: spineRecord, head: 0, tail: 0, stride: segmentStride); + + // Write new spine head record to the network + await spine.operate((spine) async { + final success = await spine.writeSpineHead(); + assert(success, 'false return should never happen on create'); + }); + + return spine; + } + + // Pull the latest or updated copy of the spine head record from the network + static Future<_DHTLogSpine> load({required DHTRecord spineRecord}) async { + // Get an updated spine head record copy if one exists + final spineHead = await spineRecord.getProtobuf(proto.DHTLog.fromBuffer, + subkey: 0, refreshMode: DHTRecordRefreshMode.refresh); + if (spineHead == null) { + throw StateError('spine head missing during refresh'); + } + return _DHTLogSpine._( + spineRecord: spineRecord, + head: spineHead.head, + tail: spineHead.tail, + stride: spineHead.stride); + } + + proto.DHTLog _toProto() { + assert(_spineMutex.isLocked, 'should be in mutex here'); + + final logHead = proto.DHTLog() + ..head = _head + ..tail = _tail + ..stride = _segmentStride; + return logHead; + } + + Future close() async { + await _spineMutex.protect(() async { + if (!isOpen) { + return; + } + final futures = >[_spineRecord.close()]; + for (final (_, sc) in _spineCache) { + futures.add(sc.close()); + } + await Future.wait(futures); + }); + } + + Future delete() async { + await _spineMutex.protect(() async { + final pool = DHTRecordPool.instance; + final futures = >[pool.deleteRecord(_spineRecord.key)]; + for (final (_, sc) in _spineCache) { + futures.add(sc.delete()); + } + await Future.wait(futures); + }); + } + + Future operate(Future Function(_DHTLogSpine) closure) async => + // ignore: prefer_expression_function_bodies + _spineMutex.protect(() async { + return closure(this); + }); + + Future operateAppend(Future Function(_DHTLogSpine) closure) async => + _spineMutex.protect(() async { + final oldHead = _head; + final oldTail = _tail; + try { + final out = await closure(this); + // Write head assuming it has been changed + if (!await writeSpineHead()) { + // Failed to write head means head got overwritten so write should + // be considered failed + throw DHTExceptionTryAgain(); + } + + onUpdatedSpine?.call(); + return out; + } on Exception { + // Exception means state needs to be reverted + _head = oldHead; + _tail = oldTail; + rethrow; + } + }); + + Future operateAppendEventual( + Future Function(_DHTLogSpine) closure, + {Duration? timeout}) async { + final timeoutTs = timeout == null + ? null + : Veilid.instance.now().offset(TimestampDuration.fromDuration(timeout)); + + await _spineMutex.protect(() async { + late int oldHead; + late int oldTail; + + try { + // Iterate until we have a successful element and head write + + do { + // Save off old values each pass of writeSpineHead because the head + // will have changed + oldHead = _head; + oldTail = _tail; + + // Try to do the element write + while (true) { + if (timeoutTs != null) { + final now = Veilid.instance.now(); + if (now >= timeoutTs) { + throw TimeoutException('timeout reached'); + } + } + if (await closure(this)) { + break; + } + // Failed to write in closure resets state + _head = oldHead; + _tail = oldTail; + } + + // Try to do the head write + } while (!await writeSpineHead()); + + onUpdatedSpine?.call(); + } on Exception { + // Exception means state needs to be reverted + _head = oldHead; + _tail = oldTail; + rethrow; + } + }); + } + + /// Serialize and write out the current spine head subkey, possibly updating + /// it if a newer copy is available online. Returns true if the write was + /// successful + Future writeSpineHead() async { + assert(_spineMutex.isLocked, 'should be in mutex here'); + + final headBuffer = _toProto().writeToBuffer(); + + final existingData = await _spineRecord.tryWriteBytes(headBuffer); + if (existingData != null) { + // Head write failed, incorporate update + await _updateHead(proto.DHTLog.fromBuffer(existingData)); + return false; + } + + return true; + } + + /// Validate a new spine head subkey that has come in from the network + Future _updateHead(proto.DHTLog spineHead) async { + assert(_spineMutex.isLocked, 'should be in mutex here'); + + _head = spineHead.head; + _tail = spineHead.tail; + } + + ///////////////////////////////////////////////////////////////////////////// + // Spine element management + + static final Uint8List _emptySegmentKey = + Uint8List.fromList(List.filled(TypedKey.decodedLength(), 0)); + static Uint8List _makeEmptySubkey() => Uint8List.fromList(List.filled( + DHTLog.segmentsPerSubkey * TypedKey.decodedLength(), 0)); + + static TypedKey? _getSegmentKey(Uint8List subkeyData, int segment) { + final decodedLength = TypedKey.decodedLength(); + final segmentKeyBytes = subkeyData.sublist( + decodedLength * segment, (decodedLength + 1) * segment); + if (segmentKeyBytes.equals(_emptySegmentKey)) { + return null; + } + return TypedKey.fromBytes(segmentKeyBytes); + } + + static void _setSegmentKey( + Uint8List subkeyData, int segment, TypedKey? segmentKey) { + final decodedLength = TypedKey.decodedLength(); + late final Uint8List segmentKeyBytes; + if (segmentKey == null) { + segmentKeyBytes = _emptySegmentKey; + } else { + segmentKeyBytes = segmentKey.decode(); + } + subkeyData.setRange(decodedLength * segment, (decodedLength + 1) * segment, + segmentKeyBytes); + } + + Future _getOrCreateSegmentInner(int segmentNumber) async { + assert(_spineMutex.isLocked, 'should be in mutex here'); + assert(_spineRecord.writer != null, 'should be writable'); + + // Lookup what subkey and segment subrange has this position's segment + // shortarray + final l = lookupSegment(segmentNumber); + final subkey = l.subkey; + final segment = l.segment; + + var subkeyData = await _spineRecord.get(subkey: subkey); + subkeyData ??= _makeEmptySubkey(); + while (true) { + final segmentKey = _getSegmentKey(subkeyData!, segment); + if (segmentKey == null) { + // Create a shortarray segment + final segmentRec = await DHTShortArray.create( + debugName: '${_spineRecord.debugName}_spine_${subkey}_$segment', + stride: _segmentStride, + crypto: _spineRecord.crypto, + parent: _spineRecord.key, + routingContext: _spineRecord.routingContext, + writer: _spineRecord.writer, + ); + var success = false; + try { + // Write it back to the spine record + _setSegmentKey(subkeyData, segment, segmentRec.recordKey); + subkeyData = + await _spineRecord.tryWriteBytes(subkeyData, subkey: subkey); + // If the write was successful then we're done + if (subkeyData == null) { + // Return it + success = true; + return segmentRec; + } + } finally { + if (!success) { + await segmentRec.close(); + await segmentRec.delete(); + } + } + } else { + // Open a shortarray segment + final segmentRec = await DHTShortArray.openWrite( + segmentKey, + _spineRecord.writer!, + debugName: '${_spineRecord.debugName}_spine_${subkey}_$segment', + crypto: _spineRecord.crypto, + parent: _spineRecord.key, + routingContext: _spineRecord.routingContext, + ); + return segmentRec; + } + // Loop if we need to try again with the new data from the network + } + } + + Future _getSegmentInner(int segmentNumber) async { + assert(_spineMutex.isLocked, 'should be in mutex here'); + + // Lookup what subkey and segment subrange has this position's segment + // shortarray + final l = lookupSegment(segmentNumber); + final subkey = l.subkey; + final segment = l.segment; + + final subkeyData = await _spineRecord.get(subkey: subkey); + if (subkeyData == null) { + return null; + } + final segmentKey = _getSegmentKey(subkeyData, segment); + if (segmentKey == null) { + return null; + } + + // Open a shortarray segment + final segmentRec = await DHTShortArray.openRead( + segmentKey, + debugName: '${_spineRecord.debugName}_spine_${subkey}_$segment', + crypto: _spineRecord.crypto, + parent: _spineRecord.key, + routingContext: _spineRecord.routingContext, + ); + return segmentRec; + } + + Future getOrCreateSegment(int segmentNumber) async { + assert(_spineMutex.isLocked, 'should be in mutex here'); + + // See if we already have this in the cache + for (var i = 0; i < _spineCache.length; i++) { + if (_spineCache[i].$1 == segmentNumber) { + // Touch the element + final x = _spineCache.removeAt(i); + _spineCache.add(x); + // Return the shortarray for this position + return x.$2; + } + } + + // If we don't have it in the cache, get/create it and then cache it + final segment = await _getOrCreateSegmentInner(segmentNumber); + _spineCache.add((segmentNumber, segment)); + if (_spineCache.length > _spineCacheLength) { + // Trim the LRU cache + _spineCache.removeAt(0); + } + return segment; + } + + Future getSegment(int segmentNumber) async { + assert(_spineMutex.isLocked, 'should be in mutex here'); + + // See if we already have this in the cache + for (var i = 0; i < _spineCache.length; i++) { + if (_spineCache[i].$1 == segmentNumber) { + // Touch the element + final x = _spineCache.removeAt(i); + _spineCache.add(x); + // Return the shortarray for this position + return x.$2; + } + } + + // If we don't have it in the cache, get it and then cache it + final segment = await _getSegmentInner(segmentNumber); + if (segment == null) { + return null; + } + _spineCache.add((segmentNumber, segment)); + if (_spineCache.length > _spineCacheLength) { + // Trim the LRU cache + _spineCache.removeAt(0); + } + return segment; + } + + _DHTLogSegmentLookup lookupSegment(int segmentNumber) { + assert(_spineMutex.isLocked, 'should be in mutex here'); + + if (segmentNumber < 0) { + throw IndexError.withLength( + segmentNumber, DHTLog.spineSubkeys * DHTLog.segmentsPerSubkey); + } + final subkey = segmentNumber ~/ DHTLog.segmentsPerSubkey; + if (subkey >= DHTLog.spineSubkeys) { + throw IndexError.withLength( + segmentNumber, DHTLog.spineSubkeys * DHTLog.segmentsPerSubkey); + } + final segment = segmentNumber % DHTLog.segmentsPerSubkey; + return _DHTLogSegmentLookup(subkey: subkey + 1, segment: segment); + } + + /////////////////////////////////////////// + // API for public interfaces + + Future lookupPosition(int pos) async { + assert(_spineMutex.isLocked, 'should be locked'); + + // Check if our position is in bounds + final endPos = length; + if (pos < 0 || pos >= endPos) { + throw IndexError.withLength(pos, endPos); + } + + // Calculate absolute position, ring-buffer style + final absolutePosition = (_head + pos) % _positionLimit; + + // Determine the segment number and position within the segment + final segmentNumber = absolutePosition ~/ DHTShortArray.maxElements; + final segmentPos = absolutePosition % DHTShortArray.maxElements; + + // Get the segment shortArray + final shortArray = (_spineRecord.writer == null) + ? await getSegment(segmentNumber) + : await getOrCreateSegment(segmentNumber); + if (shortArray == null) { + return null; + } + return DHTLogPositionLookup(shortArray: shortArray, pos: segmentPos); + } + + void allocateTail(int count) { + assert(_spineMutex.isLocked, 'should be locked'); + + final currentLength = length; + if (count <= 0) { + throw StateError('count should be > 0'); + } + if (currentLength + count >= _positionLimit) { + throw StateError('ring buffer overflow'); + } + + _tail = (_tail + count) % _positionLimit; + } + + void releaseHead(int count) { + assert(_spineMutex.isLocked, 'should be locked'); + + final currentLength = length; + if (count <= 0) { + throw StateError('count should be > 0'); + } + if (count > currentLength) { + throw StateError('ring buffer underflow'); + } + + _head = (_head + count) % _positionLimit; + } + + ///////////////////////////////////////////////////////////////////////////// + // Watch For Updates + + // Watch head for changes + Future watch() async { + // This will update any existing watches if necessary + try { + await _spineRecord.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 _spineRecord.listen(localChanges: false, _onSpineChanged); + } 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 _spineRecord.cancelWatch(); + await _subscription?.cancel(); + _subscription = null; + } + + // Called when the log changes online and we find out from a watch + // but not when we make a change locally + Future _onSpineChanged( + DHTRecord record, Uint8List? data, List subkeys) async { + // If head record subkey zero changes, then the layout + // of the dhtshortarray has changed + if (data == null) { + throw StateError('spine head changed without data'); + } + if (record.key != _spineRecord.key || + subkeys.length != 1 || + subkeys[0] != ValueSubkeyRange.single(0)) { + throw StateError('watch returning wrong subkey range'); + } + + // Decode updated head + final headData = proto.DHTLog.fromBuffer(data); + + // Then update the head record + await _spineMutex.protect(() async { + await _updateHead(headData); + onUpdatedSpine?.call(); + }); + } + + //////////////////////////////////////////////////////////////////////////// + + TypedKey get recordKey => _spineRecord.key; + OwnedDHTRecordPointer get recordPointer => _spineRecord.ownedDHTRecordPointer; + int get length => + (_tail < _head) ? (_positionLimit - _head) + _tail : _tail - _head; + bool get isOpen => _spineRecord.isOpen; + + static const _positionLimit = DHTLog.segmentsPerSubkey * + DHTLog.spineSubkeys * + DHTShortArray.maxElements; + + // Spine head mutex to ensure we keep the representation valid + final Mutex _spineMutex = Mutex(); + // Subscription to head record internal changes + StreamSubscription? _subscription; + // Notify closure for external spine head changes + void Function()? onUpdatedSpine; + + // Spine DHT record + final DHTRecord _spineRecord; + + // Position of the start of the log (oldest items) + int _head; + // Position of the end of the log (newest items) + int _tail; + + // LRU cache of DHT spine elements accessed recently + // Pair of position and associated shortarray segment + final List<(int, DHTShortArray)> _spineCache; + static const int _spineCacheLength = 3; + // Segment stride to use for spine elements + final int _segmentStride; +} diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart index 1cf97d5..0b4e0b6 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart @@ -38,7 +38,8 @@ class DefaultDHTRecordCubit extends DHTRecordCubit { final Uint8List data; final firstSubkey = subkeys.firstOrNull!.low; if (firstSubkey != defaultSubkey || updatedata == null) { - final maybeData = await record.get(forceRefresh: true); + final maybeData = + await record.get(refreshMode: DHTRecordRefreshMode.refresh); if (maybeData == null) { return null; } 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 ccb09f8..3d625f8 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 @@ -13,9 +13,22 @@ class DHTRecordWatchChange extends Equatable { List get props => [local, data, subkeys]; } +/// Refresh mode for DHT record 'get' +enum DHTRecordRefreshMode { + /// Return existing subkey values if they exist locally already + existing, + + /// Always check the network for a newer subkey value + refresh, + + /// Always check the network for a newer subkey value but only + /// return that value if its sequence number is newer than the local value + refreshOnlyUpdates, +} + ///////////////////////////////////////////////// -class DHTRecord { +class DHTRecord implements DHTOpenable { DHTRecord._( {required VeilidRoutingContext routingContext, required SharedDHTRecordData sharedDHTRecordData, @@ -30,20 +43,33 @@ class DHTRecord { _open = true, _sharedDHTRecordData = sharedDHTRecordData; - final SharedDHTRecordData _sharedDHTRecordData; - final VeilidRoutingContext _routingContext; - final int _defaultSubkey; - final KeyPair? _writer; - final DHTRecordCrypto _crypto; - final String debugName; + //////////////////////////////////////////////////////////////////////////// + // DHTOpenable - bool _open; - @internal - StreamController? watchController; - @internal - WatchState? watchState; + /// Check if the DHTRecord is open + @override + bool get isOpen => _open; - int subkeyOrDefault(int subkey) => (subkey == -1) ? _defaultSubkey : subkey; + /// Free all resources for the DHTRecord + @override + Future close() async { + if (!_open) { + return; + } + await watchController?.close(); + await DHTRecordPool.instance._recordClosed(this); + _open = false; + } + + /// Free all resources for the DHTRecord and delete it from the DHT + /// Will wait until the record is closed to delete it + @override + Future delete() async { + await DHTRecordPool.instance.deleteRecord(key); + } + + //////////////////////////////////////////////////////////////////////////// + // Public API VeilidRoutingContext get routingContext => _routingContext; TypedKey get key => _sharedDHTRecordData.recordDescriptor.key; @@ -57,64 +83,30 @@ class DHTRecord { DHTRecordCrypto get crypto => _crypto; OwnedDHTRecordPointer get ownedDHTRecordPointer => OwnedDHTRecordPointer(recordKey: key, owner: ownerKeyPair!); - bool get isOpen => _open; - - Future close() async { - if (!_open) { - return; - } - await watchController?.close(); - await DHTRecordPool.instance._recordClosed(this); - _open = false; - } - - Future scope(Future Function(DHTRecord) scopeFunction) async { - try { - return await scopeFunction(this); - } finally { - await close(); - } - } - - Future deleteScope(Future Function(DHTRecord) scopeFunction) async { - try { - final out = await scopeFunction(this); - if (_open) { - await close(); - } - return out; - } on Exception catch (_) { - if (_open) { - await close(); - } - await DHTRecordPool.instance.deleteRecord(key); - rethrow; - } - } - - Future maybeDeleteScope( - bool delete, Future Function(DHTRecord) scopeFunction) async { - if (delete) { - return deleteScope(scopeFunction); - } else { - return scope(scopeFunction); - } - } + int subkeyOrDefault(int subkey) => (subkey == -1) ? _defaultSubkey : subkey; + /// Get a subkey value from this record. + /// Returns the most recent value data for this subkey or null if this subkey + /// has not yet been written to. + /// * 'refreshMode' determines whether or not to return a locally existing + /// value or always check the network + /// * 'outSeqNum' optionally returns the sequence number of the value being + /// returned if one was returned. Future get( {int subkey = -1, DHTRecordCrypto? crypto, - bool forceRefresh = false, - bool onlyUpdates = false, + DHTRecordRefreshMode refreshMode = DHTRecordRefreshMode.existing, Output? outSeqNum}) async { subkey = subkeyOrDefault(subkey); final valueData = await _routingContext.getDHTValue(key, subkey, - forceRefresh: forceRefresh); + forceRefresh: refreshMode != DHTRecordRefreshMode.existing); if (valueData == null) { return null; } final lastSeq = _sharedDHTRecordData.subkeySeqCache[subkey]; - if (onlyUpdates && lastSeq != null && valueData.seq <= lastSeq) { + if (refreshMode == DHTRecordRefreshMode.refreshOnlyUpdates && + lastSeq != null && + valueData.seq <= lastSeq) { return null; } final out = (crypto ?? _crypto).decrypt(valueData.data, subkey); @@ -125,17 +117,23 @@ class DHTRecord { return out; } + /// Get a subkey value from this record. + /// Process the record returned with a JSON unmarshal function 'fromJson'. + /// Returns the most recent value data for this subkey or null if this subkey + /// has not yet been written to. + /// * 'refreshMode' determines whether or not to return a locally existing + /// value or always check the network + /// * 'outSeqNum' optionally returns the sequence number of the value being + /// returned if one was returned. Future getJson(T Function(dynamic) fromJson, {int subkey = -1, DHTRecordCrypto? crypto, - bool forceRefresh = false, - bool onlyUpdates = false, + DHTRecordRefreshMode refreshMode = DHTRecordRefreshMode.existing, Output? outSeqNum}) async { final data = await get( subkey: subkey, crypto: crypto, - forceRefresh: forceRefresh, - onlyUpdates: onlyUpdates, + refreshMode: refreshMode, outSeqNum: outSeqNum); if (data == null) { return null; @@ -143,18 +141,25 @@ class DHTRecord { return jsonDecodeBytes(fromJson, data); } + /// Get a subkey value from this record. + /// Process the record returned with a protobuf unmarshal + /// function 'fromBuffer'. + /// Returns the most recent value data for this subkey or null if this subkey + /// has not yet been written to. + /// * 'refreshMode' determines whether or not to return a locally existing + /// value or always check the network + /// * 'outSeqNum' optionally returns the sequence number of the value being + /// returned if one was returned. Future getProtobuf( T Function(List i) fromBuffer, {int subkey = -1, DHTRecordCrypto? crypto, - bool forceRefresh = false, - bool onlyUpdates = false, + DHTRecordRefreshMode refreshMode = DHTRecordRefreshMode.existing, Output? outSeqNum}) async { final data = await get( subkey: subkey, crypto: crypto, - forceRefresh: forceRefresh, - onlyUpdates: onlyUpdates, + refreshMode: refreshMode, outSeqNum: outSeqNum); if (data == null) { return null; @@ -162,6 +167,9 @@ class DHTRecord { return fromBuffer(data.toList()); } + /// Attempt to write a byte buffer to a DHTRecord subkey + /// If a newer value was found on the network, it is returned + /// If the value was succesfully written, null is returned Future tryWriteBytes(Uint8List newValue, {int subkey = -1, DHTRecordCrypto? crypto, @@ -211,6 +219,9 @@ class DHTRecord { return decryptedNewValue; } + /// Attempt to write a byte buffer to a DHTRecord subkey + /// If a newer value was found on the network, another attempt + /// will be made to write the subkey until this succeeds Future eventualWriteBytes(Uint8List newValue, {int subkey = -1, DHTRecordCrypto? crypto, @@ -256,6 +267,11 @@ class DHTRecord { } } + /// Attempt to write a byte buffer to a DHTRecord subkey + /// If a newer value was found on the network, another attempt + /// will be made to write the subkey until this succeeds + /// Each attempt to write the value calls an update function with the + /// old value to determine what new value should be attempted for that write. Future eventualUpdateBytes( Future Function(Uint8List? oldValue) update, {int subkey = -1, @@ -281,6 +297,7 @@ class DHTRecord { } while (oldValue != null); } + /// Like 'tryWriteBytes' but with JSON marshal/unmarshal of the value Future tryWriteJson(T Function(dynamic) fromJson, T newValue, {int subkey = -1, DHTRecordCrypto? crypto, @@ -298,6 +315,7 @@ class DHTRecord { return jsonDecodeBytes(fromJson, out); }); + /// Like 'tryWriteBytes' but with protobuf marshal/unmarshal of the value Future tryWriteProtobuf( T Function(List) fromBuffer, T newValue, {int subkey = -1, @@ -316,6 +334,7 @@ class DHTRecord { return fromBuffer(out); }); + /// Like 'eventualWriteBytes' but with JSON marshal/unmarshal of the value Future eventualWriteJson(T newValue, {int subkey = -1, DHTRecordCrypto? crypto, @@ -324,6 +343,7 @@ class DHTRecord { eventualWriteBytes(jsonEncodeBytes(newValue), subkey: subkey, crypto: crypto, writer: writer, outSeqNum: outSeqNum); + /// Like 'eventualWriteBytes' but with protobuf marshal/unmarshal of the value Future eventualWriteProtobuf(T newValue, {int subkey = -1, DHTRecordCrypto? crypto, @@ -332,6 +352,7 @@ class DHTRecord { eventualWriteBytes(newValue.writeToBuffer(), subkey: subkey, crypto: crypto, writer: writer, outSeqNum: outSeqNum); + /// Like 'eventualUpdateBytes' but with JSON marshal/unmarshal of the value Future eventualUpdateJson( T Function(dynamic) fromJson, Future Function(T?) update, {int subkey = -1, @@ -341,6 +362,7 @@ class DHTRecord { eventualUpdateBytes(jsonUpdate(fromJson, update), subkey: subkey, crypto: crypto, writer: writer, outSeqNum: outSeqNum); + /// Like 'eventualUpdateBytes' but with protobuf marshal/unmarshal of the value Future eventualUpdateProtobuf( T Function(List) fromBuffer, Future Function(T?) update, {int subkey = -1, @@ -350,6 +372,8 @@ class DHTRecord { eventualUpdateBytes(protobufUpdate(fromBuffer, update), subkey: subkey, crypto: crypto, writer: writer, outSeqNum: outSeqNum); + /// Watch a subkey range of this DHT record for changes + /// Takes effect on the next DHTRecordPool tick Future watch( {List? subkeys, Timestamp? expiration, @@ -363,6 +387,13 @@ class DHTRecord { } } + /// Register a callback for changes made on this this DHT record. + /// You must 'watch' the record as well as listen to it in order for this + /// call back to be called. + /// * 'localChanges' also enables calling the callback if changed are made + /// locally, otherwise only changes seen from the network itself are + /// reported + /// Future> listen( Future Function( DHTRecord record, Uint8List? data, List subkeys) @@ -405,6 +436,8 @@ class DHTRecord { }); } + /// Stop watching this record for changes + /// Takes effect on the next DHTRecordPool tick Future cancelWatch() async { // Tear down watch requirements if (watchState != null) { @@ -413,11 +446,15 @@ class DHTRecord { } } + /// Return the inspection state of a set of subkeys of the DHTRecord + /// See Veilid's 'inspectDHTRecord' call for details on how this works Future inspect( {List? subkeys, DHTReportScope scope = DHTReportScope.local}) => _routingContext.inspectDHTRecord(key, subkeys: subkeys, scope: scope); + ////////////////////////////////////////////////////////////////////////// + void _addValueChange( {required bool local, required Uint8List? data, @@ -458,4 +495,19 @@ class DHTRecord { _addValueChange( local: false, data: update.value?.data, subkeys: update.subkeys); } + + ////////////////////////////////////////////////////////////// + + final SharedDHTRecordData _sharedDHTRecordData; + final VeilidRoutingContext _routingContext; + final int _defaultSubkey; + final KeyPair? _writer; + final DHTRecordCrypto _crypto; + final String debugName; + + bool _open; + @internal + StreamController? watchController; + @internal + WatchState? watchState; } 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 15919f9..8616658 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 @@ -93,7 +93,7 @@ class DHTRecordCubit extends Cubit> { 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); + subkey: sk, refreshMode: DHTRecordRefreshMode.refreshOnlyUpdates); if (data != null) { final newState = await _stateFunction(_record, updateSubkeys, data); if (newState != null) { 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 f15987a..a305e22 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 @@ -3,7 +3,6 @@ import 'dart:typed_data'; import 'package:async_tools/async_tools.dart'; import 'package:collection/collection.dart'; -import 'package:protobuf/protobuf.dart'; import '../../../veilid_support.dart'; import '../../proto/proto.dart' as proto; @@ -14,7 +13,7 @@ part 'dht_short_array_write.dart'; /////////////////////////////////////////////////////////////////////// -class DHTShortArray { +class DHTShortArray implements DHTOpenable { //////////////////////////////////////////////////////////////// // Constructors @@ -34,22 +33,22 @@ class DHTShortArray { VeilidRoutingContext? routingContext, TypedKey? parent, DHTRecordCrypto? crypto, - KeyPair? smplWriter}) async { + KeyPair? writer}) async { assert(stride <= maxElements, 'stride too long'); final pool = DHTRecordPool.instance; late final DHTRecord dhtRecord; - if (smplWriter != null) { + if (writer != null) { final schema = DHTSchema.smpl( oCnt: 0, - members: [DHTSchemaMember(mKey: smplWriter.key, mCnt: stride + 1)]); + members: [DHTSchemaMember(mKey: writer.key, mCnt: stride + 1)]); dhtRecord = await pool.createRecord( debugName: debugName, parent: parent, routingContext: routingContext, schema: schema, crypto: crypto, - writer: smplWriter); + writer: writer); } else { final schema = DHTSchema.dflt(oCnt: stride + 1); dhtRecord = await pool.createRecord( @@ -120,15 +119,15 @@ class DHTShortArray { } static Future openOwned( - OwnedDHTRecordPointer ownedDHTRecordPointer, { + OwnedDHTRecordPointer ownedShortArrayRecordPointer, { required String debugName, required TypedKey parent, VeilidRoutingContext? routingContext, DHTRecordCrypto? crypto, }) => openWrite( - ownedDHTRecordPointer.recordKey, - ownedDHTRecordPointer.owner, + ownedShortArrayRecordPointer.recordKey, + ownedShortArrayRecordPointer.owner, debugName: debugName, routingContext: routingContext, parent: parent, @@ -136,18 +135,14 @@ class DHTShortArray { ); //////////////////////////////////////////////////////////////////////////// - // Public API - - /// Get the record key for this shortarray - TypedKey get recordKey => _head.recordKey; - - /// Get the record pointer foir this shortarray - OwnedDHTRecordPointer get recordPointer => _head.recordPointer; + // DHTOpenable /// Check if the shortarray is open + @override bool get isOpen => _head.isOpen; /// Free all resources for the DHTShortArray + @override Future close() async { if (!isOpen) { return; @@ -159,44 +154,22 @@ class DHTShortArray { /// Free all resources for the DHTShortArray and delete it from the DHT /// Will wait until the short array is closed to delete it + @override Future delete() async { 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 { - if (!isOpen) { - throw StateError('short array is not open"'); - } - try { - return await scopeFunction(this); - } finally { - await close(); - } - } + //////////////////////////////////////////////////////////////////////////// + // Public API - /// 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 { - if (!isOpen) { - throw StateError('short array is not open"'); - } + /// Get the record key for this shortarray + TypedKey get recordKey => _head.recordKey; - try { - final out = await scopeFunction(this); - await close(); - return out; - } on Exception catch (_) { - await delete(); - rethrow; - } - } + /// Get the record pointer foir this shortarray + OwnedDHTRecordPointer get recordPointer => _head.recordPointer; /// Runs a closure allowing read-only access to the shortarray - Future operate(Future Function(DHTShortArrayRead) closure) async { + Future operate(Future Function(DHTRandomRead) closure) async { if (!isOpen) { throw StateError('short array is not open"'); } @@ -209,14 +182,19 @@ class DHTShortArray { /// 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 => - _head.operateWrite((head) async { - final writer = _DHTShortArrayWrite._(head); - return closure(writer); - }); + /// Returns result of the closure if the write could be performed + /// Throws DHTOperateException if the write could not be performed at this time + Future operateWrite( + Future Function(DHTRandomReadWrite) closure) async { + if (!isOpen) { + throw StateError('short array is not open"'); + } + + return _head.operateWrite((head) async { + final writer = _DHTShortArrayWrite._(head); + return closure(writer); + }); + } /// Runs a closure allowing read-write access to the shortarray /// Will execute the closure multiple times if a consistent write to the DHT @@ -225,7 +203,7 @@ class DHTShortArray { /// succeeded, returning false will trigger another eventual consistency /// attempt. Future operateWriteEventual( - Future Function(DHTShortArrayWrite) closure, + Future Function(DHTRandomReadWrite) closure, {Duration? timeout}) async { if (!isOpen) { throw StateError('short array is not open"'); 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 7465715..e0b2504 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 @@ -41,19 +41,6 @@ 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); - // }); - // } - Future refresh({bool forceRefresh = false}) async { await _initWait(); await _refreshNoWait(forceRefresh: forceRefresh); @@ -67,12 +54,13 @@ class DHTShortArrayCubit extends Cubit> try { final newState = await _shortArray.operate((reader) async { final offlinePositions = await reader.getOfflinePositions(); - final allItems = (await reader.getAllItems(forceRefresh: forceRefresh)) - ?.indexed - .map((x) => DHTShortArrayElementState( - value: _decodeElement(x.$2), - isOffline: offlinePositions.contains(x.$1))) - .toIList(); + final allItems = + (await reader.getItemRange(0, forceRefresh: forceRefresh)) + ?.indexed + .map((x) => DHTShortArrayElementState( + value: _decodeElement(x.$2), + isOffline: offlinePositions.contains(x.$1))) + .toIList(); return allItems; }); if (newState != null) { @@ -103,19 +91,19 @@ class DHTShortArrayCubit extends Cubit> await super.close(); } - Future operate(Future Function(DHTShortArrayRead) closure) async { + Future operate(Future Function(DHTRandomRead) closure) async { await _initWait(); return _shortArray.operate(closure); } - Future<(R?, bool)> operateWrite( - Future Function(DHTShortArrayWrite) closure) async { + Future operateWrite( + Future Function(DHTRandomReadWrite) closure) async { await _initWait(); return _shortArray.operateWrite(closure); } Future operateWriteEventual( - Future Function(DHTShortArrayWrite) closure, + Future Function(DHTRandomReadWrite) closure, {Duration? timeout}) async { await _initWait(); return _shortArray.operateWriteEventual(closure, timeout: timeout); 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 6da7791..0a2b7d2 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 @@ -82,8 +82,8 @@ class _DHTShortArrayHead { return closure(this); }); - Future<(T?, bool)> operateWrite( - Future Function(_DHTShortArrayHead) closure) async => + Future operateWrite( + Future Function(_DHTShortArrayHead) closure) async => _headMutex.protect(() async { final oldLinkedRecords = List.of(_linkedRecords); final oldIndex = List.of(_index); @@ -95,11 +95,11 @@ class _DHTShortArrayHead { if (!await _writeHead()) { // Failed to write head means head got overwritten so write should // be considered failed - return (null, false); + throw DHTExceptionTryAgain(); } onUpdatedHead?.call(); - return (out, true); + return out; } on Exception { // Exception means state needs to be reverted _linkedRecords = oldLinkedRecords; @@ -249,22 +249,15 @@ class _DHTShortArrayHead { } // Pull the latest or updated copy of the head record from the network - Future _loadHead( - {bool forceRefresh = true, bool onlyUpdates = false}) async { + Future _loadHead() async { // Get an updated head record copy if one exists final head = await _headRecord.getProtobuf(proto.DHTShortArray.fromBuffer, - subkey: 0, forceRefresh: forceRefresh, onlyUpdates: onlyUpdates); + subkey: 0, refreshMode: DHTRecordRefreshMode.refresh); if (head == null) { - if (onlyUpdates) { - // No update - return false; - } - throw StateError('head missing during refresh'); + throw StateError('shortarray head missing during refresh'); } await _updateHead(head); - - return true; } ///////////////////////////////////////////////////////////////////////////// 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 index 342e67a..88cefde 100644 --- 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 @@ -1,70 +1,14 @@ 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}); - - /// Get a list of the positions that were written offline and not flushed yet - Future> getOfflinePositions(); -} - -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 { +class _DHTShortArrayRead implements DHTRandomRead { _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) { @@ -77,7 +21,9 @@ class _DHTShortArrayRead implements DHTShortArrayRead { final outSeqNum = Output(); final out = lookup.record.get( subkey: lookup.recordSubkey, - forceRefresh: refresh, + refreshMode: refresh + ? DHTRecordRefreshMode.refresh + : DHTRecordRefreshMode.existing, outSeqNum: outSeqNum); if (outSeqNum.value != null) { _head.updatePositionSeq(pos, false, outSeqNum.value!); @@ -86,17 +32,29 @@ class _DHTShortArrayRead implements DHTShortArrayRead { 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 = []; + (int, int) _clampStartLen(int start, int? len) { + len ??= _head.length; + if (start < 0) { + throw IndexError.withLength(start, _head.length); + } + if (start > _head.length) { + throw IndexError.withLength(start, _head.length); + } + if ((len + start) > _head.length) { + len = _head.length - start; + } + return (start, len); + } - final chunks = Iterable.generate(_head.length) - .slices(maxDHTConcurrency) - .map((chunk) => - chunk.map((pos) => getItem(pos, forceRefresh: forceRefresh))); + @override + Future?> getItemRange(int start, + {int? length, bool forceRefresh = false}) async { + final out = []; + (start, length) = _clampStartLen(start, length); + + final chunks = Iterable.generate(length).slices(maxDHTConcurrency).map( + (chunk) => chunk + .map((pos) => getItem(pos + start, forceRefresh: forceRefresh))); for (final chunk in chunks) { final elems = await chunk.wait; @@ -109,9 +67,10 @@ class _DHTShortArrayRead implements DHTShortArrayRead { return out; } - /// Get a list of the positions that were written offline and not flushed yet @override Future> getOfflinePositions() async { + final (start, length) = _clampStartLen(0, DHTShortArray.maxElements); + final indexOffline = {}; final inspects = await [ _head._headRecord.inspect(), @@ -134,7 +93,7 @@ class _DHTShortArrayRead implements DHTShortArrayRead { // See which positions map to offline indexes final positionOffline = {}; - for (var i = 0; i < _head._index.length; i++) { + for (var i = start; i < (start + length); i++) { final idx = _head._index[i]; if (indexOffline.contains(idx)) { positionOffline.add(i); 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 index d1c8b2f..0d51663 100644 --- 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 @@ -1,101 +1,10 @@ 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 implementation class _DHTShortArrayWrite extends _DHTShortArrayRead - implements DHTShortArrayWrite { + implements DHTRandomReadWrite { _DHTShortArrayWrite._(super.head) : super._(); @override @@ -105,12 +14,12 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead _head.allocateIndex(pos); // Write item - final (_, wasSet) = await tryWriteItem(pos, value); - if (!wasSet) { - return false; + final ok = await tryWriteItem(pos, value); + if (!ok) { + _head.freeIndex(pos); } - return true; + return ok; } @override @@ -119,16 +28,15 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead _head.allocateIndex(pos); // Write item - final (_, wasSet) = await tryWriteItem(pos, value); - if (!wasSet) { - return false; + final ok = await tryWriteItem(pos, value); + if (!ok) { + _head.freeIndex(pos); } - return true; } @override - Future trySwapItem(int aPos, int bPos) async { + Future swapItem(int aPos, int bPos) async { if (aPos < 0 || aPos >= _head.length) { throw IndexError.withLength(aPos, _head.length); } @@ -137,12 +45,10 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead } // Swap indices _head.swapIndex(aPos, bPos); - - return true; } @override - Future tryRemoveItem(int pos) async { + Future removeItem(int pos, {Output? output}) async { if (pos < 0 || pos >= _head.length) { throw IndexError.withLength(pos, _head.length); } @@ -162,17 +68,17 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead throw StateError('Element does not exist'); } _head.freeIndex(pos); - return result; + output?.save(result); } @override - Future tryClear() async { + Future clear() async { _head.clearIndex(); - return true; } @override - Future<(Uint8List?, bool)> tryWriteItem(int pos, Uint8List newValue) async { + Future tryWriteItem(int pos, Uint8List newValue, + {Output? output}) async { if (pos < 0 || pos >= _head.length) { throw IndexError.withLength(pos, _head.length); } @@ -198,8 +104,10 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead if (result != null) { // A result coming back means the element was overwritten already - return (result, false); + output?.save(result); + return false; } - return (oldValue, true); + output?.save(oldValue); + return true; } } diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_append_truncate.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_append_truncate.dart new file mode 100644 index 0000000..babcc7d --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_append_truncate.dart @@ -0,0 +1,44 @@ +import 'dart:typed_data'; + +import 'package:protobuf/protobuf.dart'; + +import '../../../veilid_support.dart'; + +//////////////////////////////////////////////////////////////////////////// +// Append/truncate interface +abstract class DHTAppendTruncate { + /// Try to add an item to the end of the DHT data structure. + /// 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 limits. + Future tryAppendItem(Uint8List value); + + /// Try to remove a number of items from the head of the DHT data structure. + /// Throws StateError if count < 0 + Future truncate(int count); + + /// Remove all items in the DHT data structure. + Future clear(); +} + +abstract class DHTAppendTruncateRandomRead + implements DHTAppendTruncate, DHTRandomRead {} + +extension DHTAppendTruncateExt on DHTAppendTruncate { + /// Convenience function: + /// Like tryAppendItem but also encodes the input value as JSON and parses the + /// returned element as JSON + Future tryAppendItemJson( + T newValue, + ) => + tryAppendItem(jsonEncodeBytes(newValue)); + + /// Convenience function: + /// Like tryAppendItem but also encodes the input value as a protobuf object + /// and parses the returned element as a protobuf object + Future tryAppendItemProtobuf( + T newValue, + ) => + tryAppendItem(newValue.writeToBuffer()); +} diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_openable.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_openable.dart new file mode 100644 index 0000000..1ee1140 --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_openable.dart @@ -0,0 +1,49 @@ +import 'dart:async'; + +abstract class DHTOpenable { + bool get isOpen; + Future close(); + Future delete(); +} + +extension DHTOpenableExt on D { + /// Runs a closure that guarantees the DHTOpenable + /// will be closed upon exit, even if an uncaught exception is thrown + Future scope(Future Function(D) scopeFunction) async { + if (!isOpen) { + throw StateError('not open in scope'); + } + try { + return await scopeFunction(this); + } finally { + await close(); + } + } + + /// Runs a closure that guarantees the DHTOpenable + /// will be closed upon exit, and deleted if an an + /// uncaught exception is thrown + Future deleteScope(Future Function(D) scopeFunction) async { + if (!isOpen) { + throw StateError('not open in deleteScope'); + } + + try { + final out = await scopeFunction(this); + await close(); + return out; + } on Exception catch (_) { + await delete(); + rethrow; + } + } + + /// Scopes a closure that conditionally deletes the DHTOpenable on exit + Future maybeDeleteScope( + bool delete, Future Function(D) scopeFunction) async { + if (delete) { + return deleteScope(scopeFunction); + } + return scope(scopeFunction); + } +} diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart new file mode 100644 index 0000000..d52676e --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart @@ -0,0 +1,63 @@ +import 'dart:typed_data'; + +import 'package:protobuf/protobuf.dart'; + +import '../../../veilid_support.dart'; + +//////////////////////////////////////////////////////////////////////////// +// Reader interface +abstract class DHTRandomRead { + /// Returns the number of elements in the DHTArray + /// This number will be >= 0 and <= DHTShortArray.maxElements (256) + int get length; + + /// Return the item at position 'pos' in the DHTArray. If 'forceRefresh' + /// is specified, the network will always be checked for newer values + /// rather than returning the existing locally stored copy of the elements. + /// * 'pos' must be >= 0 and < 'length' + Future getItem(int pos, {bool forceRefresh = false}); + + /// Return a list of a range of items in the DHTArray. If 'forceRefresh' + /// is specified, the network will always be checked for newer values + /// rather than returning the existing locally stored copy of the elements. + /// * 'start' must be >= 0 + /// * 'len' must be >= 0 and <= DHTShortArray.maxElements (256) and defaults + /// to the maximum length + Future?> getItemRange(int start, + {int? length, bool forceRefresh = false}); + + /// Get a list of the positions that were written offline and not flushed yet + Future> getOfflinePositions(); +} + +extension DHTRandomReadExt on DHTRandomRead { + /// 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?> getItemRangeJson(T Function(dynamic) fromJson, int start, + {int? length, bool forceRefresh = false}) => + getItemRange(start, length: length, 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?> getItemRangeProtobuf( + T Function(List) fromBuffer, int start, + {int? length, bool forceRefresh = false}) => + getItemRange(start, length: length, forceRefresh: forceRefresh) + .then((out) => out?.map(fromBuffer).toList()); +} diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart new file mode 100644 index 0000000..53f307c --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart @@ -0,0 +1,104 @@ +import 'dart:typed_data'; + +import 'package:protobuf/protobuf.dart'; + +import '../../../veilid_support.dart'; + +//////////////////////////////////////////////////////////////////////////// +// Writer interface +abstract class DHTRandomWrite { + /// Try to set an item at position 'pos' of the DHTArray. + /// If the set was successful this returns: + /// * A boolean true + /// * outValue will return the prior contents of the element, + /// or null if there was no value yet + /// + /// If the set was found a newer value on the network this returns: + /// * A boolean false + /// * outValue will return the newer value of the element, + /// or null if the head record changed. + /// + /// This may throw an exception if the position exceeds the built-in limit of + /// 'maxElements = 256' entries. + Future tryWriteItem(int pos, Uint8List newValue, + {Output? output}); + + /// Try to add an item to the end of the DHTArray. 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 DHTArray. + /// 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); + + /// Swap items at position 'aPos' and 'bPos' in the DHTArray. + /// Throws IndexError if either of the positions swapped exceed + /// the length of the list + Future swapItem(int aPos, int bPos); + + /// Remove an item at position 'pos' in the DHTArray. + /// If the remove was successful this returns: + /// * outValue will return the prior contents of the element + /// Throws IndexError if the position removed exceeds the length of + /// the list. + Future removeItem(int pos, {Output? output}); + + /// Remove all items in the DHTShortArray. + Future clear(); +} + +extension DHTRandomWriteExt on DHTRandomWrite { + /// 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, + {Output? output}) async { + final outValueBytes = output == null ? null : Output(); + final out = await tryWriteItem(pos, jsonEncodeBytes(newValue), + output: outValueBytes); + output.mapSave(outValueBytes, (b) => jsonDecodeBytes(fromJson, b)); + return 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, + {Output? output}) async { + final outValueBytes = output == null ? null : Output(); + final out = await tryWriteItem(pos, newValue.writeToBuffer(), + output: outValueBytes); + output.mapSave(outValueBytes, fromBuffer); + return out; + } + + /// Convenience function: + /// Like removeItem but also parses the returned element as JSON + Future removeItemJson(T Function(dynamic) fromJson, int pos, + {Output? output}) async { + final outValueBytes = output == null ? null : Output(); + await removeItem(pos, output: outValueBytes); + output.mapSave(outValueBytes, (b) => jsonDecodeBytes(fromJson, b)); + } + + /// Convenience function: + /// Like removeItem but also parses the returned element as JSON + Future removeItemProtobuf( + T Function(List) fromBuffer, int pos, + {Output? output}) async { + final outValueBytes = output == null ? null : Output(); + await removeItem(pos, output: outValueBytes); + output.mapSave(outValueBytes, fromBuffer); + } +} + +abstract class DHTRandomReadWrite implements DHTRandomRead, DHTRandomWrite {} diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/exceptions.dart b/packages/veilid_support/lib/dht_support/src/interfaces/exceptions.dart new file mode 100644 index 0000000..2b95033 --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/interfaces/exceptions.dart @@ -0,0 +1,5 @@ +class DHTExceptionTryAgain implements Exception { + DHTExceptionTryAgain( + [this.cause = 'operation failed due to newer dht value']); + String cause; +} diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart b/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart new file mode 100644 index 0000000..6c61075 --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart @@ -0,0 +1,4 @@ +export 'dht_openable.dart'; +export 'dht_random_read.dart'; +export 'dht_random_write.dart'; +export 'exceptions.dart'; diff --git a/packages/veilid_support/lib/proto/dht.pb.dart b/packages/veilid_support/lib/proto/dht.pb.dart index 7c96a7f..4007d3d 100644 --- a/packages/veilid_support/lib/proto/dht.pb.dart +++ b/packages/veilid_support/lib/proto/dht.pb.dart @@ -83,6 +83,68 @@ class DHTData extends $pb.GeneratedMessage { void clearSize() => clearField(4); } +class DHTLog extends $pb.GeneratedMessage { + factory DHTLog() => create(); + DHTLog._() : super(); + factory DHTLog.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory DHTLog.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DHTLog', package: const $pb.PackageName(_omitMessageNames ? '' : 'dht'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'head', $pb.PbFieldType.OU3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'tail', $pb.PbFieldType.OU3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'stride', $pb.PbFieldType.OU3) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + DHTLog clone() => DHTLog()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + DHTLog copyWith(void Function(DHTLog) updates) => super.copyWith((message) => updates(message as DHTLog)) as DHTLog; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DHTLog create() => DHTLog._(); + DHTLog createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DHTLog getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static DHTLog? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get head => $_getIZ(0); + @$pb.TagNumber(1) + set head($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasHead() => $_has(0); + @$pb.TagNumber(1) + void clearHead() => clearField(1); + + @$pb.TagNumber(2) + $core.int get tail => $_getIZ(1); + @$pb.TagNumber(2) + set tail($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasTail() => $_has(1); + @$pb.TagNumber(2) + void clearTail() => clearField(2); + + @$pb.TagNumber(3) + $core.int get stride => $_getIZ(2); + @$pb.TagNumber(3) + set stride($core.int v) { $_setUnsignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasStride() => $_has(2); + @$pb.TagNumber(3) + void clearStride() => clearField(3); +} + class DHTShortArray extends $pb.GeneratedMessage { factory DHTShortArray() => create(); DHTShortArray._() : super(); @@ -133,68 +195,6 @@ class DHTShortArray extends $pb.GeneratedMessage { $core.List<$core.int> get seqs => $_getList(2); } -class DHTLog extends $pb.GeneratedMessage { - factory DHTLog() => create(); - DHTLog._() : super(); - factory DHTLog.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory DHTLog.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DHTLog', package: const $pb.PackageName(_omitMessageNames ? '' : 'dht'), createEmptyInstance: create) - ..pc<$0.TypedKey>(1, _omitFieldNames ? '' : 'keys', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create) - ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'back', subBuilder: $0.TypedKey.create) - ..p<$core.int>(3, _omitFieldNames ? '' : 'subkeyCounts', $pb.PbFieldType.KU3) - ..a<$core.int>(4, _omitFieldNames ? '' : 'totalSubkeys', $pb.PbFieldType.OU3) - ..hasRequiredFields = false - ; - - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - DHTLog clone() => DHTLog()..mergeFromMessage(this); - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - DHTLog copyWith(void Function(DHTLog) updates) => super.copyWith((message) => updates(message as DHTLog)) as DHTLog; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static DHTLog create() => DHTLog._(); - DHTLog createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static DHTLog getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static DHTLog? _defaultInstance; - - @$pb.TagNumber(1) - $core.List<$0.TypedKey> get keys => $_getList(0); - - @$pb.TagNumber(2) - $0.TypedKey get back => $_getN(1); - @$pb.TagNumber(2) - set back($0.TypedKey v) { setField(2, v); } - @$pb.TagNumber(2) - $core.bool hasBack() => $_has(1); - @$pb.TagNumber(2) - void clearBack() => clearField(2); - @$pb.TagNumber(2) - $0.TypedKey ensureBack() => $_ensure(1); - - @$pb.TagNumber(3) - $core.List<$core.int> get subkeyCounts => $_getList(2); - - @$pb.TagNumber(4) - $core.int get totalSubkeys => $_getIZ(3); - @$pb.TagNumber(4) - set totalSubkeys($core.int v) { $_setUnsignedInt32(3, v); } - @$pb.TagNumber(4) - $core.bool hasTotalSubkeys() => $_has(3); - @$pb.TagNumber(4) - void clearTotalSubkeys() => clearField(4); -} - enum DataReference_Kind { dhtData, notSet diff --git a/packages/veilid_support/lib/proto/dht.pbjson.dart b/packages/veilid_support/lib/proto/dht.pbjson.dart index bf31c30..6c99cb7 100644 --- a/packages/veilid_support/lib/proto/dht.pbjson.dart +++ b/packages/veilid_support/lib/proto/dht.pbjson.dart @@ -30,6 +30,21 @@ final $typed_data.Uint8List dHTDataDescriptor = $convert.base64Decode( 'gCIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIEaGFzaBIUCgVjaHVuaxgDIAEoDVIFY2h1bmsSEgoE' 'c2l6ZRgEIAEoDVIEc2l6ZQ=='); +@$core.Deprecated('Use dHTLogDescriptor instead') +const DHTLog$json = { + '1': 'DHTLog', + '2': [ + {'1': 'head', '3': 1, '4': 1, '5': 13, '10': 'head'}, + {'1': 'tail', '3': 2, '4': 1, '5': 13, '10': 'tail'}, + {'1': 'stride', '3': 3, '4': 1, '5': 13, '10': 'stride'}, + ], +}; + +/// Descriptor for `DHTLog`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List dHTLogDescriptor = $convert.base64Decode( + 'CgZESFRMb2cSEgoEaGVhZBgBIAEoDVIEaGVhZBISCgR0YWlsGAIgASgNUgR0YWlsEhYKBnN0cm' + 'lkZRgDIAEoDVIGc3RyaWRl'); + @$core.Deprecated('Use dHTShortArrayDescriptor instead') const DHTShortArray$json = { '1': 'DHTShortArray', @@ -45,23 +60,6 @@ final $typed_data.Uint8List dHTShortArrayDescriptor = $convert.base64Decode( 'Cg1ESFRTaG9ydEFycmF5EiQKBGtleXMYASADKAsyEC52ZWlsaWQuVHlwZWRLZXlSBGtleXMSFA' 'oFaW5kZXgYAiABKAxSBWluZGV4EhIKBHNlcXMYAyADKA1SBHNlcXM='); -@$core.Deprecated('Use dHTLogDescriptor instead') -const DHTLog$json = { - '1': 'DHTLog', - '2': [ - {'1': 'keys', '3': 1, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'keys'}, - {'1': 'back', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'back'}, - {'1': 'subkey_counts', '3': 3, '4': 3, '5': 13, '10': 'subkeyCounts'}, - {'1': 'total_subkeys', '3': 4, '4': 1, '5': 13, '10': 'totalSubkeys'}, - ], -}; - -/// Descriptor for `DHTLog`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List dHTLogDescriptor = $convert.base64Decode( - 'CgZESFRMb2cSJAoEa2V5cxgBIAMoCzIQLnZlaWxpZC5UeXBlZEtleVIEa2V5cxIkCgRiYWNrGA' - 'IgASgLMhAudmVpbGlkLlR5cGVkS2V5UgRiYWNrEiMKDXN1YmtleV9jb3VudHMYAyADKA1SDHN1' - 'YmtleUNvdW50cxIjCg10b3RhbF9zdWJrZXlzGAQgASgNUgx0b3RhbFN1YmtleXM='); - @$core.Deprecated('Use dataReferenceDescriptor instead') const DataReference$json = { '1': 'DataReference', diff --git a/packages/veilid_support/lib/src/identity.dart b/packages/veilid_support/lib/src/identity.dart index baae797..2645894 100644 --- a/packages/veilid_support/lib/src/identity.dart +++ b/packages/veilid_support/lib/src/identity.dart @@ -300,8 +300,8 @@ Future openIdentityMaster( debugName: 'IdentityMaster::openIdentityMaster::IdentityMasterRecord')) .deleteScope((masterRec) async { - final identityMaster = - (await masterRec.getJson(IdentityMaster.fromJson, forceRefresh: true))!; + final identityMaster = (await masterRec.getJson(IdentityMaster.fromJson, + refreshMode: DHTRecordRefreshMode.refresh))!; // Validate IdentityMaster final masterRecordKey = masterRec.key; diff --git a/packages/veilid_support/lib/src/output.dart b/packages/veilid_support/lib/src/output.dart new file mode 100644 index 0000000..78902b3 --- /dev/null +++ b/packages/veilid_support/lib/src/output.dart @@ -0,0 +1,33 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; + +export 'package:fast_immutable_collections/fast_immutable_collections.dart' + show Output; + +extension OutputNullExt on Output? { + void mapSave(Output? other, T Function(S output) closure) { + if (this == null) { + return; + } + if (other == null) { + return; + } + final v = other.value; + if (v == null) { + return; + } + return this!.save(closure(v)); + } +} + +extension OutputExt on Output { + void mapSave(Output? other, T Function(S output) closure) { + if (other == null) { + return; + } + final v = other.value; + if (v == null) { + return; + } + return save(closure(v)); + } +} diff --git a/packages/veilid_support/lib/veilid_support.dart b/packages/veilid_support/lib/veilid_support.dart index fcbbaf4..e741990 100644 --- a/packages/veilid_support/lib/veilid_support.dart +++ b/packages/veilid_support/lib/veilid_support.dart @@ -10,6 +10,7 @@ export 'src/config.dart'; export 'src/identity.dart'; export 'src/json_tools.dart'; export 'src/memory_tools.dart'; +export 'src/output.dart'; export 'src/persistent_queue.dart'; export 'src/protobuf_tools.dart'; export 'src/table_db.dart'; diff --git a/pubspec.lock b/pubspec.lock index 21e4f2e..07b05e5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: "direct main" description: name: archive - sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + sha256: ecf4273855368121b1caed0d10d4513c7241dfc813f7d3c8933b36622ae9b265 url: "https://pub.dev" source: hosted - version: "3.4.10" + version: "3.5.1" args: dependency: transitive description: @@ -203,18 +203,18 @@ packages: dependency: transitive description: name: cached_network_image_web - sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316" + sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" camera: dependency: transitive description: name: camera - sha256: "9499cbc2e51d8eb0beadc158b288380037618ce4e30c9acbc4fae1ac3ecb5797" + sha256: dfa8fc5a1adaeb95e7a54d86a5bd56f4bb0e035515354c8ac6d262e35cec2ec8 url: "https://pub.dev" source: hosted - version: "0.10.5+9" + version: "0.10.6" camera_android: dependency: transitive description: @@ -227,10 +227,10 @@ packages: dependency: transitive description: name: camera_avfoundation - sha256: "9dbbb253aaf201a69c40cf95571f366ca936305d2de012684e21f6f1b1433d31" + sha256: "7d021e8cd30d9b71b8b92b4ad669e80af432d722d18d6aac338572754a786c15" url: "https://pub.dev" source: hosted - version: "0.9.15+4" + version: "0.9.16" camera_platform_interface: dependency: transitive description: @@ -456,10 +456,10 @@ packages: dependency: transitive description: name: flutter_cache_manager - sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" + sha256: "395d6b7831f21f3b989ebedbb785545932adb9afe2622c1ffacf7f4b53a7e544" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.3.2" flutter_chat_types: dependency: "direct main" description: @@ -634,10 +634,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "771c8feb40ad0ef639973d7ecf1b43d55ffcedb2207fd43fab030f5639e40446" + sha256: b465e99ce64ba75e61c8c0ce3d87b66d8ac07f0b35d0a7e0263fcfc10f99e836 url: "https://pub.dev" source: hosted - version: "13.2.4" + version: "13.2.5" graphs: dependency: transitive description: @@ -898,10 +898,10 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" path_provider_linux: dependency: transitive description: @@ -970,10 +970,10 @@ packages: dependency: transitive description: name: pointycastle - sha256: "79fbafed02cfdbe85ef3fd06c7f4bc2cbcba0177e61b765264853d4253b21744" + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" url: "https://pub.dev" source: hosted - version: "3.9.0" + version: "3.9.1" pool: dependency: transitive description: @@ -1106,10 +1106,10 @@ packages: dependency: "direct main" description: name: searchable_listview - sha256: d8513a968bdd540cb011220a5670b23b346e04a7bcb99690a859ed58092f72a4 + sha256: f9bc1a57dfcba49ce2d190d642567fb82309dd23849b3b0a328266e3f90054db url: "https://pub.dev" source: hosted - version: "2.11.2" + version: "2.12.0" share_plus: dependency: "direct main" description: @@ -1146,10 +1146,10 @@ packages: dependency: transitive description: name: shared_preferences_foundation - sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.4.0" shared_preferences_linux: dependency: transitive description: @@ -1263,10 +1263,10 @@ packages: dependency: transitive description: name: sqflite - sha256: "5ce2e1a15e822c3b4bfb5400455775e421da7098eed8adc8f26298ada7c9308c" + sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.3.3+1" sqflite_common: dependency: transitive description: @@ -1407,10 +1407,10 @@ packages: dependency: transitive description: name: url_launcher_ios - sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5" + sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89" url: "https://pub.dev" source: hosted - version: "6.2.5" + version: "6.3.0" url_launcher_linux: dependency: transitive description: @@ -1423,10 +1423,10 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.0" url_launcher_platform_interface: dependency: transitive description: @@ -1541,10 +1541,10 @@ packages: dependency: transitive description: name: win32 - sha256: "0a989dc7ca2bb51eac91e8fd00851297cfffd641aa7538b165c62637ca0eaa4a" + sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.5.0" window_manager: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 821c214..d3e5a50 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,7 +10,7 @@ environment: dependencies: animated_theme_switcher: ^2.0.10 ansicolor: ^2.0.2 - archive: ^3.4.10 + archive: ^3.5.1 async_tools: ^0.1.1 awesome_extensions: ^2.0.14 badges: ^3.1.2 @@ -44,11 +44,11 @@ dependencies: flutter_translate: ^4.0.4 form_builder_validators: ^9.1.0 freezed_annotation: ^2.4.1 - go_router: ^13.2.4 + go_router: ^13.2.5 hydrated_bloc: ^9.1.5 image: ^4.1.7 intl: ^0.18.1 - json_annotation: ^4.8.1 + json_annotation: ^4.9.0 loggy: ^2.0.3 meta: ^1.11.0 mobile_scanner: ^4.0.1 @@ -65,7 +65,7 @@ dependencies: quickalert: ^1.1.0 radix_colors: ^1.0.4 reorderable_grid: ^1.0.10 - searchable_listview: ^2.11.2 + searchable_listview: ^2.12.0 share_plus: ^8.0.3 shared_preferences: ^2.2.3 signal_strength_indicator: ^0.4.1 @@ -93,7 +93,7 @@ dev_dependencies: build_runner: ^2.4.9 freezed: ^2.5.2 icons_launcher: ^2.1.7 - json_serializable: ^6.7.1 + json_serializable: ^6.8.0 lint_hard: ^4.0.0 flutter_native_splash: From 3315644ba886bcbd0c1a8544b76b587c76ee199d Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 14 May 2024 10:06:43 -0400 Subject: [PATCH 097/270] dht log implementation --- .../lib/dht_support/src/dht_log/barrel.dart | 4 +- .../lib/dht_support/src/dht_log/dht_log.dart | 29 +- .../src/dht_log/dht_log_cubit.dart | 289 ++++++++++++------ .../src/dht_log/dht_log_spine.dart | 43 ++- .../src/dht_short_array/dht_short_array.dart | 3 +- 5 files changed, 252 insertions(+), 116 deletions(-) diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/barrel.dart b/packages/veilid_support/lib/dht_support/src/dht_log/barrel.dart index 18686f2..39d1c41 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/barrel.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/barrel.dart @@ -1,2 +1,2 @@ -export 'dht_array.dart'; -export 'dht_array_cubit.dart'; +export 'dht_log.dart'; +export 'dht_log_cubit.dart'; diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart index a132bdb..7513c8b 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:async_tools/async_tools.dart'; import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; import '../../../veilid_support.dart'; import '../../proto/proto.dart' as proto; @@ -15,6 +16,21 @@ part 'dht_log_append.dart'; /////////////////////////////////////////////////////////////////////// +@immutable +class DHTLogUpdate extends Equatable { + const DHTLogUpdate( + {required this.headDelta, required this.tailDelta, required this.length}) + : assert(headDelta >= 0, 'should never have negative head delta'), + assert(tailDelta >= 0, 'should never have negative tail delta'), + assert(length >= 0, 'should never have negative length'); + final int headDelta; + final int tailDelta; + final int length; + + @override + List get props => [headDelta, tailDelta, length]; +} + /// DHTLog is a ring-buffer queue like data structure with the following /// operations: /// * Add elements to the tail @@ -30,8 +46,8 @@ class DHTLog implements DHTOpenable { // Constructors DHTLog._({required _DHTLogSpine spine}) : _spine = spine { - _spine.onUpdatedSpine = () { - _watchController?.sink.add(null); + _spine.onUpdatedSpine = (update) { + _watchController?.sink.add(update); }; } @@ -225,7 +241,7 @@ class DHTLog implements DHTOpenable { /// Listen to and any all changes to the structure of this log /// regardless of where the changes are coming from Future> listen( - void Function() onChanged, + void Function(DHTLogUpdate) onChanged, ) { if (!isOpen) { throw StateError('log is not open"'); @@ -235,7 +251,8 @@ class DHTLog implements DHTOpenable { // If don't have a controller yet, set it up if (_watchController == null) { // Set up watch requirements - _watchController = StreamController.broadcast(onCancel: () { + _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 { @@ -249,7 +266,7 @@ class DHTLog implements DHTOpenable { await _spine.watch(); } // Return subscription - return _watchController!.stream.listen((_) => onChanged()); + return _watchController!.stream.listen((upd) => onChanged(upd)); }); } @@ -269,5 +286,5 @@ class DHTLog implements DHTOpenable { // Watch mutex to ensure we keep the representation valid final Mutex _listenMutex = Mutex(); // Stream of external changes - StreamController? _watchController; + StreamController? _watchController; } diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart index a7d5333..30bac27 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart @@ -8,112 +8,213 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:meta/meta.dart'; import '../../../veilid_support.dart'; +import '../interfaces/dht_append_truncate.dart'; -// xxx paginate and remember to paginate watches (could use short array cubit as a subcubit here?) +@immutable +class DHTLogElementState extends Equatable { + const DHTLogElementState({required this.value, required this.isOffline}); + final T value; + final bool isOffline; -// @immutable -// class DHTArrayElementState extends Equatable { -// const DHTArrayElementState( -// {required this.value, required this.isOffline}); -// final T value; -// final bool isOffline; + @override + List get props => [value, isOffline]; +} -// @override -// List get props => [value, isOffline]; -// } +@immutable +class DHTLogStateData extends Equatable { + const DHTLogStateData( + {required this.elements, + required this.tail, + required this.count, + required this.follow}); + // The view of the elements in the dhtlog + // Span is from [tail-length, tail) + final IList> elements; + // One past the end of the last element + final int tail; + // The total number of elements to try to keep in 'elements' + final int count; + // If we should have the tail following the log + final bool follow; -// typedef DHTArrayState = AsyncValue>>; -// typedef DHTArrayBusyState = BlocBusyState>; + @override + List get props => [elements, tail, count, follow]; +} -// class DHTArrayCubit extends Cubit> -// with BlocBusyWrapper> { -// DHTArrayCubit({ -// required Future Function() open, -// required T Function(List data) decodeElement, -// }) : _decodeElement = decodeElement, -// super(const BlocBusyState(AsyncValue.loading())) { -// _initWait.add(() async { -// // Open DHT record -// _array = await open(); -// _wantsCloseRecord = true; +typedef DHTLogState = AsyncValue>; +typedef DHTLogBusyState = BlocBusyState>; -// // Make initial state update -// await _refreshNoWait(); -// _subscription = await _array.listen(_update); -// }); -// } +class DHTLogCubit extends Cubit> + with BlocBusyWrapper> { + DHTLogCubit({ + required Future Function() open, + required T Function(List data) decodeElement, + }) : _decodeElement = decodeElement, + super(const BlocBusyState(AsyncValue.loading())) { + _initWait.add(() async { + // Open DHT record + _log = await open(); + _wantsCloseRecord = true; -// Future refresh({bool forceRefresh = false}) async { -// await _initWait(); -// await _refreshNoWait(forceRefresh: forceRefresh); -// } + // Make initial state update + await _refreshNoWait(); + _subscription = await _log.listen(_update); + }); + } -// Future _refreshNoWait({bool forceRefresh = false}) async => -// busy((emit) async => _refreshInner(emit, forceRefresh: forceRefresh)); + // Set the tail position of the log for pagination. + // If tail is 0, the end of the log is used. + // If tail is negative, the position is subtracted from the current log + // length. + // If tail is positive, the position is absolute from the head of the log + // If follow is enabled, the tail offset will update when the log changes + Future setWindow( + {int? tail, int? count, bool? follow, bool forceRefresh = false}) async { + await _initWait(); + if (tail != null) { + _tail = tail; + } + if (count != null) { + _count = count; + } + if (follow != null) { + _follow = follow; + } + await _refreshNoWait(forceRefresh: forceRefresh); + } -// Future _refreshInner(void Function(DHTShortArrayState) emit, -// {bool forceRefresh = false}) async { -// try { -// final newState = await _shortArray.operate((reader) async { -// final offlinePositions = await reader.getOfflinePositions(); -// final allItems = (await reader.getAllItems(forceRefresh: forceRefresh)) -// ?.indexed -// .map((x) => DHTShortArrayElementState( -// value: _decodeElement(x.$2), -// isOffline: offlinePositions.contains(x.$1))) -// .toIList(); -// return allItems; -// }); -// if (newState != null) { -// emit(AsyncValue.data(newState)); -// } -// } on Exception catch (e) { -// emit(AsyncValue.error(e)); -// } -// } + Future refresh({bool forceRefresh = false}) async { + await _initWait(); + await _refreshNoWait(forceRefresh: forceRefresh); + } -// 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. Only called after init future has run -// // so we dont have to wait for that here. -// _sspUpdate.busyUpdate>( -// busy, (emit) async => _refreshInner(emit)); -// } + Future _refreshNoWait({bool forceRefresh = false}) async => + busy((emit) async => _refreshInner(emit, forceRefresh: forceRefresh)); -// @override -// Future close() async { -// await _initWait(); -// await _subscription?.cancel(); -// _subscription = null; -// if (_wantsCloseRecord) { -// await _shortArray.close(); -// } -// await super.close(); -// } + Future _refreshInner(void Function(AsyncValue>) emit, + {bool forceRefresh = false}) async { + final avElements = await _loadElements(_tail, _count); + final err = avElements.asError; + if (err != null) { + emit(AsyncValue.error(err.error, err.stackTrace)); + return; + } + final loading = avElements.asLoading; + if (loading != null) { + emit(const AsyncValue.loading()); + return; + } + final elements = avElements.asData!.value; + emit(AsyncValue.data(DHTLogStateData( + elements: elements, tail: _tail, count: _count, follow: _follow))); + } -// Future operate(Future Function(DHTShortArrayRead) closure) async { -// await _initWait(); -// return _shortArray.operate(closure); -// } + Future>>> _loadElements( + int tail, int count, + {bool forceRefresh = false}) async { + try { + final allItems = await _log.operate((reader) async { + final length = reader.length; + final end = ((tail - 1) % length) + 1; + final start = (count < end) ? end - count : 0; -// Future<(R?, bool)> operateWrite( -// Future Function(DHTShortArrayWrite) closure) async { -// await _initWait(); -// return _shortArray.operateWrite(closure); -// } + final offlinePositions = await reader.getOfflinePositions(); + final allItems = (await reader.getItemRange(start, + length: end - start, forceRefresh: forceRefresh)) + ?.indexed + .map((x) => DHTLogElementState( + value: _decodeElement(x.$2), + isOffline: offlinePositions.contains(x.$1))) + .toIList(); + return allItems; + }); + if (allItems == null) { + return const AsyncValue.loading(); + } + return AsyncValue.data(allItems); + } on Exception catch (e, st) { + return AsyncValue.error(e, st); + } + } -// Future operateWriteEventual( -// Future Function(DHTShortArrayWrite) closure, -// {Duration? timeout}) async { -// await _initWait(); -// return _shortArray.operateWriteEventual(closure, timeout: timeout); -// } + void _update(DHTLogUpdate upd) { + // Run at most one background update process + // 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. -// final WaitSet _initWait = WaitSet(); -// late final DHTShortArray _shortArray; -// final T Function(List data) _decodeElement; -// StreamSubscription? _subscription; -// bool _wantsCloseRecord = false; -// final _sspUpdate = SingleStatelessProcessor(); -// } + // Accumulate head and tail deltas + _headDelta += upd.headDelta; + _tailDelta += upd.tailDelta; + + _sspUpdate.busyUpdate>(busy, (emit) async { + // apply follow + if (_follow) { + if (_tail <= 0) { + // Negative tail is already following tail changes + } else { + // Positive tail is measured from the head, so apply deltas + _tail = (_tail + _tailDelta - _headDelta) % upd.length; + } + } else { + if (_tail <= 0) { + // Negative tail is following tail changes so apply deltas + var posTail = _tail + upd.length; + posTail = (posTail + _tailDelta - _headDelta) % upd.length; + _tail = posTail - upd.length; + } else { + // Positive tail is measured from head so not following tail + } + } + _headDelta = 0; + _tailDelta = 0; + + await _refreshInner(emit); + }); + } + + @override + Future close() async { + await _initWait(); + await _subscription?.cancel(); + _subscription = null; + if (_wantsCloseRecord) { + await _log.close(); + } + await super.close(); + } + + Future operate(Future Function(DHTRandomRead) closure) async { + await _initWait(); + return _log.operate(closure); + } + + Future operateAppend( + Future Function(DHTAppendTruncateRandomRead) closure) async { + await _initWait(); + return _log.operateAppend(closure); + } + + Future operateAppendEventual( + Future Function(DHTAppendTruncateRandomRead) closure, + {Duration? timeout}) async { + await _initWait(); + return _log.operateAppendEventual(closure, timeout: timeout); + } + + final WaitSet _initWait = WaitSet(); + late final DHTLog _log; + final T Function(List data) _decodeElement; + StreamSubscription? _subscription; + bool _wantsCloseRecord = false; + final _sspUpdate = SingleStatelessProcessor(); + + // Accumulated deltas since last update + var _headDelta = 0; + var _tailDelta = 0; + + // Cubit window into the DHTLog + var _tail = 0; + var _count = DHTShortArray.maxElements; + var _follow = true; +} diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index 76a3f0c..65f7110 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -105,13 +105,11 @@ class _DHTLogSpine { try { final out = await closure(this); // Write head assuming it has been changed - if (!await writeSpineHead()) { + if (!await writeSpineHead(old: (oldHead, oldTail))) { // Failed to write head means head got overwritten so write should // be considered failed throw DHTExceptionTryAgain(); } - - onUpdatedSpine?.call(); return out; } on Exception { // Exception means state needs to be reverted @@ -134,7 +132,6 @@ class _DHTLogSpine { try { // Iterate until we have a successful element and head write - do { // Save off old values each pass of writeSpineHead because the head // will have changed @@ -158,9 +155,7 @@ class _DHTLogSpine { } // Try to do the head write - } while (!await writeSpineHead()); - - onUpdatedSpine?.call(); + } while (!await writeSpineHead(old: (oldHead, oldTail))); } on Exception { // Exception means state needs to be reverted _head = oldHead; @@ -173,7 +168,7 @@ class _DHTLogSpine { /// Serialize and write out the current spine head subkey, possibly updating /// it if a newer copy is available online. Returns true if the write was /// successful - Future writeSpineHead() async { + Future writeSpineHead({(int, int)? old}) async { assert(_spineMutex.isLocked, 'should be in mutex here'); final headBuffer = _toProto().writeToBuffer(); @@ -182,12 +177,28 @@ class _DHTLogSpine { if (existingData != null) { // Head write failed, incorporate update await _updateHead(proto.DHTLog.fromBuffer(existingData)); + if (old != null) { + sendUpdate(old.$1, old.$2); + } return false; } - + if (old != null) { + sendUpdate(old.$1, old.$2); + } return true; } + /// Send a spine update callback + void sendUpdate(int oldHead, int oldTail) { + final oldLength = _ringDistance(oldTail, oldHead); + if (oldHead != _head || oldTail != _tail || oldLength != length) { + onUpdatedSpine?.call(DHTLogUpdate( + headDelta: _ringDistance(_head, oldHead), + tailDelta: _ringDistance(_tail, oldTail), + length: length)); + } + } + /// Validate a new spine head subkey that has come in from the network Future _updateHead(proto.DHTLog spineHead) async { assert(_spineMutex.isLocked, 'should be in mutex here'); @@ -486,8 +497,10 @@ class _DHTLogSpine { // Then update the head record await _spineMutex.protect(() async { + final oldHead = _head; + final oldTail = _tail; await _updateHead(headData); - onUpdatedSpine?.call(); + sendUpdate(oldHead, oldTail); }); } @@ -495,10 +508,14 @@ class _DHTLogSpine { TypedKey get recordKey => _spineRecord.key; OwnedDHTRecordPointer get recordPointer => _spineRecord.ownedDHTRecordPointer; - int get length => - (_tail < _head) ? (_positionLimit - _head) + _tail : _tail - _head; + int get length => _ringDistance(_tail, _head); + bool get isOpen => _spineRecord.isOpen; + // Ring buffer distance from old to new + static int _ringDistance(int n, int o) => + (n < o) ? (_positionLimit - o) + n : n - o; + static const _positionLimit = DHTLog.segmentsPerSubkey * DHTLog.spineSubkeys * DHTShortArray.maxElements; @@ -508,7 +525,7 @@ class _DHTLogSpine { // Subscription to head record internal changes StreamSubscription? _subscription; // Notify closure for external spine head changes - void Function()? onUpdatedSpine; + void Function(DHTLogUpdate)? onUpdatedSpine; // Spine DHT record final DHTRecord _spineRecord; 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 a305e22..082a391 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 @@ -183,7 +183,8 @@ class DHTShortArray implements DHTOpenable { /// Runs a closure allowing read-write access to the shortarray /// Makes only one attempt to consistently write the changes to the DHT /// Returns result of the closure if the write could be performed - /// Throws DHTOperateException if the write could not be performed at this time + /// Throws DHTOperateException if the write could not be performed + /// at this time Future operateWrite( Future Function(DHTRandomReadWrite) closure) async { if (!isOpen) { From 8cd73b2844c3894dc6fde77a1e18890294e29288 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 15 May 2024 22:45:50 -0400 Subject: [PATCH 098/270] checkpoint --- .../example/integration_test/app_test.dart | 44 ++++-- .../fixtures/dht_record_pool_fixture.dart | 13 +- .../integration_test/test_dht_log.dart | 130 ++++++++++++++++++ .../test_dht_record_pool.dart | 74 +++++----- .../test_dht_short_array.dart | 38 ++++- .../lib/dht_support/src/dht_log/dht_log.dart | 1 + .../src/dht_log/dht_log_append.dart | 60 ++++++-- .../src/dht_log/dht_log_spine.dart | 83 +++++++++-- .../src/dht_record/dht_record_pool.dart | 106 +++++++++++--- .../dht_short_array/dht_short_array_head.dart | 8 +- .../dht_short_array_write.dart | 40 ++++-- .../src/interfaces/dht_append_truncate.dart | 7 + .../src/interfaces/dht_openable.dart | 8 +- .../src/interfaces/dht_random_write.dart | 15 ++ 14 files changed, 513 insertions(+), 114 deletions(-) create mode 100644 packages/veilid_support/example/integration_test/test_dht_log.dart diff --git a/packages/veilid_support/example/integration_test/app_test.dart b/packages/veilid_support/example/integration_test/app_test.dart index 4a5b1e1..ba7785c 100644 --- a/packages/veilid_support/example/integration_test/app_test.dart +++ b/packages/veilid_support/example/integration_test/app_test.dart @@ -7,6 +7,7 @@ import 'package:integration_test/integration_test.dart'; import 'package:veilid_test/veilid_test.dart'; import 'fixtures/fixtures.dart'; +import 'test_dht_log.dart'; import 'test_dht_record_pool.dart'; import 'test_dht_short_array.dart'; @@ -38,24 +39,37 @@ void main() { test('create pool', testDHTRecordPoolCreate); - group('DHTRecordPool Tests', () { + // group('DHTRecordPool Tests', () { + // setUpAll(dhtRecordPoolFixture.setUp); + // tearDownAll(dhtRecordPoolFixture.tearDown); + + // test('create/delete record', testDHTRecordCreateDelete); + // test('record scopes', testDHTRecordScopes); + // test('create/delete deep record', testDHTRecordDeepCreateDelete); + // }); + + // group('DHTShortArray Tests', () { + // setUpAll(dhtRecordPoolFixture.setUp); + // tearDownAll(dhtRecordPoolFixture.tearDown); + + // for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { + // test('create shortarray stride=$stride', + // makeTestDHTShortArrayCreateDelete(stride: stride)); + // test('add shortarray stride=$stride', + // makeTestDHTShortArrayAdd(stride: 256)); + // } + // }); + + group('DHTLog Tests', () { setUpAll(dhtRecordPoolFixture.setUp); tearDownAll(dhtRecordPoolFixture.tearDown); - test('create/delete record', testDHTRecordCreateDelete); - test('record scopes', testDHTRecordScopes); - test('create/delete deep record', testDHTRecordDeepCreateDelete); - }); - - group('DHTShortArray Tests', () { - setUpAll(dhtRecordPoolFixture.setUp); - tearDownAll(dhtRecordPoolFixture.tearDown); - - for (final stride in [256, 64, 32, 16, 8, 4, 2, 1]) { - test('create shortarray stride=$stride', - makeTestDHTShortArrayCreateDelete(stride: stride)); - test('add shortarray stride=$stride', - makeTestDHTShortArrayAdd(stride: 256)); + for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { + test('create log stride=$stride', + makeTestDHTLogCreateDelete(stride: stride)); + test('add/truncate log stride=$stride', + makeTestDHTLogAddTruncate(stride: 256), + timeout: const Timeout(Duration(seconds: 480))); } }); }); diff --git a/packages/veilid_support/example/integration_test/fixtures/dht_record_pool_fixture.dart b/packages/veilid_support/example/integration_test/fixtures/dht_record_pool_fixture.dart index 216d00f..d38181f 100644 --- a/packages/veilid_support/example/integration_test/fixtures/dht_record_pool_fixture.dart +++ b/packages/veilid_support/example/integration_test/fixtures/dht_record_pool_fixture.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:async_tools/async_tools.dart'; +import 'package:flutter/foundation.dart'; import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_test/veilid_test.dart'; @@ -12,9 +13,13 @@ class DHTRecordPoolFixture implements TickerFixtureTickable { UpdateProcessorFixture updateProcessorFixture; TickerFixture tickerFixture; - Future setUp() async { + Future setUp({bool purge = true}) async { await _fixtureMutex.acquire(); - await DHTRecordPool.init(); + if (purge) { + await Veilid.instance.debug('record purge local'); + await Veilid.instance.debug('record purge remote'); + } + await DHTRecordPool.init(logger: debugPrintSynchronously); tickerFixture.register(this); } @@ -22,6 +27,10 @@ class DHTRecordPoolFixture implements TickerFixtureTickable { assert(_fixtureMutex.isLocked, 'should not tearDown without setUp'); tickerFixture.unregister(this); await DHTRecordPool.close(); + + final recordList = await Veilid.instance.debug('record list local'); + debugPrintSynchronously('DHT Record List:\n$recordList'); + _fixtureMutex.release(); } diff --git a/packages/veilid_support/example/integration_test/test_dht_log.dart b/packages/veilid_support/example/integration_test/test_dht_log.dart new file mode 100644 index 0000000..fcdabad --- /dev/null +++ b/packages/veilid_support/example/integration_test/test_dht_log.dart @@ -0,0 +1,130 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:veilid_support/veilid_support.dart'; + +Future Function() makeTestDHTLogCreateDelete({required int stride}) => + () async { + // Close before delete + { + final dlog = await DHTLog.create( + debugName: 'log_create_delete 1 stride $stride', stride: stride); + expect(await dlog.operate((r) async => r.length), isZero); + expect(dlog.isOpen, isTrue); + await dlog.close(); + expect(dlog.isOpen, isFalse); + await dlog.delete(); + // Operate should fail + await expectLater(() async => dlog.operate((r) async => r.length), + throwsA(isA())); + } + + // Close after delete + { + final dlog = await DHTLog.create( + debugName: 'log_create_delete 2 stride $stride', stride: stride); + await dlog.delete(); + // Operate should still succeed because things aren't closed + expect(await dlog.operate((r) async => r.length), isZero); + await dlog.close(); + // Operate should fail + await expectLater(() async => dlog.operate((r) async => r.length), + throwsA(isA())); + } + + // Close after delete multiple + // Okay to request delete multiple times before close + { + final dlog = await DHTLog.create( + debugName: 'log_create_delete 3 stride $stride', stride: stride); + await dlog.delete(); + await dlog.delete(); + // Operate should still succeed because things aren't closed + expect(await dlog.operate((r) async => r.length), isZero); + await dlog.close(); + await dlog.close(); + // Operate should fail + await expectLater(() async => dlog.operate((r) async => r.length), + throwsA(isA())); + } + }; + +Future Function() makeTestDHTLogAddTruncate({required int stride}) => + () async { + final startTime = DateTime.now(); + + final dlog = await DHTLog.create( + debugName: 'log_add 1 stride $stride', stride: stride); + + final dataset = Iterable.generate(1000) + .map((n) => utf8.encode('elem $n')) + .toList(); + + print('adding\n'); + { + final res = await dlog.operateAppend((w) async { + const chunk = 50; + for (var n = 0; n < dataset.length; n += chunk) { + print('$n-${n + chunk - 1} '); + final success = + await w.tryAppendItems(dataset.sublist(n, n + chunk)); + expect(success, isTrue); + } + }); + expect(res, isNull); + } + + print('get all\n'); + { + final dataset2 = await dlog.operate((r) async => r.getItemRange(0)); + expect(dataset2, equals(dataset)); + } + { + final dataset3 = + await dlog.operate((r) async => r.getItemRange(64, length: 128)); + expect(dataset3, equals(dataset.sublist(64, 64 + 128))); + } + { + final dataset4 = + await dlog.operate((r) async => r.getItemRange(0, length: 1000)); + expect(dataset4, equals(dataset.sublist(0, 1000))); + } + { + final dataset5 = + await dlog.operate((r) async => r.getItemRange(500, length: 499)); + expect(dataset5, equals(dataset.sublist(500, 999))); + } + print('truncate\n'); + { + await dlog.operateAppend((w) async => w.truncate(5)); + } + { + final dataset6 = await dlog + .operate((r) async => r.getItemRange(500 - 5, length: 499)); + expect(dataset6, equals(dataset.sublist(500, 999))); + } + print('truncate 2\n'); + { + await dlog.operateAppend((w) async => w.truncate(251)); + } + { + final dataset7 = await dlog + .operate((r) async => r.getItemRange(500 - 256, length: 499)); + expect(dataset7, equals(dataset.sublist(500, 999))); + } + print('clear\n'); + { + await dlog.operateAppend((w) async => w.clear()); + } + print('get all\n'); + { + final dataset8 = await dlog.operate((r) async => r.getItemRange(0)); + expect(dataset8, isEmpty); + } + + await dlog.delete(); + await dlog.close(); + + final endTime = DateTime.now(); + print('Duration: ${endTime.difference(startTime)}'); + }; diff --git a/packages/veilid_support/example/integration_test/test_dht_record_pool.dart b/packages/veilid_support/example/integration_test/test_dht_record_pool.dart index 2f05d0b..45b26a7 100644 --- a/packages/veilid_support/example/integration_test/test_dht_record_pool.dart +++ b/packages/veilid_support/example/integration_test/test_dht_record_pool.dart @@ -151,17 +151,29 @@ Future testDHTRecordDeepCreateDelete() async { // Make root record final recroot = await pool.createRecord(debugName: 'test_deep_create_delete'); - for (var d = 0; d < numIterations; d++) { - // Make child set 1 - var parent = recroot; - final children = []; - for (var n = 0; n < numChildren; n++) { - final child = - await pool.createRecord(debugName: 'deep $n', parent: parent.key); - children.add(child); - parent = child; - } + // Make child set 1 + var parent = recroot; + final children = []; + for (var n = 0; n < numChildren; n++) { + final child = + await pool.createRecord(debugName: 'deep $n', parent: parent.key); + children.add(child); + parent = child; + } + // Should mark for deletion + expect(await pool.deleteRecord(recroot.key), isFalse); + + // Root should still be valid + expect(await pool.isValidRecordKey(recroot.key), isTrue); + + // Close root record + await recroot.close(); + + // Root should still be valid because children still exist + expect(await pool.isValidRecordKey(recroot.key), isTrue); + + for (var d = 0; d < numIterations; d++) { // Make child set 2 final children2 = []; parent = recroot; @@ -171,31 +183,31 @@ Future testDHTRecordDeepCreateDelete() async { children2.add(child); parent = child; } - // Should fail to delete root - await expectLater( - () async => pool.deleteRecord(recroot.key), throwsA(isA())); - - // Close child set 1 - await children.map((c) => c.close()).wait; - - // Delete child set 1 in reverse order - for (var n = numChildren - 1; n >= 0; n--) { - await pool.deleteRecord(children[n].key); - } - - // Should fail to delete root - await expectLater( - () async => pool.deleteRecord(recroot.key), throwsA(isA())); - - // Close child set 1 - await children2.map((c) => c.close()).wait; // Delete child set 2 in reverse order for (var n = numChildren - 1; n >= 0; n--) { - await pool.deleteRecord(children2[n].key); + expect(await pool.deleteRecord(children2[n].key), isFalse); } + + // Root should still be there + expect(await pool.isValidRecordKey(recroot.key), isTrue); + + // Close child set 2 + await children2.map((c) => c.close()).wait; + + // All child set 2 should be invalid + for (final c2 in children2) { + // Children should be invalid and deleted now + expect(await pool.isValidRecordKey(c2.key), isFalse); + } + + // Root should still be valid + expect(await pool.isValidRecordKey(recroot.key), isTrue); } - // Should be able to delete root now - await pool.deleteRecord(recroot.key); + // Close child set 1 + await children.map((c) => c.close()).wait; + + // Root should have gone away + expect(await pool.isValidRecordKey(recroot.key), isFalse); } diff --git a/packages/veilid_support/example/integration_test/test_dht_short_array.dart b/packages/veilid_support/example/integration_test/test_dht_short_array.dart index c2fcc2b..6ba2d23 100644 --- a/packages/veilid_support/example/integration_test/test_dht_short_array.dart +++ b/packages/veilid_support/example/integration_test/test_dht_short_array.dart @@ -61,10 +61,10 @@ Future Function() makeTestDHTShortArrayAdd({required int stride}) => .map((n) => utf8.encode('elem $n')) .toList(); - print('adding\n'); + print('adding singles\n'); { final res = await arr.operateWrite((w) async { - for (var n = 0; n < dataset.length; n++) { + for (var n = 4; n < 8; n++) { print('$n '); final success = await w.tryAddItem(dataset[n]); expect(success, isTrue); @@ -73,6 +73,40 @@ Future Function() makeTestDHTShortArrayAdd({required int stride}) => expect(res, isNull); } + print('adding batch\n'); + { + final res = await arr.operateWrite((w) async { + print('${dataset.length ~/ 2}-${dataset.length}'); + final success = await w.tryAddItems( + dataset.sublist(dataset.length ~/ 2, dataset.length)); + expect(success, isTrue); + }); + expect(res, isNull); + } + + print('inserting singles\n'); + { + final res = await arr.operateWrite((w) async { + for (var n = 0; n < 4; n++) { + print('$n '); + final success = await w.tryInsertItem(n, dataset[n]); + expect(success, isTrue); + } + }); + expect(res, isNull); + } + + print('inserting batch\n'); + { + final res = await arr.operateWrite((w) async { + print('8-${dataset.length ~/ 2}'); + final success = await w.tryInsertItems( + 8, dataset.sublist(8, dataset.length ~/ 2)); + expect(success, isTrue); + }); + expect(res, isNull); + } + //print('get all\n'); { final dataset2 = await arr.operate((r) async => r.getItemRange(0)); diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart index 7513c8b..f7b606c 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math'; import 'dart:typed_data'; import 'package:async_tools/async_tools.dart'; diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_append.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_append.dart index 6a172a7..96c3eb4 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_append.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_append.dart @@ -9,34 +9,74 @@ class _DHTLogAppend extends _DHTLogRead implements DHTAppendTruncateRandomRead { @override Future tryAppendItem(Uint8List value) async { // Allocate empty index at the end of the list - final endPos = _spine.length; + final insertPos = _spine.length; _spine.allocateTail(1); - final lookup = await _spine.lookupPosition(endPos); + final lookup = await _spine.lookupPosition(insertPos); if (lookup == null) { throw StateError("can't write to dht log"); } + // Write item to the segment - return lookup.shortArray - .operateWrite((write) async => write.tryWriteItem(lookup.pos, value)); + return lookup.shortArray.operateWrite((write) async { + // If this a new segment, then clear it in case we have wrapped around + if (lookup.pos == 0) { + await write.clear(); + } else if (lookup.pos != write.length) { + // We should always be appending at the length + throw StateError('appending should be at the end'); + } + return write.tryAddItem(value); + }); + } + + @override + Future tryAppendItems(List values) async { + // Allocate empty index at the end of the list + final insertPos = _spine.length; + _spine.allocateTail(values.length); + + // Look up the first position and shortarray + for (var valueIdx = 0; valueIdx < values.length;) { + final remaining = values.length - valueIdx; + + final lookup = await _spine.lookupPosition(insertPos + valueIdx); + if (lookup == null) { + throw StateError("can't write to dht log"); + } + + final sacount = min(remaining, DHTShortArray.maxElements - lookup.pos); + final success = await lookup.shortArray.operateWrite((write) async { + // If this a new segment, then clear it in case we have wrapped around + if (lookup.pos == 0) { + await write.clear(); + } else if (lookup.pos != write.length) { + // We should always be appending at the length + throw StateError('appending should be at the end'); + } + return write.tryAddItems(values.sublist(valueIdx, valueIdx + sacount)); + }); + if (!success) { + return false; + } + valueIdx += sacount; + } + return true; } @override Future truncate(int count) async { - final len = _spine.length; - if (count > len) { - count = len; - } + count = min(count, _spine.length); if (count == 0) { return; } if (count < 0) { throw StateError('can not remove negative items'); } - _spine.releaseHead(count); + await _spine.releaseHead(count); } @override Future clear() async { - _spine.releaseHead(_spine.length); + await _spine.releaseHead(_spine.length); } } diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index 65f7110..7ce3dbf 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -83,12 +83,8 @@ class _DHTLogSpine { Future delete() async { await _spineMutex.protect(() async { - final pool = DHTRecordPool.instance; - final futures = >[pool.deleteRecord(_spineRecord.key)]; - for (final (_, sc) in _spineCache) { - futures.add(sc.delete()); - } - await Future.wait(futures); + // Will deep delete all segment records as they are children + await _spineRecord.delete(); }); } @@ -218,7 +214,7 @@ class _DHTLogSpine { static TypedKey? _getSegmentKey(Uint8List subkeyData, int segment) { final decodedLength = TypedKey.decodedLength(); final segmentKeyBytes = subkeyData.sublist( - decodedLength * segment, (decodedLength + 1) * segment); + decodedLength * segment, decodedLength * (segment + 1)); if (segmentKeyBytes.equals(_emptySegmentKey)) { return null; } @@ -234,7 +230,7 @@ class _DHTLogSpine { } else { segmentKeyBytes = segmentKey.decode(); } - subkeyData.setRange(decodedLength * segment, (decodedLength + 1) * segment, + subkeyData.setRange(decodedLength * segment, decodedLength * (segment + 1), segmentKeyBytes); } @@ -435,7 +431,7 @@ class _DHTLogSpine { _tail = (_tail + count) % _positionLimit; } - void releaseHead(int count) { + Future releaseHead(int count) async { assert(_spineMutex.isLocked, 'should be locked'); final currentLength = length; @@ -447,6 +443,73 @@ class _DHTLogSpine { } _head = (_head + count) % _positionLimit; + await _purgeUnusedSegments(); + } + + Future _deleteSegmentsContiguous(int start, int end) async { + assert(_spineMutex.isLocked, 'should be in mutex here'); + + final startSegmentNumber = start ~/ DHTShortArray.maxElements; + final startSegmentPos = start % DHTShortArray.maxElements; + + final endSegmentNumber = end ~/ DHTShortArray.maxElements; + final endSegmentPos = end % DHTShortArray.maxElements; + + final firstDeleteSegment = + (startSegmentPos == 0) ? startSegmentNumber : startSegmentNumber + 1; + final lastDeleteSegment = + (endSegmentPos == 0) ? endSegmentNumber - 1 : endSegmentNumber - 2; + + int? lastSubkey; + Uint8List? subkeyData; + for (var segmentNumber = firstDeleteSegment; + segmentNumber <= lastDeleteSegment; + segmentNumber++) { + // Lookup what subkey and segment subrange has this position's segment + // shortarray + final l = lookupSegment(segmentNumber); + final subkey = l.subkey; + final segment = l.segment; + + if (lastSubkey != subkey) { + // Flush subkey writes + if (lastSubkey != null) { + await _spineRecord.eventualWriteBytes(subkeyData!, + subkey: lastSubkey); + } + + xxx debug this, it takes forever + + // Get next subkey + subkeyData = await _spineRecord.get(subkey: subkey); + if (subkeyData != null) { + lastSubkey = subkey; + } else { + lastSubkey = null; + } + } + if (subkeyData != null) { + final segmentKey = _getSegmentKey(subkeyData, segment); + if (segmentKey != null) { + await DHTRecordPool.instance.deleteRecord(segmentKey); + _setSegmentKey(subkeyData, segment, null); + } + } + } + // Flush subkey writes + if (lastSubkey != null) { + await _spineRecord.eventualWriteBytes(subkeyData!, subkey: lastSubkey); + } + } + + Future _purgeUnusedSegments() async { + assert(_spineMutex.isLocked, 'should be in mutex here'); + if (_head < _tail) { + await _deleteSegmentsContiguous(0, _head); + await _deleteSegmentsContiguous(_tail, _positionLimit); + } else if (_head > _tail) { + await _deleteSegmentsContiguous(_tail, _head); + } } ///////////////////////////////////////////////////////////////////////////// @@ -532,7 +595,7 @@ class _DHTLogSpine { // Position of the start of the log (oldest items) int _head; - // Position of the end of the log (newest items) + // Position of the end of the log (newest items) (exclusive) int _tail; // LRU cache of DHT spine elements accessed recently 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 a64f461..a4748df 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 @@ -91,7 +91,6 @@ class SharedDHTRecordData { Map subkeySeqCache = {}; bool needsWatchStateUpdate = false; WatchState? unionWatchState; - bool deleteOnClose = false; } // Per opened record data @@ -128,6 +127,7 @@ class DHTRecordPool with TableDBBackedJson { : _state = const DHTRecordPoolAllocations(), _mutex = Mutex(), _opened = {}, + _markedForDelete = {}, _routingContext = routingContext, _veilid = veilid; @@ -140,6 +140,8 @@ class DHTRecordPool with TableDBBackedJson { final Mutex _mutex; // Which DHT records are currently open final Map _opened; + // Which DHT records are marked for deletion + final Set _markedForDelete; // Default routing context to use for new keys final VeilidRoutingContext _routingContext; // Convenience accessor @@ -288,6 +290,8 @@ class DHTRecordPool with TableDBBackedJson { return openedRecordInfo; } + // Called when a DHTRecord is closed + // Cleans up the opened record housekeeping and processes any late deletions Future _recordClosed(DHTRecord record) async { await _mutex.protect(() async { final key = record.key; @@ -301,14 +305,37 @@ class DHTRecordPool with TableDBBackedJson { } if (openedRecordInfo.records.isEmpty) { await _routingContext.closeDHTRecord(key); - if (openedRecordInfo.shared.deleteOnClose) { - await _deleteRecordInner(key); - } _opened.remove(key); + + await _checkForLateDeletesInner(key); } }); } + // Check to see if this key can finally be deleted + // If any parents are marked for deletion, try them first + Future _checkForLateDeletesInner(TypedKey key) async { + // Get parent list in bottom up order including our own key + final parents = []; + TypedKey? nextParent = key; + while (nextParent != null) { + parents.add(nextParent); + nextParent = getParentRecordKey(nextParent); + } + + // If any parent is ready to delete all its children do it + for (final parent in parents) { + if (_markedForDelete.contains(parent)) { + final deleted = await _deleteRecordInner(parent); + if (!deleted) { + // If we couldn't delete a child then no 'marked for delete' parents + // above us will be ready to delete either + break; + } + } + } + } + // Collect all dependencies (including the record itself) // in reverse (bottom-up/delete order) List _collectChildrenInner(TypedKey recordKey) { @@ -328,7 +355,13 @@ class DHTRecordPool with TableDBBackedJson { return allDeps.reversedView; } - String _debugChildren(TypedKey recordKey, {List? allDeps}) { + /// Collect all dependencies (including the record itself) + /// in reverse (bottom-up/delete order) + Future> collectChildren(TypedKey recordKey) => + _mutex.protect(() async => _collectChildrenInner(recordKey)); + + /// Print children + String debugChildren(TypedKey recordKey, {List? allDeps}) { allDeps ??= _collectChildrenInner(recordKey); // ignore: avoid_print var out = @@ -342,32 +375,48 @@ class DHTRecordPool with TableDBBackedJson { return out; } - Future _deleteRecordInner(TypedKey recordKey) async { - log('deleteDHTRecord: key=$recordKey'); + // Actual delete function + Future _finalizeDeleteRecordInner(TypedKey recordKey) async { + log('_finalizeDeleteRecordInner: key=$recordKey'); // Remove this child from parents await _removeDependenciesInner([recordKey]); await _routingContext.deleteDHTRecord(recordKey); + _markedForDelete.remove(recordKey); } - Future deleteRecord(TypedKey recordKey) async { - await _mutex.protect(() async { - final allDeps = _collectChildrenInner(recordKey); - - if (allDeps.singleOrNull != recordKey) { - final dbgstr = _debugChildren(recordKey, allDeps: allDeps); - throw StateError('must delete children first: $dbgstr'); + // Deep delete mechanism inside mutex + Future _deleteRecordInner(TypedKey recordKey) async { + final toDelete = _readyForDeleteInner(recordKey); + if (toDelete.isNotEmpty) { + // delete now + for (final deleteKey in toDelete) { + await _finalizeDeleteRecordInner(deleteKey); } + return true; + } + // mark for deletion + _markedForDelete.add(recordKey); + return false; + } - final ori = _opened[recordKey]; - if (ori != null) { - // delete after close - ori.shared.deleteOnClose = true; - } else { - // delete now - await _deleteRecordInner(recordKey); + /// Delete a record and its children if they are all closed + /// otherwise mark that record for deletion eventually + /// Returns true if the deletion was processed immediately + /// Returns false if the deletion was marked for later + Future deleteRecord(TypedKey recordKey) async => + _mutex.protect(() async => _deleteRecordInner(recordKey)); + + // If everything underneath is closed including itself, return the + // list of children (and itself) to finally actually delete + List _readyForDeleteInner(TypedKey recordKey) { + final allDeps = _collectChildrenInner(recordKey); + for (final dep in allDeps) { + if (_opened.containsKey(dep)) { + return []; } - }); + } + return allDeps; } void _validateParentInner(TypedKey? parent, TypedKey child) { @@ -456,6 +505,19 @@ class DHTRecordPool with TableDBBackedJson { } } + bool _isValidRecordKeyInner(TypedKey key) { + if (_state.rootRecords.contains(key)) { + return true; + } + if (_state.childrenByParent.containsKey(key.toJson())) { + return true; + } + return false; + } + + Future isValidRecordKey(TypedKey key) => + _mutex.protect(() async => _isValidRecordKeyInner(key)); + /////////////////////////////////////////////////////////////////////// /// Create a root DHTRecord that has no dependent records 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 0a2b7d2..e2bf392 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 @@ -67,12 +67,8 @@ class _DHTShortArrayHead { Future delete() async { await _headMutex.protect(() async { - final pool = DHTRecordPool.instance; - final futures = >[pool.deleteRecord(_headRecord.key)]; - for (final lr in _linkedRecords) { - futures.add(pool.deleteRecord(lr.key)); - } - await Future.wait(futures); + // Will deep delete all linked records as they are children + await _headRecord.delete(); }); } 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 index 0d51663..dbd8984 100644 --- 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 @@ -8,19 +8,12 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead _DHTShortArrayWrite._(super.head) : super._(); @override - Future tryAddItem(Uint8List value) async { - // Allocate empty index at the end of the list - final pos = _head.length; - _head.allocateIndex(pos); + Future tryAddItem(Uint8List value) => + tryInsertItem(_head.length, value); - // Write item - final ok = await tryWriteItem(pos, value); - if (!ok) { - _head.freeIndex(pos); - } - - return ok; - } + @override + Future tryAddItems(List values) => + tryInsertItems(_head.length, values); @override Future tryInsertItem(int pos, Uint8List value) async { @@ -35,6 +28,29 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead return true; } + @override + Future tryInsertItems(int pos, List values) async { + // Allocate empty indices at the end of the list + for (var i = 0; i < values.length; i++) { + _head.allocateIndex(pos + i); + } + + // Write items + var success = true; + final dws = DelayedWaitSet(); + for (var i = 0; i < values.length; i++) { + dws.add(() async { + final ok = await tryWriteItem(pos + i, values[i]); + if (!ok) { + _head.freeIndex(pos + i); + success = false; + } + }); + } + await dws(chunkSize: maxDHTConcurrency, onChunkDone: (_) => success); + return success; + } + @override Future swapItem(int aPos, int bPos) async { if (aPos < 0 || aPos >= _head.length) { diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_append_truncate.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_append_truncate.dart index babcc7d..d98037c 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_append_truncate.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_append_truncate.dart @@ -14,6 +14,13 @@ abstract class DHTAppendTruncate { /// This may throw an exception if the number elements added exceeds limits. Future tryAppendItem(Uint8List value); + /// Try to add a list of items to the end of the DHT data structure. + /// Return true if the elements were 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 limits. + Future tryAppendItems(List values); + /// Try to remove a number of items from the head of the DHT data structure. /// Throws StateError if count < 0 Future truncate(int count); diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_openable.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_openable.dart index 1ee1140..e28f703 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_openable.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_openable.dart @@ -29,12 +29,12 @@ extension DHTOpenableExt on D { } try { - final out = await scopeFunction(this); - await close(); - return out; - } on Exception catch (_) { + return await scopeFunction(this); + } on Exception { await delete(); rethrow; + } finally { + await close(); } } diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart index 53f307c..17a450e 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart @@ -30,6 +30,13 @@ abstract class DHTRandomWrite { /// built-in limit of 'maxElements = 256' entries. Future tryAddItem(Uint8List value); + /// Try to add a list of items to the end of the DHTArray. Return true if the + /// elements were successfully added, and false if the state changed before + /// the elements 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 tryAddItems(List values); + /// Try to insert an item as position 'pos' of the DHTArray. /// 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 @@ -38,6 +45,14 @@ abstract class DHTRandomWrite { /// built-in limit of 'maxElements = 256' entries. Future tryInsertItem(int pos, Uint8List value); + /// Try to insert items at position 'pos' of the DHTArray. + /// Return true if the elements were successfully inserted, and false if the + /// state changed before the elements 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 tryInsertItems(int pos, List values); + /// Swap items at position 'aPos' and 'bPos' in the DHTArray. /// Throws IndexError if either of the positions swapped exceed /// the length of the list From cf837e21764267c21ba3f3367521e0bbd80eb90b Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 16 May 2024 14:07:25 -0400 Subject: [PATCH 099/270] dht log passes tests --- .../example/integration_test/app_test.dart | 39 +++--- .../integration_test/test_dht_log.dart | 8 +- .../test_dht_record_pool.dart | 6 +- .../test_dht_short_array.dart | 7 +- .../lib/dht_support/src/dht_log/dht_log.dart | 39 ++++-- .../src/dht_log/dht_log_append.dart | 43 ++++--- .../dht_support/src/dht_log/dht_log_read.dart | 34 +++--- .../src/dht_log/dht_log_spine.dart | 98 ++++++++------- .../dht_record/default_dht_record_cubit.dart | 2 +- .../src/dht_record/dht_record.dart | 115 ++++++++++++------ .../src/dht_record/dht_record_cubit.dart | 2 +- .../src/dht_record/dht_record_pool.dart | 1 - .../src/dht_short_array/dht_short_array.dart | 39 ++++-- .../dht_short_array/dht_short_array_head.dart | 2 +- .../dht_short_array/dht_short_array_read.dart | 4 +- .../src/interfaces/dht_openable.dart | 5 +- packages/veilid_support/lib/src/identity.dart | 2 +- 17 files changed, 267 insertions(+), 179 deletions(-) diff --git a/packages/veilid_support/example/integration_test/app_test.dart b/packages/veilid_support/example/integration_test/app_test.dart index ba7785c..b3548a2 100644 --- a/packages/veilid_support/example/integration_test/app_test.dart +++ b/packages/veilid_support/example/integration_test/app_test.dart @@ -12,6 +12,8 @@ import 'test_dht_record_pool.dart'; import 'test_dht_short_array.dart'; void main() { + final startTime = DateTime.now(); + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); final veilidFixture = DefaultVeilidFixture(programName: 'veilid_support integration test'); @@ -39,26 +41,26 @@ void main() { test('create pool', testDHTRecordPoolCreate); - // group('DHTRecordPool Tests', () { - // setUpAll(dhtRecordPoolFixture.setUp); - // tearDownAll(dhtRecordPoolFixture.tearDown); + group('DHTRecordPool Tests', () { + setUpAll(dhtRecordPoolFixture.setUp); + tearDownAll(dhtRecordPoolFixture.tearDown); - // test('create/delete record', testDHTRecordCreateDelete); - // test('record scopes', testDHTRecordScopes); - // test('create/delete deep record', testDHTRecordDeepCreateDelete); - // }); + test('create/delete record', testDHTRecordCreateDelete); + test('record scopes', testDHTRecordScopes); + test('create/delete deep record', testDHTRecordDeepCreateDelete); + }); - // group('DHTShortArray Tests', () { - // setUpAll(dhtRecordPoolFixture.setUp); - // tearDownAll(dhtRecordPoolFixture.tearDown); + group('DHTShortArray Tests', () { + setUpAll(dhtRecordPoolFixture.setUp); + tearDownAll(dhtRecordPoolFixture.tearDown); - // for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { - // test('create shortarray stride=$stride', - // makeTestDHTShortArrayCreateDelete(stride: stride)); - // test('add shortarray stride=$stride', - // makeTestDHTShortArrayAdd(stride: 256)); - // } - // }); + for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { + test('create shortarray stride=$stride', + makeTestDHTShortArrayCreateDelete(stride: stride)); + test('add shortarray stride=$stride', + makeTestDHTShortArrayAdd(stride: 256)); + } + }); group('DHTLog Tests', () { setUpAll(dhtRecordPoolFixture.setUp); @@ -75,4 +77,7 @@ void main() { }); }); }); + + final endTime = DateTime.now(); + print('Duration: ${endTime.difference(startTime)}'); } diff --git a/packages/veilid_support/example/integration_test/test_dht_log.dart b/packages/veilid_support/example/integration_test/test_dht_log.dart index fcdabad..f8d758e 100644 --- a/packages/veilid_support/example/integration_test/test_dht_log.dart +++ b/packages/veilid_support/example/integration_test/test_dht_log.dart @@ -42,7 +42,7 @@ Future Function() makeTestDHTLogCreateDelete({required int stride}) => // Operate should still succeed because things aren't closed expect(await dlog.operate((r) async => r.length), isZero); await dlog.close(); - await dlog.close(); + await expectLater(() async => dlog.close(), throwsA(isA())); // Operate should fail await expectLater(() async => dlog.operate((r) async => r.length), throwsA(isA())); @@ -51,8 +51,6 @@ Future Function() makeTestDHTLogCreateDelete({required int stride}) => Future Function() makeTestDHTLogAddTruncate({required int stride}) => () async { - final startTime = DateTime.now(); - final dlog = await DHTLog.create( debugName: 'log_add 1 stride $stride', stride: stride); @@ -121,10 +119,8 @@ Future Function() makeTestDHTLogAddTruncate({required int stride}) => final dataset8 = await dlog.operate((r) async => r.getItemRange(0)); expect(dataset8, isEmpty); } + print('delete and close\n'); await dlog.delete(); await dlog.close(); - - final endTime = DateTime.now(); - print('Duration: ${endTime.difference(startTime)}'); }; diff --git a/packages/veilid_support/example/integration_test/test_dht_record_pool.dart b/packages/veilid_support/example/integration_test/test_dht_record_pool.dart index 45b26a7..2f52c00 100644 --- a/packages/veilid_support/example/integration_test/test_dht_record_pool.dart +++ b/packages/veilid_support/example/integration_test/test_dht_record_pool.dart @@ -48,7 +48,7 @@ Future testDHTRecordCreateDelete() async { // Set should succeed still await rec3.tryWriteBytes(utf8.encode('test')); await rec3.close(); - await rec3.close(); + await expectLater(() async => rec3.close(), throwsA(isA())); // Set should fail await expectLater(() async => rec3.tryWriteBytes(utf8.encode('test')), throwsA(isA())); @@ -84,7 +84,7 @@ Future testDHTRecordScopes() async { } on Exception { assert(false, 'should not throw'); } - await rec2.close(); + await expectLater(() async => rec2.close(), throwsA(isA())); await pool.deleteRecord(rec2.key); } @@ -115,6 +115,7 @@ Future testDHTRecordGetSet() async { final val = await rec.get(); await pool.deleteRecord(rec.key); expect(val, isNull); + await rec.close(); } // Test set then get @@ -125,6 +126,7 @@ Future testDHTRecordGetSet() async { // Invalid subkey should throw await expectLater( () async => rec2.get(subkey: 1), throwsA(isA())); + await rec2.close(); await pool.deleteRecord(rec2.key); } diff --git a/packages/veilid_support/example/integration_test/test_dht_short_array.dart b/packages/veilid_support/example/integration_test/test_dht_short_array.dart index 6ba2d23..7dead48 100644 --- a/packages/veilid_support/example/integration_test/test_dht_short_array.dart +++ b/packages/veilid_support/example/integration_test/test_dht_short_array.dart @@ -43,7 +43,7 @@ Future Function() makeTestDHTShortArrayCreateDelete( // Operate should still succeed because things aren't closed expect(await arr.operate((r) async => r.length), isZero); await arr.close(); - await arr.close(); + await expectLater(() async => arr.close(), throwsA(isA())); // Operate should fail await expectLater(() async => arr.operate((r) async => r.length), throwsA(isA())); @@ -52,8 +52,6 @@ Future Function() makeTestDHTShortArrayCreateDelete( Future Function() makeTestDHTShortArrayAdd({required int stride}) => () async { - final startTime = DateTime.now(); - final arr = await DHTShortArray.create( debugName: 'sa_add 1 stride $stride', stride: stride); @@ -131,7 +129,4 @@ Future Function() makeTestDHTShortArrayAdd({required int stride}) => await arr.delete(); await arr.close(); - - final endTime = DateTime.now(); - print('Duration: ${endTime.difference(startTime)}'); }; diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart index f7b606c..5dd36a0 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart @@ -42,11 +42,13 @@ class DHTLogUpdate extends Equatable { /// * The head and tail position of the log /// - subkeyIdx = pos / recordsPerSubkey /// - recordIdx = pos % recordsPerSubkey -class DHTLog implements DHTOpenable { +class DHTLog implements DHTOpenable { //////////////////////////////////////////////////////////////// // Constructors - DHTLog._({required _DHTLogSpine spine}) : _spine = spine { + DHTLog._({required _DHTLogSpine spine}) + : _spine = spine, + _openCount = 1 { _spine.onUpdatedSpine = (update) { _watchController?.sink.add(update); }; @@ -162,18 +164,29 @@ class DHTLog implements DHTOpenable { /// Check if the DHTLog is open @override - bool get isOpen => _spine.isOpen; + bool get isOpen => _openCount > 0; + + /// Add a reference to this log + @override + Future ref() async => _mutex.protect(() async { + _openCount++; + return this; + }); /// Free all resources for the DHTLog @override - Future close() async { - if (!isOpen) { - return; - } - await _watchController?.close(); - _watchController = null; - await _spine.close(); - } + Future close() async => _mutex.protect(() async { + if (_openCount == 0) { + throw StateError('already closed'); + } + _openCount--; + if (_openCount != 0) { + return; + } + await _watchController?.close(); + _watchController = null; + await _spine.close(); + }); /// Free all resources for the DHTLog and delete it from the DHT /// Will wait until the short array is closed to delete it @@ -284,6 +297,10 @@ class DHTLog implements DHTOpenable { // Internal representation refreshed from spine record final _DHTLogSpine _spine; + // Openable + int _openCount; + final _mutex = Mutex(); + // Watch mutex to ensure we keep the representation valid final Mutex _listenMutex = Mutex(); // Stream of external changes diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_append.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_append.dart index 96c3eb4..26c22da 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_append.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_append.dart @@ -17,16 +17,16 @@ class _DHTLogAppend extends _DHTLogRead implements DHTAppendTruncateRandomRead { } // Write item to the segment - return lookup.shortArray.operateWrite((write) async { - // If this a new segment, then clear it in case we have wrapped around - if (lookup.pos == 0) { - await write.clear(); - } else if (lookup.pos != write.length) { - // We should always be appending at the length - throw StateError('appending should be at the end'); - } - return write.tryAddItem(value); - }); + return lookup.shortArray.scope((sa) => sa.operateWrite((write) async { + // If this a new segment, then clear it in case we have wrapped around + if (lookup.pos == 0) { + await write.clear(); + } else if (lookup.pos != write.length) { + // We should always be appending at the length + throw StateError('appending should be at the end'); + } + return write.tryAddItem(value); + })); } @override @@ -45,16 +45,19 @@ class _DHTLogAppend extends _DHTLogRead implements DHTAppendTruncateRandomRead { } final sacount = min(remaining, DHTShortArray.maxElements - lookup.pos); - final success = await lookup.shortArray.operateWrite((write) async { - // If this a new segment, then clear it in case we have wrapped around - if (lookup.pos == 0) { - await write.clear(); - } else if (lookup.pos != write.length) { - // We should always be appending at the length - throw StateError('appending should be at the end'); - } - return write.tryAddItems(values.sublist(valueIdx, valueIdx + sacount)); - }); + final success = + await lookup.shortArray.scope((sa) => sa.operateWrite((write) async { + // If this a new segment, then clear it in + // case we have wrapped around + if (lookup.pos == 0) { + await write.clear(); + } else if (lookup.pos != write.length) { + // We should always be appending at the length + throw StateError('appending should be at the end'); + } + return write + .tryAddItems(values.sublist(valueIdx, valueIdx + sacount)); + })); if (!success) { return false; } diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart index 0919412..ea36fc2 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart @@ -19,8 +19,8 @@ class _DHTLogRead implements DHTRandomRead { return null; } - return lookup.shortArray.operate( - (read) => read.getItem(lookup.pos, forceRefresh: forceRefresh)); + return lookup.shortArray.scope((sa) => sa.operate( + (read) => read.getItem(lookup.pos, forceRefresh: forceRefresh))); } (int, int) _clampStartLen(int start, int? len) { @@ -71,22 +71,22 @@ class _DHTLogRead implements DHTRandomRead { // Check each segment for offline positions var foundOffline = false; - await lookup.shortArray.operate((read) async { - final segmentOffline = await read.getOfflinePositions(); + await lookup.shortArray.scope((sa) => sa.operate((read) async { + final segmentOffline = await read.getOfflinePositions(); - // For each shortarray segment go through their segment positions - // in reverse order and see if they are offline - for (var segmentPos = lookup.pos; - segmentPos >= 0 && pos >= 0; - segmentPos--, pos--) { - // If the position in the segment is offline, then - // mark the position in the log as offline - if (segmentOffline.contains(segmentPos)) { - positionOffline.add(pos); - foundOffline = true; - } - } - }); + // For each shortarray segment go through their segment positions + // in reverse order and see if they are offline + for (var segmentPos = lookup.pos; + segmentPos >= 0 && pos >= 0; + segmentPos--, pos--) { + // If the position in the segment is offline, then + // mark the position in the log as offline + if (segmentOffline.contains(segmentPos)) { + positionOffline.add(pos); + foundOffline = true; + } + } + })); // If we found nothing offline in this segment then we can stop if (!foundOffline) { diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index 7ce3dbf..12a80c9 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -15,6 +15,13 @@ class _DHTLogSegmentLookup extends Equatable { List get props => [subkey, segment]; } +class _SubkeyData { + _SubkeyData({required this.subkey, required this.data}); + int subkey; + Uint8List data; + bool changed = false; +} + class _DHTLogSpine { _DHTLogSpine._( {required DHTRecord spineRecord, @@ -47,7 +54,7 @@ class _DHTLogSpine { static Future<_DHTLogSpine> load({required DHTRecord spineRecord}) async { // Get an updated spine head record copy if one exists final spineHead = await spineRecord.getProtobuf(proto.DHTLog.fromBuffer, - subkey: 0, refreshMode: DHTRecordRefreshMode.refresh); + subkey: 0, refreshMode: DHTRecordRefreshMode.network); if (spineHead == null) { throw StateError('spine head missing during refresh'); } @@ -234,7 +241,7 @@ class _DHTLogSpine { segmentKeyBytes); } - Future _getOrCreateSegmentInner(int segmentNumber) async { + Future _openOrCreateSegmentInner(int segmentNumber) async { assert(_spineMutex.isLocked, 'should be in mutex here'); assert(_spineRecord.writer != null, 'should be writable'); @@ -292,7 +299,7 @@ class _DHTLogSpine { } } - Future _getSegmentInner(int segmentNumber) async { + Future _openSegmentInner(int segmentNumber) async { assert(_spineMutex.isLocked, 'should be in mutex here'); // Lookup what subkey and segment subrange has this position's segment @@ -321,7 +328,7 @@ class _DHTLogSpine { return segmentRec; } - Future getOrCreateSegment(int segmentNumber) async { + Future _openOrCreateSegment(int segmentNumber) async { assert(_spineMutex.isLocked, 'should be in mutex here'); // See if we already have this in the cache @@ -331,21 +338,22 @@ class _DHTLogSpine { final x = _spineCache.removeAt(i); _spineCache.add(x); // Return the shortarray for this position - return x.$2; + return x.$2.ref(); } } - // If we don't have it in the cache, get/create it and then cache it - final segment = await _getOrCreateSegmentInner(segmentNumber); - _spineCache.add((segmentNumber, segment)); + // If we don't have it in the cache, get/create it and then cache a ref + final segment = await _openOrCreateSegmentInner(segmentNumber); + _spineCache.add((segmentNumber, await segment.ref())); if (_spineCache.length > _spineCacheLength) { // Trim the LRU cache - _spineCache.removeAt(0); + final (_, sa) = _spineCache.removeAt(0); + await sa.close(); } return segment; } - Future getSegment(int segmentNumber) async { + Future _openSegment(int segmentNumber) async { assert(_spineMutex.isLocked, 'should be in mutex here'); // See if we already have this in the cache @@ -355,19 +363,20 @@ class _DHTLogSpine { final x = _spineCache.removeAt(i); _spineCache.add(x); // Return the shortarray for this position - return x.$2; + return x.$2.ref(); } } // If we don't have it in the cache, get it and then cache it - final segment = await _getSegmentInner(segmentNumber); + final segment = await _openSegmentInner(segmentNumber); if (segment == null) { return null; } - _spineCache.add((segmentNumber, segment)); + _spineCache.add((segmentNumber, await segment.ref())); if (_spineCache.length > _spineCacheLength) { // Trim the LRU cache - _spineCache.removeAt(0); + final (_, sa) = _spineCache.removeAt(0); + await sa.close(); } return segment; } @@ -409,8 +418,8 @@ class _DHTLogSpine { // Get the segment shortArray final shortArray = (_spineRecord.writer == null) - ? await getSegment(segmentNumber) - : await getOrCreateSegment(segmentNumber); + ? await _openSegment(segmentNumber) + : await _openOrCreateSegment(segmentNumber); if (shortArray == null) { return null; } @@ -442,12 +451,16 @@ class _DHTLogSpine { throw StateError('ring buffer underflow'); } + final oldHead = _head; _head = (_head + count) % _positionLimit; - await _purgeUnusedSegments(); + final newHead = _head; + await _purgeSegments(oldHead, newHead); } Future _deleteSegmentsContiguous(int start, int end) async { assert(_spineMutex.isLocked, 'should be in mutex here'); + DHTRecordPool.instance + .log('_deleteSegmentsContiguous: start=$start, end=$end'); final startSegmentNumber = start ~/ DHTShortArray.maxElements; final startSegmentPos = start % DHTShortArray.maxElements; @@ -460,8 +473,7 @@ class _DHTLogSpine { final lastDeleteSegment = (endSegmentPos == 0) ? endSegmentNumber - 1 : endSegmentNumber - 2; - int? lastSubkey; - Uint8List? subkeyData; + _SubkeyData? lastSubkeyData; for (var segmentNumber = firstDeleteSegment; segmentNumber <= lastDeleteSegment; segmentNumber++) { @@ -471,44 +483,48 @@ class _DHTLogSpine { final subkey = l.subkey; final segment = l.segment; - if (lastSubkey != subkey) { + if (subkey != lastSubkeyData?.subkey) { // Flush subkey writes - if (lastSubkey != null) { - await _spineRecord.eventualWriteBytes(subkeyData!, - subkey: lastSubkey); + if (lastSubkeyData != null && lastSubkeyData.changed) { + await _spineRecord.eventualWriteBytes(lastSubkeyData.data, + subkey: lastSubkeyData.subkey); } - xxx debug this, it takes forever - - // Get next subkey - subkeyData = await _spineRecord.get(subkey: subkey); - if (subkeyData != null) { - lastSubkey = subkey; + // Get next subkey if available locally + final data = await _spineRecord.get( + subkey: subkey, refreshMode: DHTRecordRefreshMode.local); + if (data != null) { + lastSubkeyData = _SubkeyData(subkey: subkey, data: data); } else { - lastSubkey = null; + lastSubkeyData = null; + // If the subkey was not available locally we can go to the + // last segment number at the end of this subkey + segmentNumber = ((subkey + 1) * DHTLog.segmentsPerSubkey) - 1; } } - if (subkeyData != null) { - final segmentKey = _getSegmentKey(subkeyData, segment); + if (lastSubkeyData != null) { + final segmentKey = _getSegmentKey(lastSubkeyData.data, segment); if (segmentKey != null) { await DHTRecordPool.instance.deleteRecord(segmentKey); - _setSegmentKey(subkeyData, segment, null); + _setSegmentKey(lastSubkeyData.data, segment, null); + lastSubkeyData.changed = true; } } } // Flush subkey writes - if (lastSubkey != null) { - await _spineRecord.eventualWriteBytes(subkeyData!, subkey: lastSubkey); + if (lastSubkeyData != null) { + await _spineRecord.eventualWriteBytes(lastSubkeyData.data, + subkey: lastSubkeyData.subkey); } } - Future _purgeUnusedSegments() async { + Future _purgeSegments(int from, int to) async { assert(_spineMutex.isLocked, 'should be in mutex here'); - if (_head < _tail) { - await _deleteSegmentsContiguous(0, _head); - await _deleteSegmentsContiguous(_tail, _positionLimit); - } else if (_head > _tail) { - await _deleteSegmentsContiguous(_tail, _head); + if (from < to) { + await _deleteSegmentsContiguous(from, to); + } else if (from > to) { + await _deleteSegmentsContiguous(from, _positionLimit); + await _deleteSegmentsContiguous(0, to); } } diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart index 0b4e0b6..a333160 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart @@ -39,7 +39,7 @@ class DefaultDHTRecordCubit extends DHTRecordCubit { final firstSubkey = subkeys.firstOrNull!.low; if (firstSubkey != defaultSubkey || updatedata == null) { final maybeData = - await record.get(refreshMode: DHTRecordRefreshMode.refresh); + await record.get(refreshMode: DHTRecordRefreshMode.network); if (maybeData == null) { return null; } 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 3d625f8..7bf5129 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 @@ -16,19 +16,27 @@ class DHTRecordWatchChange extends Equatable { /// Refresh mode for DHT record 'get' enum DHTRecordRefreshMode { /// Return existing subkey values if they exist locally already - existing, + /// And then check the network for a newer value + /// This is the default refresh mode + cached, + + /// Return existing subkey values only if they exist locally already + local, /// Always check the network for a newer subkey value - refresh, + network, /// Always check the network for a newer subkey value but only /// return that value if its sequence number is newer than the local value - refreshOnlyUpdates, + update; + + bool get _forceRefresh => this == network || this == update; + bool get _inspectLocal => this == local || this == update; } ///////////////////////////////////////////////// -class DHTRecord implements DHTOpenable { +class DHTRecord implements DHTOpenable { DHTRecord._( {required VeilidRoutingContext routingContext, required SharedDHTRecordData sharedDHTRecordData, @@ -40,7 +48,7 @@ class DHTRecord implements DHTOpenable { _routingContext = routingContext, _defaultSubkey = defaultSubkey, _writer = writer, - _open = true, + _openCount = 1, _sharedDHTRecordData = sharedDHTRecordData; //////////////////////////////////////////////////////////////////////////// @@ -48,25 +56,37 @@ class DHTRecord implements DHTOpenable { /// Check if the DHTRecord is open @override - bool get isOpen => _open; + bool get isOpen => _openCount > 0; + + /// Add a reference to this DHTRecord + @override + Future ref() async => _mutex.protect(() async { + _openCount++; + return this; + }); /// Free all resources for the DHTRecord @override - Future close() async { - if (!_open) { - return; - } - await watchController?.close(); - await DHTRecordPool.instance._recordClosed(this); - _open = false; - } + Future close() async => _mutex.protect(() async { + if (_openCount == 0) { + throw StateError('already closed'); + } + _openCount--; + if (_openCount != 0) { + return; + } + + await _watchController?.close(); + _watchController = null; + await DHTRecordPool.instance._recordClosed(this); + }); /// Free all resources for the DHTRecord and delete it from the DHT /// Will wait until the record is closed to delete it @override - Future delete() async { - await DHTRecordPool.instance.deleteRecord(key); - } + Future delete() async => _mutex.protect(() async { + await DHTRecordPool.instance.deleteRecord(key); + }); //////////////////////////////////////////////////////////////////////////// // Public API @@ -95,25 +115,37 @@ class DHTRecord implements DHTOpenable { Future get( {int subkey = -1, DHTRecordCrypto? crypto, - DHTRecordRefreshMode refreshMode = DHTRecordRefreshMode.existing, + DHTRecordRefreshMode refreshMode = DHTRecordRefreshMode.cached, Output? outSeqNum}) async { subkey = subkeyOrDefault(subkey); + + // Get the last sequence number if we need it + final lastSeq = + refreshMode._inspectLocal ? await _localSubkeySeq(subkey) : null; + + // See if we only ever want the locally stored value + if (refreshMode == DHTRecordRefreshMode.local && lastSeq == null) { + // If it's not available locally already just return null now + return null; + } + final valueData = await _routingContext.getDHTValue(key, subkey, - forceRefresh: refreshMode != DHTRecordRefreshMode.existing); + forceRefresh: refreshMode._forceRefresh); if (valueData == null) { return null; } - final lastSeq = _sharedDHTRecordData.subkeySeqCache[subkey]; - if (refreshMode == DHTRecordRefreshMode.refreshOnlyUpdates && + // See if this get resulted in a newer sequence number + if (refreshMode == DHTRecordRefreshMode.update && lastSeq != null && valueData.seq <= lastSeq) { + // If we're only returning updates then punt now return null; } + // If we're returning a value, decrypt it final out = (crypto ?? _crypto).decrypt(valueData.data, subkey); if (outSeqNum != null) { outSeqNum.save(valueData.seq); } - _sharedDHTRecordData.subkeySeqCache[subkey] = valueData.seq; return out; } @@ -128,7 +160,7 @@ class DHTRecord implements DHTOpenable { Future getJson(T Function(dynamic) fromJson, {int subkey = -1, DHTRecordCrypto? crypto, - DHTRecordRefreshMode refreshMode = DHTRecordRefreshMode.existing, + DHTRecordRefreshMode refreshMode = DHTRecordRefreshMode.cached, Output? outSeqNum}) async { final data = await get( subkey: subkey, @@ -154,7 +186,7 @@ class DHTRecord implements DHTOpenable { T Function(List i) fromBuffer, {int subkey = -1, DHTRecordCrypto? crypto, - DHTRecordRefreshMode refreshMode = DHTRecordRefreshMode.existing, + DHTRecordRefreshMode refreshMode = DHTRecordRefreshMode.cached, Output? outSeqNum}) async { final data = await get( subkey: subkey, @@ -176,7 +208,7 @@ class DHTRecord implements DHTOpenable { KeyPair? writer, Output? outSeqNum}) async { subkey = subkeyOrDefault(subkey); - final lastSeq = _sharedDHTRecordData.subkeySeqCache[subkey]; + final lastSeq = await _localSubkeySeq(subkey); final encryptedNewValue = await (crypto ?? _crypto).encrypt(newValue, subkey); @@ -198,7 +230,6 @@ class DHTRecord implements DHTOpenable { if (isUpdated && outSeqNum != null) { outSeqNum.save(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 @@ -228,7 +259,7 @@ class DHTRecord implements DHTOpenable { KeyPair? writer, Output? outSeqNum}) async { subkey = subkeyOrDefault(subkey); - final lastSeq = _sharedDHTRecordData.subkeySeqCache[subkey]; + final lastSeq = await _localSubkeySeq(subkey); final encryptedNewValue = await (crypto ?? _crypto).encrypt(newValue, subkey); @@ -254,7 +285,6 @@ class DHTRecord implements DHTOpenable { if (outSeqNum != null) { outSeqNum.save(newValueData.seq); } - _sharedDHTRecordData.subkeySeqCache[subkey] = newValueData.seq; // The encrypted data returned should be exactly the same // as what we are trying to set, @@ -402,13 +432,13 @@ class DHTRecord implements DHTOpenable { DHTRecordCrypto? crypto, }) async { // Set up watch requirements - watchController ??= + _watchController ??= StreamController.broadcast(onCancel: () { // If there are no more listeners then we can get rid of the controller - watchController = null; + _watchController = null; }); - return watchController!.stream.listen( + return _watchController!.stream.listen( (change) { if (change.local && !localChanges) { return; @@ -431,8 +461,8 @@ class DHTRecord implements DHTOpenable { }, cancelOnError: true, onError: (e) async { - await watchController!.close(); - watchController = null; + await _watchController!.close(); + _watchController = null; }); } @@ -455,6 +485,14 @@ class DHTRecord implements DHTOpenable { ////////////////////////////////////////////////////////////////////////// + Future _localSubkeySeq(int subkey) async { + final rr = await _routingContext.inspectDHTRecord( + key, + subkeys: [ValueSubkeyRange.single(subkey)], + ); + return rr.localSeqs.firstOrNull ?? 0xFFFFFFFF; + } + void _addValueChange( {required bool local, required Uint8List? data, @@ -464,7 +502,7 @@ class DHTRecord implements DHTOpenable { final watchedSubkeys = ws.subkeys; if (watchedSubkeys == null) { // Report all subkeys - watchController?.add( + _watchController?.add( DHTRecordWatchChange(local: local, data: data, subkeys: subkeys)); } else { // Only some subkeys are being watched, see if the reported update @@ -479,7 +517,7 @@ class DHTRecord implements DHTOpenable { overlappedFirstSubkey == updateFirstSubkey ? data : null; // Report only watched subkeys - watchController?.add(DHTRecordWatchChange( + _watchController?.add(DHTRecordWatchChange( local: local, data: updatedData, subkeys: overlappedSubkeys)); } } @@ -504,10 +542,9 @@ class DHTRecord implements DHTOpenable { final KeyPair? _writer; final DHTRecordCrypto _crypto; final String debugName; - - bool _open; - @internal - StreamController? watchController; + final _mutex = Mutex(); + int _openCount; + StreamController? _watchController; @internal WatchState? watchState; } 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 8616658..1cfcfcd 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 @@ -93,7 +93,7 @@ class DHTRecordCubit extends Cubit> { for (final skr in subkeys) { for (var sk = skr.low; sk <= skr.high; sk++) { final data = await _record.get( - subkey: sk, refreshMode: DHTRecordRefreshMode.refreshOnlyUpdates); + subkey: sk, refreshMode: DHTRecordRefreshMode.update); if (data != null) { final newState = await _stateFunction(_record, updateSubkeys, data); if (newState != null) { 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 a4748df..a8e86a1 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 @@ -88,7 +88,6 @@ class SharedDHTRecordData { DHTRecordDescriptor recordDescriptor; KeyPair? defaultWriter; VeilidRoutingContext defaultRoutingContext; - Map subkeySeqCache = {}; bool needsWatchStateUpdate = false; WatchState? unionWatchState; } 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 082a391..cd62fa6 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 @@ -13,12 +13,13 @@ part 'dht_short_array_write.dart'; /////////////////////////////////////////////////////////////////////// -class DHTShortArray implements DHTOpenable { +class DHTShortArray implements DHTOpenable { //////////////////////////////////////////////////////////////// // Constructors DHTShortArray._({required DHTRecord headRecord}) - : _head = _DHTShortArrayHead(headRecord: headRecord) { + : _head = _DHTShortArrayHead(headRecord: headRecord), + _openCount = 1 { _head.onUpdatedHead = () { _watchController?.sink.add(null); }; @@ -139,18 +140,30 @@ class DHTShortArray implements DHTOpenable { /// Check if the shortarray is open @override - bool get isOpen => _head.isOpen; + bool get isOpen => _openCount > 0; + + /// Add a reference to this shortarray + @override + Future ref() async => _mutex.protect(() async { + _openCount++; + return this; + }); /// Free all resources for the DHTShortArray @override - Future close() async { - if (!isOpen) { - return; - } - await _watchController?.close(); - _watchController = null; - await _head.close(); - } + Future close() async => _mutex.protect(() async { + if (_openCount == 0) { + throw StateError('already closed'); + } + _openCount--; + if (_openCount != 0) { + return; + } + + await _watchController?.close(); + _watchController = null; + await _head.close(); + }); /// Free all resources for the DHTShortArray and delete it from the DHT /// Will wait until the short array is closed to delete it @@ -255,6 +268,10 @@ class DHTShortArray implements DHTOpenable { // Internal representation refreshed from head record final _DHTShortArrayHead _head; + // Openable + int _openCount; + final _mutex = Mutex(); + // Watch mutex to ensure we keep the representation valid final Mutex _listenMutex = Mutex(); // Stream of external changes 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 e2bf392..1403e87 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 @@ -248,7 +248,7 @@ class _DHTShortArrayHead { Future _loadHead() async { // Get an updated head record copy if one exists final head = await _headRecord.getProtobuf(proto.DHTShortArray.fromBuffer, - subkey: 0, refreshMode: DHTRecordRefreshMode.refresh); + subkey: 0, refreshMode: DHTRecordRefreshMode.network); if (head == null) { throw StateError('shortarray head missing during refresh'); } 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 index 88cefde..919564c 100644 --- 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 @@ -22,8 +22,8 @@ class _DHTShortArrayRead implements DHTRandomRead { final out = lookup.record.get( subkey: lookup.recordSubkey, refreshMode: refresh - ? DHTRecordRefreshMode.refresh - : DHTRecordRefreshMode.existing, + ? DHTRecordRefreshMode.network + : DHTRecordRefreshMode.cached, outSeqNum: outSeqNum); if (outSeqNum.value != null) { _head.updatePositionSeq(pos, false, outSeqNum.value!); diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_openable.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_openable.dart index e28f703..ffd58f9 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_openable.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_openable.dart @@ -1,12 +1,13 @@ import 'dart:async'; -abstract class DHTOpenable { +abstract class DHTOpenable { bool get isOpen; + Future ref(); Future close(); Future delete(); } -extension DHTOpenableExt on D { +extension DHTOpenableExt> on D { /// Runs a closure that guarantees the DHTOpenable /// will be closed upon exit, even if an uncaught exception is thrown Future scope(Future Function(D) scopeFunction) async { diff --git a/packages/veilid_support/lib/src/identity.dart b/packages/veilid_support/lib/src/identity.dart index 2645894..5721461 100644 --- a/packages/veilid_support/lib/src/identity.dart +++ b/packages/veilid_support/lib/src/identity.dart @@ -301,7 +301,7 @@ Future openIdentityMaster( 'IdentityMaster::openIdentityMaster::IdentityMasterRecord')) .deleteScope((masterRec) async { final identityMaster = (await masterRec.getJson(IdentityMaster.fromJson, - refreshMode: DHTRecordRefreshMode.refresh))!; + refreshMode: DHTRecordRefreshMode.network))!; // Validate IdentityMaster final masterRecordKey = masterRec.key; From ed893852a2308e0e8f5fe3f4d2ed30803e476655 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 20 May 2024 20:48:17 -0400 Subject: [PATCH 100/270] flutter updates --- .../chat_single_contact_list_widget.dart | 2 +- .../views/scan_invitation_dialog.dart | 117 +++---- lib/contacts/views/contact_list_widget.dart | 2 +- .../example/integration_test/app_test.dart | 18 +- packages/veilid_support/example/pubspec.lock | 290 +++++++++++++++++- packages/veilid_support/example/pubspec.yaml | 1 + .../src/dht_log/dht_log_append.dart | 44 ++- .../dht_short_array/dht_short_array_head.dart | 31 +- .../dht_short_array/dht_short_array_read.dart | 2 +- .../dht_short_array_write.dart | 94 ++++-- packages/veilid_support/pubspec.lock | 22 +- packages/veilid_support/pubspec.yaml | 6 +- pubspec.lock | 90 +++--- pubspec.yaml | 9 +- 14 files changed, 528 insertions(+), 200 deletions(-) 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 a671011..785dbcb 100644 --- a/lib/chat_list/views/chat_single_contact_list_widget.dart +++ b/lib/chat_list/views/chat_single_contact_list_widget.dart @@ -34,7 +34,7 @@ class ChatSingleContactListWidget extends StatelessWidget { ? const EmptyChatListWidget() : SearchableList( initialList: chatList.map((x) => x.value).toList(), - builder: (l, i, c) { + itemBuilder: (c) { final contact = contactMap[c.remoteConversationRecordKey]; if (contact == null) { diff --git a/lib/contact_invitation/views/scan_invitation_dialog.dart b/lib/contact_invitation/views/scan_invitation_dialog.dart index 44bb32e..ab47df0 100644 --- a/lib/contact_invitation/views/scan_invitation_dialog.dart +++ b/lib/contact_invitation/views/scan_invitation_dialog.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:io'; import 'dart:typed_data'; import 'package:awesome_extensions/awesome_extensions.dart'; @@ -16,65 +15,64 @@ import '../../theme/theme.dart'; import '../../tools/tools.dart'; import 'invitation_dialog.dart'; -class BarcodeOverlay extends CustomPainter { - BarcodeOverlay({ - required this.barcode, - required this.arguments, - required this.boxFit, - required this.capture, - }); +// class BarcodeOverlay extends CustomPainter { +// BarcodeOverlay({ +// required this.barcode, +// required this.boxFit, +// required this.capture, +// required this.size, +// }); - final BarcodeCapture capture; - final Barcode barcode; - final MobileScannerArguments arguments; - final BoxFit boxFit; +// final BarcodeCapture capture; +// final Barcode barcode; +// final BoxFit boxFit; +// final Size size; - @override - void paint(Canvas canvas, Size size) { - final adjustedSize = applyBoxFit(boxFit, arguments.size, size); +// @override +// void paint(Canvas canvas, Size size) { +// final adjustedSize = applyBoxFit(boxFit, 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; - } +// 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; - } +// 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 ratioWidth = (Platform.isIOS ? capture.size.width : size.width) / +// adjustedSize.destination.width; +// final ratioHeight = (Platform.isIOS ? capture.size.height : 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 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; +// final backgroundPaint = Paint() +// ..color = Colors.red.withOpacity(0.3) +// ..style = PaintingStyle.fill +// ..blendMode = BlendMode.dstOut; - canvas.drawPath(cutoutPath, backgroundPaint); - } +// canvas.drawPath(cutoutPath, backgroundPaint); +// } - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; -} +// @override +// bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +// } class ScannerOverlay extends CustomPainter { ScannerOverlay(this.scanWindow); @@ -202,9 +200,9 @@ class ScanInvitationDialogState extends State { IconButton( color: Colors.white, icon: ValueListenableBuilder( - valueListenable: cameraController.torchState, + valueListenable: cameraController, builder: (context, state, child) { - switch (state) { + switch (state.torchState) { case TorchState.off: return Icon(Icons.flash_off, color: @@ -212,6 +210,12 @@ class ScanInvitationDialogState extends State { case TorchState.on: return Icon(Icons.flash_on, color: scale.primaryScale.primary); + case TorchState.auto: + return Icon(Icons.flash_auto, + color: scale.primaryScale.primary); + case TorchState.unavailable: + return Icon(Icons.no_flash, + color: scale.primaryScale.primary); } }, ), @@ -236,10 +240,9 @@ class ScanInvitationDialogState extends State { IconButton( color: Colors.white, icon: ValueListenableBuilder( - valueListenable: - cameraController.cameraFacingState, + valueListenable: cameraController, builder: (context, state, child) { - switch (state) { + switch (state.cameraDirection) { case CameraFacing.front: return const Icon(Icons.camera_front); case CameraFacing.back: @@ -265,7 +268,7 @@ class ScanInvitationDialogState extends State { SchedulerBinding.instance .addPostFrameCallback((_) { cameraController.dispose(); - Navigator.pop(context, null); + Navigator.pop(context); }) })), ], diff --git a/lib/contacts/views/contact_list_widget.dart b/lib/contacts/views/contact_list_widget.dart index 4c83a92..6ef3ca0 100644 --- a/lib/contacts/views/contact_list_widget.dart +++ b/lib/contacts/views/contact_list_widget.dart @@ -38,7 +38,7 @@ class ContactListWidget extends StatelessWidget { ? const EmptyContactListWidget() : SearchableList( initialList: contactList.toList(), - builder: (l, i, c) => + itemBuilder: (c) => ContactItemWidget(contact: c, disabled: disabled) .paddingLTRB(0, 4, 0, 0), filter: (value) { diff --git a/packages/veilid_support/example/integration_test/app_test.dart b/packages/veilid_support/example/integration_test/app_test.dart index b3548a2..9c85998 100644 --- a/packages/veilid_support/example/integration_test/app_test.dart +++ b/packages/veilid_support/example/integration_test/app_test.dart @@ -1,8 +1,9 @@ -@Timeout(Duration(seconds: 240)) +//@Timeout(Duration(seconds: 240)) -library veilid_support_integration_test; +//library veilid_support_integration_test; -import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/foundation.dart'; +import 'package:test/test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:veilid_test/veilid_test.dart'; @@ -28,6 +29,10 @@ void main() { group('Started Tests', () { setUpAll(veilidFixture.setUp); tearDownAll(veilidFixture.tearDown); + tearDownAll(() { + final endTime = DateTime.now(); + debugPrintSynchronously('Duration: ${endTime.difference(startTime)}'); + }); group('Attached Tests', () { setUpAll(veilidFixture.attach); @@ -58,7 +63,7 @@ void main() { test('create shortarray stride=$stride', makeTestDHTShortArrayCreateDelete(stride: stride)); test('add shortarray stride=$stride', - makeTestDHTShortArrayAdd(stride: 256)); + makeTestDHTShortArrayAdd(stride: stride)); } }); @@ -70,14 +75,11 @@ void main() { test('create log stride=$stride', makeTestDHTLogCreateDelete(stride: stride)); test('add/truncate log stride=$stride', - makeTestDHTLogAddTruncate(stride: 256), + makeTestDHTLogAddTruncate(stride: stride), timeout: const Timeout(Duration(seconds: 480))); } }); }); }); }); - - final endTime = DateTime.now(); - print('Duration: ${endTime.difference(startTime)}'); } diff --git a/packages/veilid_support/example/pubspec.lock b/packages/veilid_support/example/pubspec.lock index a3fee79..3defe80 100644 --- a/packages/veilid_support/example/pubspec.lock +++ b/packages/veilid_support/example/pubspec.lock @@ -1,6 +1,30 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + url: "https://pub.dev" + source: hosted + version: "67.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + args: + dependency: transitive + description: + name: args + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + url: "https://pub.dev" + source: hosted + version: "2.5.0" async: dependency: transitive description: @@ -81,6 +105,30 @@ packages: 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: "3945034e86ea203af7a056d98e98e42a5518fff200d6e8e6647e1886b07e936e" + url: "https://pub.dev" + source: hosted + version: "1.8.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" cupertino_icons: dependency: "direct main" description: @@ -109,10 +157,10 @@ packages: dependency: transitive description: name: fast_immutable_collections - sha256: "38fbc50df5b219dcfb83ebbc3275ec09872530ca1153858fc56fceadb310d037" + sha256: "533806a7f0c624c2e479d05d3fdce4c87109a7cd0db39b8cc3830d3a2e8dedc7" url: "https://pub.dev" source: hosted - version: "10.2.2" + version: "10.2.3" ffi: dependency: transitive description: @@ -165,11 +213,27 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" fuchsia_remote_debug_protocol: dependency: transitive description: flutter source: sdk version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" globbing: dependency: transitive description: @@ -178,11 +242,43 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + 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" integration_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + 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: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" json_annotation: dependency: transitive description: @@ -195,26 +291,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lint_hard: dependency: "direct dev" description: @@ -223,6 +319,14 @@ packages: 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: transitive description: @@ -251,10 +355,34 @@ packages: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" + mime: + dependency: transitive + description: + name: mime + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + 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: @@ -327,6 +455,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" process: dependency: transitive description: @@ -343,11 +479,67 @@ packages: 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" + 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_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: @@ -412,14 +604,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" + url: "https://pub.dev" + source: hosted + version: "1.25.2" test_api: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" + test_core: + dependency: transitive + description: + name: test_core + sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + 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: @@ -453,10 +669,34 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.1" + 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: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + url: "https://pub.dev" + source: hosted + version: "2.4.5" webdriver: dependency: transitive description: @@ -465,14 +705,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + 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: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" + sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 url: "https://pub.dev" source: hosted - version: "5.5.0" + version: "5.5.1" xdg_directories: dependency: transitive description: @@ -481,6 +729,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" sdks: - dart: ">=3.3.4 <4.0.0" + dart: ">=3.4.0 <4.0.0" flutter: ">=3.19.1" diff --git a/packages/veilid_support/example/pubspec.yaml b/packages/veilid_support/example/pubspec.yaml index 1c73078..60f06e7 100644 --- a/packages/veilid_support/example/pubspec.yaml +++ b/packages/veilid_support/example/pubspec.yaml @@ -20,6 +20,7 @@ dev_dependencies: integration_test: sdk: flutter lint_hard: ^4.0.0 + test: ^1.25.2 veilid_test: path: ../../../../veilid/veilid-flutter/packages/veilid_test diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_append.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_append.dart index 26c22da..877df89 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_append.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_append.dart @@ -36,6 +36,9 @@ class _DHTLogAppend extends _DHTLogRead implements DHTAppendTruncateRandomRead { _spine.allocateTail(values.length); // Look up the first position and shortarray + final dws = DelayedWaitSet(); + + var success = true; for (var valueIdx = 0; valueIdx < values.length;) { final remaining = values.length - valueIdx; @@ -45,25 +48,32 @@ class _DHTLogAppend extends _DHTLogRead implements DHTAppendTruncateRandomRead { } final sacount = min(remaining, DHTShortArray.maxElements - lookup.pos); - final success = - await lookup.shortArray.scope((sa) => sa.operateWrite((write) async { - // If this a new segment, then clear it in - // case we have wrapped around - if (lookup.pos == 0) { - await write.clear(); - } else if (lookup.pos != write.length) { - // We should always be appending at the length - throw StateError('appending should be at the end'); - } - return write - .tryAddItems(values.sublist(valueIdx, valueIdx + sacount)); - })); - if (!success) { - return false; - } + final sublistValues = values.sublist(valueIdx, valueIdx + sacount); + + dws.add(() async { + final ok = await lookup.shortArray + .scope((sa) => sa.operateWrite((write) async { + // If this a new segment, then clear it in + // case we have wrapped around + if (lookup.pos == 0) { + await write.clear(); + } else if (lookup.pos != write.length) { + // We should always be appending at the length + throw StateError('appending should be at the end'); + } + return write.tryAddItems(sublistValues); + })); + if (!ok) { + success = false; + } + }); + valueIdx += sacount; } - return true; + + await dws(chunkSize: maxDHTConcurrency); + + return success; } @override 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 1403e87..501892d 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 @@ -215,7 +215,7 @@ class _DHTShortArrayHead { } } on Exception catch (_) { // On any exception close the records we have opened - await Future.wait(newRecords.entries.map((e) => e.value.close())); + await newRecords.entries.map((e) => e.value.close()).wait; rethrow; } @@ -259,13 +259,22 @@ class _DHTShortArrayHead { ///////////////////////////////////////////////////////////////////////////// // Linked record management - Future _getOrCreateLinkedRecord(int recordNumber) async { + Future _getOrCreateLinkedRecord( + int recordNumber, bool allowCreate) async { if (recordNumber == 0) { return _headRecord; } - final pool = DHTRecordPool.instance; recordNumber--; - while (recordNumber >= _linkedRecords.length) { + if (recordNumber < _linkedRecords.length) { + return _linkedRecords[recordNumber]; + } + + if (!allowCreate) { + throw StateError("asked for non-existent record and can't create"); + } + + final pool = DHTRecordPool.instance; + for (var rn = _linkedRecords.length; rn <= recordNumber; rn++) { // Linked records must use SMPL schema so writer can be specified // Use the same writer as the head record final smplWriter = _headRecord.writer!; @@ -287,9 +296,6 @@ class _DHTShortArrayHead { // Add to linked records _linkedRecords.add(dhtRecord); } - if (!await _writeHead()) { - throw StateError('failed to add linked record'); - } return _linkedRecords[recordNumber]; } @@ -313,15 +319,16 @@ class _DHTShortArrayHead { ); } - Future lookupPosition(int pos) async { + Future lookupPosition( + int pos, bool allowCreate) async { final idx = _index[pos]; - return lookupIndex(idx); + return lookupIndex(idx, allowCreate); } - Future lookupIndex(int idx) async { + Future lookupIndex(int idx, bool allowCreate) async { final seq = idx < _seqs.length ? _seqs[idx] : 0xFFFFFFFF; final recordNumber = idx ~/ _stride; - final record = await _getOrCreateLinkedRecord(recordNumber); + final record = await _getOrCreateLinkedRecord(recordNumber, allowCreate); final recordSubkey = (idx % _stride) + ((recordNumber == 0) ? 1 : 0); return DHTShortArrayHeadLookup( record: record, recordSubkey: recordSubkey, seq: seq); @@ -378,7 +385,7 @@ class _DHTShortArrayHead { assert( newKeys.length <= (DHTShortArray.maxElements + (_stride - 1)) ~/ _stride, - 'too many keys'); + 'too many keys: $newKeys.length'); assert(newKeys.length == linkedKeys.length, 'duplicated linked keys'); final newIndex = index.toSet(); assert(newIndex.length <= DHTShortArray.maxElements, 'too many indexes'); 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 index 919564c..6485c02 100644 --- 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 @@ -15,7 +15,7 @@ class _DHTShortArrayRead implements DHTRandomRead { throw IndexError.withLength(pos, length); } - final lookup = await _head.lookupPosition(pos); + final lookup = await _head.lookupPosition(pos, false); final refresh = forceRefresh || _head.positionNeedsRefresh(pos); final outSeqNum = Output(); 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 index dbd8984..df93b59 100644 --- 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 @@ -17,37 +17,77 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead @override Future tryInsertItem(int pos, Uint8List value) async { + if (pos < 0 || pos > _head.length) { + throw IndexError.withLength(pos, _head.length); + } + // Allocate empty index at position _head.allocateIndex(pos); - - // Write item - final ok = await tryWriteItem(pos, value); - if (!ok) { - _head.freeIndex(pos); + var success = false; + try { + // Write item + success = await tryWriteItem(pos, value); + } finally { + if (!success) { + _head.freeIndex(pos); + } } return true; } @override Future tryInsertItems(int pos, List values) async { - // Allocate empty indices at the end of the list + if (pos < 0 || pos > _head.length) { + throw IndexError.withLength(pos, _head.length); + } + + // Allocate empty indices for (var i = 0; i < values.length; i++) { _head.allocateIndex(pos + i); } - // Write items var success = true; - final dws = DelayedWaitSet(); - for (var i = 0; i < values.length; i++) { - dws.add(() async { - final ok = await tryWriteItem(pos + i, values[i]); - if (!ok) { - _head.freeIndex(pos + i); - success = false; + final outSeqNums = List.generate(values.length, (_) => Output()); + final lookups = []; + try { + // do all lookups + for (var i = 0; i < values.length; i++) { + final lookup = await _head.lookupPosition(pos + i, true); + lookups.add(lookup); + } + + // Write items in parallel + final dws = DelayedWaitSet(); + for (var i = 0; i < values.length; i++) { + final lookup = lookups[i]; + final value = values[i]; + final outSeqNum = outSeqNums[i]; + dws.add(() async { + final outValue = await lookup.record.tryWriteBytes(value, + subkey: lookup.recordSubkey, outSeqNum: outSeqNum); + if (outValue != null) { + success = false; + } + }); + } + + await dws(chunkSize: maxDHTConcurrency, onChunkDone: (_) => success); + } finally { + // Update sequence numbers + for (var i = 0; i < values.length; i++) { + if (outSeqNums[i].value != null) { + _head.updatePositionSeq(pos + i, true, outSeqNums[i].value!); } - }); + } + + // Free indices if this was a failure + if (!success) { + for (var i = 0; i < values.length; i++) { + _head.freeIndex(pos); + } + } } - await dws(chunkSize: maxDHTConcurrency, onChunkDone: (_) => success); + return success; } @@ -68,7 +108,7 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead if (pos < 0 || pos >= _head.length) { throw IndexError.withLength(pos, _head.length); } - final lookup = await _head.lookupPosition(pos); + final lookup = await _head.lookupPosition(pos, true); final outSeqNum = Output(); @@ -98,24 +138,22 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead if (pos < 0 || pos >= _head.length) { throw IndexError.withLength(pos, _head.length); } - final lookup = await _head.lookupPosition(pos); - - final outSeqNum = Output(); + final lookup = await _head.lookupPosition(pos, true); + final outSeqNumRead = Output(); final oldValue = lookup.seq == 0xFFFFFFFF ? null : await lookup.record - .get(subkey: lookup.recordSubkey, outSeqNum: outSeqNum); - - if (outSeqNum.value != null) { - _head.updatePositionSeq(pos, false, outSeqNum.value!); + .get(subkey: lookup.recordSubkey, outSeqNum: outSeqNumRead); + if (outSeqNumRead.value != null) { + _head.updatePositionSeq(pos, false, outSeqNumRead.value!); } + final outSeqNumWrite = Output(); final result = await lookup.record.tryWriteBytes(newValue, - subkey: lookup.recordSubkey, outSeqNum: outSeqNum); - - if (outSeqNum.value != null) { - _head.updatePositionSeq(pos, true, outSeqNum.value!); + subkey: lookup.recordSubkey, outSeqNum: outSeqNumWrite); + if (outSeqNumWrite.value != null) { + _head.updatePositionSeq(pos, true, outSeqNumWrite.value!); } if (result != null) { diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index cf16648..d58ee4d 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -85,10 +85,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.2" build_resolvers: dependency: transitive description: @@ -101,10 +101,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" + sha256: "1414d6d733a85d8ad2f1dfcb3ea7945759e35a123cb99ccfac75d0758f75edfa" url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.4.10" build_runner_core: dependency: transitive description: @@ -221,10 +221,10 @@ packages: dependency: "direct main" description: name: fast_immutable_collections - sha256: "38fbc50df5b219dcfb83ebbc3275ec09872530ca1153858fc56fceadb310d037" + sha256: "533806a7f0c624c2e479d05d3fdce4c87109a7cd0db39b8cc3830d3a2e8dedc7" url: "https://pub.dev" source: hosted - version: "10.2.2" + version: "10.2.3" ffi: dependency: transitive description: @@ -399,10 +399,10 @@ packages: dependency: "direct main" description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" mime: dependency: transitive description: @@ -763,10 +763,10 @@ packages: dependency: transitive description: name: win32 - sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" + sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 url: "https://pub.dev" source: hosted - version: "5.5.0" + version: "5.5.1" xdg_directories: dependency: transitive description: @@ -784,5 +784,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.3.0 <4.0.0" + dart: ">=3.4.0 <4.0.0" flutter: ">=3.19.1" diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index a7baeed..06403ca 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -12,11 +12,11 @@ dependencies: bloc_advanced_tools: ^0.1.1 collection: ^1.18.0 equatable: ^2.0.5 - fast_immutable_collections: ^10.2.2 + fast_immutable_collections: ^10.2.3 freezed_annotation: ^2.4.1 json_annotation: ^4.9.0 loggy: ^2.0.3 - meta: ^1.11.0 + meta: ^1.12.0 protobuf: ^3.1.0 veilid: @@ -24,7 +24,7 @@ dependencies: path: ../../../veilid/veilid-flutter dev_dependencies: - build_runner: ^2.4.9 + build_runner: ^2.4.10 freezed: ^2.5.2 json_serializable: ^6.8.0 lint_hard: ^4.0.0 diff --git a/pubspec.lock b/pubspec.lock index 07b05e5..c6e754b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -68,10 +68,10 @@ packages: dependency: "direct main" description: name: awesome_extensions - sha256: c3bf11d07a69fe10ff5541717b920661c7a87a791ee182851f1c92a2d15b95a2 + sha256: "07e52221467e651cab9219a26286245760831c3852ea2c54883a48a54f120d7c" url: "https://pub.dev" source: hosted - version: "2.0.14" + version: "2.0.16" badges: dependency: "direct main" description: @@ -139,10 +139,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.2" build_resolvers: dependency: transitive description: @@ -155,10 +155,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" + sha256: "1414d6d733a85d8ad2f1dfcb3ea7945759e35a123cb99ccfac75d0758f75edfa" url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.4.10" build_runner_core: dependency: transitive description: @@ -219,10 +219,10 @@ packages: dependency: transitive description: name: camera_android - sha256: "7b0aba6398afa8475e2bc9115d976efb49cf8db781e922572d443795c04a4f4f" + sha256: b350ac087f111467e705b2b76cc1322f7f5bdc122aa83b4b243b0872f390d229 url: "https://pub.dev" source: hosted - version: "0.10.9+1" + version: "0.10.9+2" camera_avfoundation: dependency: transitive description: @@ -387,10 +387,10 @@ packages: dependency: transitive description: name: diffutil_dart - sha256: e0297e4600b9797edff228ed60f4169a778ea357691ec98408fa3b72994c7d06 + sha256: "5e74883aedf87f3b703cb85e815bdc1ed9208b33501556e4a8a5572af9845c81" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "4.0.1" equatable: dependency: "direct main" description: @@ -403,10 +403,10 @@ packages: dependency: "direct main" description: name: fast_immutable_collections - sha256: "38fbc50df5b219dcfb83ebbc3275ec09872530ca1153858fc56fceadb310d037" + sha256: "533806a7f0c624c2e479d05d3fdce4c87109a7cd0db39b8cc3830d3a2e8dedc7" url: "https://pub.dev" source: hosted - version: "10.2.2" + version: "10.2.3" ffi: dependency: transitive description: @@ -472,10 +472,10 @@ packages: dependency: "direct main" description: name: flutter_chat_ui - sha256: c8580c85e2d29359ffc84147e643d08d883eb6e757208652377f0105ef58807f + sha256: "40fb37acc328dd179eadc3d67bf8bd2d950dc0da34464aa8d48e8707e0234c09" url: "https://pub.dev" source: hosted - version: "1.6.12" + version: "1.6.13" flutter_form_builder: dependency: "direct main" description: @@ -634,10 +634,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: b465e99ce64ba75e61c8c0ce3d87b66d8ac07f0b35d0a7e0263fcfc10f99e836 + sha256: aa073287b8f43553678e6fa9e8bb9c83212ff76e09542129a8099bbc8db4df65 url: "https://pub.dev" source: hosted - version: "13.2.5" + version: "14.1.2" graphs: dependency: transitive description: @@ -714,10 +714,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" io: dependency: transitive description: @@ -802,10 +802,10 @@ packages: dependency: "direct main" description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" mime: dependency: transitive description: @@ -818,10 +818,10 @@ packages: dependency: "direct main" description: name: mobile_scanner - sha256: "827765afbd4792ff3fd105ad593821ac0f6d8a7d352689013b07ee85be336312" + sha256: b8c0e9afcfd52534f85ec666f3d52156f560b5e6c25b1e3d4fe2087763607926 url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "5.1.1" motion_toast: dependency: "direct main" description: @@ -938,10 +938,10 @@ packages: dependency: transitive description: name: photo_view - sha256: "8036802a00bae2a78fc197af8a158e3e2f7b500561ed23b4c458107685e645bb" + sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e" url: "https://pub.dev" source: hosted - version: "0.14.0" + version: "0.15.0" pinput: dependency: "direct main" description: @@ -1106,26 +1106,26 @@ packages: dependency: "direct main" description: name: searchable_listview - sha256: f9bc1a57dfcba49ce2d190d642567fb82309dd23849b3b0a328266e3f90054db + sha256: dfa6358f5e097f45b5b51a160cb6189e112e3abe0f728f4740349cd3b6575617 url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" share_plus: dependency: "direct main" description: name: share_plus - sha256: fb5319f3aab4c5dda5ebb92dca978179ba21f8c783ee4380910ef4c1c6824f51 + sha256: ef3489a969683c4f3d0239010cc8b7a2a46543a8d139e111c06c558875083544 url: "https://pub.dev" source: hosted - version: "8.0.3" + version: "9.0.0" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496" + sha256: "0f9e4418835d1b2c3ae78fdb918251959106cefdbc4dd43526e182f80e82f6d4" url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "4.0.0" shared_preferences: dependency: "direct main" description: @@ -1194,10 +1194,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" signal_strength_indicator: dependency: "direct main" description: @@ -1399,10 +1399,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775" + sha256: "17cd5e205ea615e2c6ea7a77323a11712dffa0720a8a90540db57a01347f9ad9" url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.2" url_launcher_ios: dependency: transitive description: @@ -1529,30 +1529,38 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "217f49b5213796cb508d6a942a5dc604ce1cb6a0a6b3d8cb3f0c314f0ecea712" + url: "https://pub.dev" + source: hosted + version: "0.1.4" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + sha256: a2d56211ee4d35d9b344d9d4ce60f362e4f5d1aafb988302906bd732bc731276 url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "3.0.0" win32: dependency: transitive description: name: win32 - sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" + sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 url: "https://pub.dev" source: hosted - version: "5.5.0" + version: "5.5.1" window_manager: dependency: "direct main" description: name: window_manager - sha256: b3c895bdf936c77b83c5254bec2e6b3f066710c1f89c38b20b8acc382b525494 + sha256: "8699323b30da4cdbe2aa2e7c9de567a6abd8a97d9a5c850a3c86dcd0b34bbfbf" url: "https://pub.dev" source: hosted - version: "0.3.8" + version: "0.3.9" xdg_directories: dependency: transitive description: @@ -1610,5 +1618,5 @@ packages: source: hosted version: "1.1.2" sdks: - dart: ">=3.3.0 <4.0.0" + dart: ">=3.4.0 <4.0.0" flutter: ">=3.19.1" diff --git a/pubspec.yaml b/pubspec.yaml index d3e5a50..f3bc02b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,14 +44,14 @@ dependencies: flutter_translate: ^4.0.4 form_builder_validators: ^9.1.0 freezed_annotation: ^2.4.1 - go_router: ^13.2.5 + go_router: ^14.1.2 hydrated_bloc: ^9.1.5 image: ^4.1.7 intl: ^0.18.1 json_annotation: ^4.9.0 loggy: ^2.0.3 meta: ^1.11.0 - mobile_scanner: ^4.0.1 + mobile_scanner: ^5.1.1 motion_toast: ^2.9.1 pasteboard: ^0.2.0 path: ^1.9.0 @@ -66,7 +66,7 @@ dependencies: radix_colors: ^1.0.4 reorderable_grid: ^1.0.10 searchable_listview: ^2.12.0 - share_plus: ^8.0.3 + share_plus: ^9.0.0 shared_preferences: ^2.2.3 signal_strength_indicator: ^0.4.1 split_view: ^3.2.1 @@ -88,6 +88,9 @@ dependency_overrides: path: ../dart_async_tools bloc_advanced_tools: path: ../bloc_advanced_tools + # REMOVE ONCE form_builder_validators HAS A FIX UPSTREAM + intl: 0.19.0 + dev_dependencies: build_runner: ^2.4.9 From ff1ea709a8412e6a6e03a46fdb1e60e0e1c591d8 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 21 May 2024 15:19:27 -0400 Subject: [PATCH 101/270] tests pass --- .../example/integration_test/app_test.dart | 16 +- .../integration_test/test_dht_log.dart | 4 +- .../test_dht_record_pool.dart | 2 +- .../test_dht_short_array.dart | 7 +- packages/veilid_support/example/pubspec.lock | 2 +- packages/veilid_support/example/pubspec.yaml | 2 - .../lib/dht_support/src/dht_log/dht_log.dart | 8 +- .../src/dht_log/dht_log_append.dart | 27 ++-- .../dht_support/src/dht_log/dht_log_read.dart | 4 +- .../src/dht_log/dht_log_spine.dart | 140 ++++++++++++++---- .../src/dht_record/dht_record.dart | 8 +- .../src/dht_short_array/dht_short_array.dart | 8 +- .../{dht_openable.dart => dht_closeable.dart} | 23 ++- .../src/interfaces/interfaces.dart | 2 +- 14 files changed, 178 insertions(+), 75 deletions(-) rename packages/veilid_support/lib/dht_support/src/interfaces/{dht_openable.dart => dht_closeable.dart} (61%) diff --git a/packages/veilid_support/example/integration_test/app_test.dart b/packages/veilid_support/example/integration_test/app_test.dart index 9c85998..83e3dc8 100644 --- a/packages/veilid_support/example/integration_test/app_test.dart +++ b/packages/veilid_support/example/integration_test/app_test.dart @@ -1,10 +1,6 @@ -//@Timeout(Duration(seconds: 240)) - -//library veilid_support_integration_test; - import 'package:flutter/foundation.dart'; -import 'package:test/test.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:test/test.dart'; import 'package:veilid_test/veilid_test.dart'; import 'fixtures/fixtures.dart'; @@ -26,7 +22,7 @@ void main() { tickerFixture: tickerFixture, updateProcessorFixture: updateProcessorFixture); - group('Started Tests', () { + group(timeout: const Timeout(Duration(seconds: 240)), 'Started Tests', () { setUpAll(veilidFixture.setUp); tearDownAll(veilidFixture.tearDown); tearDownAll(() { @@ -74,9 +70,11 @@ void main() { for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { test('create log stride=$stride', makeTestDHTLogCreateDelete(stride: stride)); - test('add/truncate log stride=$stride', - makeTestDHTLogAddTruncate(stride: stride), - timeout: const Timeout(Duration(seconds: 480))); + test( + timeout: const Timeout(Duration(seconds: 480)), + 'add/truncate log stride=$stride', + makeTestDHTLogAddTruncate(stride: stride), + ); } }); }); diff --git a/packages/veilid_support/example/integration_test/test_dht_log.dart b/packages/veilid_support/example/integration_test/test_dht_log.dart index f8d758e..0e5829c 100644 --- a/packages/veilid_support/example/integration_test/test_dht_log.dart +++ b/packages/veilid_support/example/integration_test/test_dht_log.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:flutter_test/flutter_test.dart'; +import 'package:test/test.dart'; import 'package:veilid_support/veilid_support.dart'; Future Function() makeTestDHTLogCreateDelete({required int stride}) => @@ -61,7 +61,7 @@ Future Function() makeTestDHTLogAddTruncate({required int stride}) => print('adding\n'); { final res = await dlog.operateAppend((w) async { - const chunk = 50; + const chunk = 25; for (var n = 0; n < dataset.length; n += chunk) { print('$n-${n + chunk - 1} '); final success = diff --git a/packages/veilid_support/example/integration_test/test_dht_record_pool.dart b/packages/veilid_support/example/integration_test/test_dht_record_pool.dart index 2f52c00..1b300a9 100644 --- a/packages/veilid_support/example/integration_test/test_dht_record_pool.dart +++ b/packages/veilid_support/example/integration_test/test_dht_record_pool.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; +import 'package:test/test.dart'; import 'package:veilid_support/veilid_support.dart'; Future testDHTRecordPoolCreate() async { diff --git a/packages/veilid_support/example/integration_test/test_dht_short_array.dart b/packages/veilid_support/example/integration_test/test_dht_short_array.dart index 7dead48..637afe0 100644 --- a/packages/veilid_support/example/integration_test/test_dht_short_array.dart +++ b/packages/veilid_support/example/integration_test/test_dht_short_array.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:flutter_test/flutter_test.dart'; +import 'package:test/test.dart'; import 'package:veilid_support/veilid_support.dart'; Future Function() makeTestDHTShortArrayCreateDelete( @@ -118,7 +118,10 @@ Future Function() makeTestDHTShortArrayAdd({required int stride}) => //print('clear\n'); { - await arr.operateWrite((w) async => w.clear()); + await arr.operateWriteEventual((w) async { + await w.clear(); + return true; + }); } //print('get all\n'); diff --git a/packages/veilid_support/example/pubspec.lock b/packages/veilid_support/example/pubspec.lock index 3defe80..b7cbcd7 100644 --- a/packages/veilid_support/example/pubspec.lock +++ b/packages/veilid_support/example/pubspec.lock @@ -196,7 +196,7 @@ packages: source: sdk version: "0.0.0" flutter_test: - dependency: "direct dev" + dependency: transitive description: flutter source: sdk version: "0.0.0" diff --git a/packages/veilid_support/example/pubspec.yaml b/packages/veilid_support/example/pubspec.yaml index 60f06e7..f353fc9 100644 --- a/packages/veilid_support/example/pubspec.yaml +++ b/packages/veilid_support/example/pubspec.yaml @@ -15,8 +15,6 @@ dependencies: dev_dependencies: async_tools: ^0.1.1 - flutter_test: - sdk: flutter integration_test: sdk: flutter lint_hard: ^4.0.0 diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart index 5dd36a0..3f561ff 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart @@ -42,7 +42,7 @@ class DHTLogUpdate extends Equatable { /// * The head and tail position of the log /// - subkeyIdx = pos / recordsPerSubkey /// - recordIdx = pos % recordsPerSubkey -class DHTLog implements DHTOpenable { +class DHTLog implements DHTDeleteable { //////////////////////////////////////////////////////////////// // Constructors @@ -160,12 +160,16 @@ class DHTLog implements DHTOpenable { ); //////////////////////////////////////////////////////////////////////////// - // DHTOpenable + // DHTCloseable /// Check if the DHTLog is open @override bool get isOpen => _openCount > 0; + /// The type of the openable scope + @override + FutureOr scoped() => this; + /// Add a reference to this log @override Future ref() async => _mutex.protect(() async { diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_append.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_append.dart index 877df89..c184032 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_append.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_append.dart @@ -17,7 +17,7 @@ class _DHTLogAppend extends _DHTLogRead implements DHTAppendTruncateRandomRead { } // Write item to the segment - return lookup.shortArray.scope((sa) => sa.operateWrite((write) async { + return lookup.scope((sa) => sa.operateWrite((write) async { // If this a new segment, then clear it in case we have wrapped around if (lookup.pos == 0) { await write.clear(); @@ -51,18 +51,17 @@ class _DHTLogAppend extends _DHTLogRead implements DHTAppendTruncateRandomRead { final sublistValues = values.sublist(valueIdx, valueIdx + sacount); dws.add(() async { - final ok = await lookup.shortArray - .scope((sa) => sa.operateWrite((write) async { - // If this a new segment, then clear it in - // case we have wrapped around - if (lookup.pos == 0) { - await write.clear(); - } else if (lookup.pos != write.length) { - // We should always be appending at the length - throw StateError('appending should be at the end'); - } - return write.tryAddItems(sublistValues); - })); + final ok = await lookup.scope((sa) => sa.operateWrite((write) async { + // If this a new segment, then clear it in + // case we have wrapped around + if (lookup.pos == 0) { + await write.clear(); + } else if (lookup.pos != write.length) { + // We should always be appending at the length + throw StateError('appending should be at the end'); + } + return write.tryAddItems(sublistValues); + })); if (!ok) { success = false; } @@ -71,7 +70,7 @@ class _DHTLogAppend extends _DHTLogRead implements DHTAppendTruncateRandomRead { valueIdx += sacount; } - await dws(chunkSize: maxDHTConcurrency); + await dws(); return success; } diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart index ea36fc2..3618abd 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart @@ -19,7 +19,7 @@ class _DHTLogRead implements DHTRandomRead { return null; } - return lookup.shortArray.scope((sa) => sa.operate( + return lookup.scope((sa) => sa.operate( (read) => read.getItem(lookup.pos, forceRefresh: forceRefresh))); } @@ -71,7 +71,7 @@ class _DHTLogRead implements DHTRandomRead { // Check each segment for offline positions var foundOffline = false; - await lookup.shortArray.scope((sa) => sa.operate((read) async { + await lookup.scope((sa) => sa.operate((read) async { final segmentOffline = await read.getOfflinePositions(); // For each shortarray segment go through their segment positions diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index 12a80c9..9a8c64e 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -1,9 +1,58 @@ part of 'dht_log.dart'; -class DHTLogPositionLookup { - const DHTLogPositionLookup({required this.shortArray, required this.pos}); - final DHTShortArray shortArray; +class _DHTLogPosition extends DHTCloseable<_DHTLogPosition, DHTShortArray> { + _DHTLogPosition._({ + required _DHTLogSpine dhtLogSpine, + required DHTShortArray shortArray, + required this.pos, + required int segmentNumber, + }) : _segmentShortArray = shortArray, + _dhtLogSpine = dhtLogSpine, + _segmentNumber = segmentNumber; final int pos; + + final _DHTLogSpine _dhtLogSpine; + final DHTShortArray _segmentShortArray; + var _openCount = 1; + final int _segmentNumber; + final Mutex _mutex = Mutex(); + + /// Check if the DHTLogPosition is open + @override + bool get isOpen => _openCount > 0; + + /// The type of the openable scope + @override + FutureOr scoped() => _segmentShortArray; + + /// Add a reference to this log + @override + Future<_DHTLogPosition> ref() async => _mutex.protect(() async { + _openCount++; + return this; + }); + + /// Free all resources for the DHTLogPosition + @override + Future close() async => _mutex.protect(() async { + if (_openCount == 0) { + throw StateError('already closed'); + } + _openCount--; + if (_openCount != 0) { + return; + } + await _dhtLogSpine._segmentClosed(_segmentNumber); + }); +} + +class _OpenedSegment { + _OpenedSegment._({ + required this.shortArray, + }); + + final DHTShortArray shortArray; + int openCount = 1; } class _DHTLogSegmentLookup extends Equatable { @@ -32,6 +81,7 @@ class _DHTLogSpine { _head = head, _tail = tail, _segmentStride = stride, + _openedSegments = {}, _spineCache = []; // Create a new spine record and push it to the network @@ -85,6 +135,8 @@ class _DHTLogSpine { futures.add(sc.close()); } await Future.wait(futures); + + assert(_openedSegments.isEmpty, 'should have closed all segments by now'); }); } @@ -247,7 +299,7 @@ class _DHTLogSpine { // Lookup what subkey and segment subrange has this position's segment // shortarray - final l = lookupSegment(segmentNumber); + final l = _lookupSegment(segmentNumber); final subkey = l.subkey; final segment = l.segment; @@ -304,7 +356,7 @@ class _DHTLogSpine { // Lookup what subkey and segment subrange has this position's segment // shortarray - final l = lookupSegment(segmentNumber); + final l = _lookupSegment(segmentNumber); final subkey = l.subkey; final segment = l.segment; @@ -381,7 +433,7 @@ class _DHTLogSpine { return segment; } - _DHTLogSegmentLookup lookupSegment(int segmentNumber) { + _DHTLogSegmentLookup _lookupSegment(int segmentNumber) { assert(_spineMutex.isLocked, 'should be in mutex here'); if (segmentNumber < 0) { @@ -400,30 +452,60 @@ class _DHTLogSpine { /////////////////////////////////////////// // API for public interfaces - Future lookupPosition(int pos) async { + Future<_DHTLogPosition?> lookupPosition(int pos) async { assert(_spineMutex.isLocked, 'should be locked'); + return _spineCacheMutex.protect(() async { + // Check if our position is in bounds + final endPos = length; + if (pos < 0 || pos >= endPos) { + throw IndexError.withLength(pos, endPos); + } - // Check if our position is in bounds - final endPos = length; - if (pos < 0 || pos >= endPos) { - throw IndexError.withLength(pos, endPos); - } + // Calculate absolute position, ring-buffer style + final absolutePosition = (_head + pos) % _positionLimit; - // Calculate absolute position, ring-buffer style - final absolutePosition = (_head + pos) % _positionLimit; + // Determine the segment number and position within the segment + final segmentNumber = absolutePosition ~/ DHTShortArray.maxElements; + final segmentPos = absolutePosition % DHTShortArray.maxElements; - // Determine the segment number and position within the segment - final segmentNumber = absolutePosition ~/ DHTShortArray.maxElements; - final segmentPos = absolutePosition % DHTShortArray.maxElements; + // Get the segment shortArray + final openedSegment = _openedSegments[segmentNumber]; + late final DHTShortArray shortArray; + if (openedSegment != null) { + openedSegment.openCount++; + shortArray = openedSegment.shortArray; + } else { + final newShortArray = (_spineRecord.writer == null) + ? await _openSegment(segmentNumber) + : await _openOrCreateSegment(segmentNumber); + if (newShortArray == null) { + return null; + } - // Get the segment shortArray - final shortArray = (_spineRecord.writer == null) - ? await _openSegment(segmentNumber) - : await _openOrCreateSegment(segmentNumber); - if (shortArray == null) { - return null; - } - return DHTLogPositionLookup(shortArray: shortArray, pos: segmentPos); + _openedSegments[segmentNumber] = + _OpenedSegment._(shortArray: newShortArray); + + shortArray = newShortArray; + } + + return _DHTLogPosition._( + dhtLogSpine: this, + shortArray: shortArray, + pos: segmentPos, + segmentNumber: segmentNumber); + }); + } + + Future _segmentClosed(int segmentNumber) async { + assert(_spineMutex.isLocked, 'should be locked'); + await _spineCacheMutex.protect(() async { + final os = _openedSegments[segmentNumber]!; + os.openCount--; + if (os.openCount == 0) { + _openedSegments.remove(segmentNumber); + await os.shortArray.close(); + } + }); } void allocateTail(int count) { @@ -479,7 +561,7 @@ class _DHTLogSpine { segmentNumber++) { // Lookup what subkey and segment subrange has this position's segment // shortarray - final l = lookupSegment(segmentNumber); + final l = _lookupSegment(segmentNumber); final subkey = l.subkey; final segment = l.segment; @@ -608,6 +690,8 @@ class _DHTLogSpine { // Spine DHT record final DHTRecord _spineRecord; + // Segment stride to use for spine elements + final int _segmentStride; // Position of the start of the log (oldest items) int _head; @@ -616,8 +700,8 @@ class _DHTLogSpine { // LRU cache of DHT spine elements accessed recently // Pair of position and associated shortarray segment + final Mutex _spineCacheMutex = Mutex(); final List<(int, DHTShortArray)> _spineCache; + final Map _openedSegments; static const int _spineCacheLength = 3; - // Segment stride to use for spine elements - final int _segmentStride; } 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 7bf5129..80b68ad 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 @@ -36,7 +36,7 @@ enum DHTRecordRefreshMode { ///////////////////////////////////////////////// -class DHTRecord implements DHTOpenable { +class DHTRecord implements DHTDeleteable { DHTRecord._( {required VeilidRoutingContext routingContext, required SharedDHTRecordData sharedDHTRecordData, @@ -52,12 +52,16 @@ class DHTRecord implements DHTOpenable { _sharedDHTRecordData = sharedDHTRecordData; //////////////////////////////////////////////////////////////////////////// - // DHTOpenable + // DHTCloseable /// Check if the DHTRecord is open @override bool get isOpen => _openCount > 0; + /// The type of the openable scope + @override + FutureOr scoped() => this; + /// Add a reference to this DHTRecord @override Future ref() async => _mutex.protect(() async { 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 cd62fa6..daf3061 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 @@ -13,7 +13,7 @@ part 'dht_short_array_write.dart'; /////////////////////////////////////////////////////////////////////// -class DHTShortArray implements DHTOpenable { +class DHTShortArray implements DHTDeleteable { //////////////////////////////////////////////////////////////// // Constructors @@ -136,12 +136,16 @@ class DHTShortArray implements DHTOpenable { ); //////////////////////////////////////////////////////////////////////////// - // DHTOpenable + // DHTCloseable /// Check if the shortarray is open @override bool get isOpen => _openCount > 0; + /// The type of the openable scope + @override + FutureOr scoped() => this; + /// Add a reference to this shortarray @override Future ref() async => _mutex.protect(() async { diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_openable.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_closeable.dart similarity index 61% rename from packages/veilid_support/lib/dht_support/src/interfaces/dht_openable.dart rename to packages/veilid_support/lib/dht_support/src/interfaces/dht_closeable.dart index ffd58f9..65e9db1 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_openable.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_closeable.dart @@ -1,27 +1,36 @@ import 'dart:async'; -abstract class DHTOpenable { +import 'package:meta/meta.dart'; + +abstract class DHTCloseable { bool get isOpen; + @protected + FutureOr scoped(); Future ref(); Future close(); +} + +abstract class DHTDeleteable extends DHTCloseable { Future delete(); } -extension DHTOpenableExt> on D { - /// Runs a closure that guarantees the DHTOpenable +extension DHTCloseableExt on DHTCloseable { + /// Runs a closure that guarantees the DHTCloseable /// will be closed upon exit, even if an uncaught exception is thrown Future scope(Future Function(D) scopeFunction) async { if (!isOpen) { throw StateError('not open in scope'); } try { - return await scopeFunction(this); + return await scopeFunction(await scoped()); } finally { await close(); } } +} - /// Runs a closure that guarantees the DHTOpenable +extension DHTDeletableExt on DHTDeleteable { + /// Runs a closure that guarantees the DHTCloseable /// will be closed upon exit, and deleted if an an /// uncaught exception is thrown Future deleteScope(Future Function(D) scopeFunction) async { @@ -30,7 +39,7 @@ extension DHTOpenableExt> on D { } try { - return await scopeFunction(this); + return await scopeFunction(await scoped()); } on Exception { await delete(); rethrow; @@ -39,7 +48,7 @@ extension DHTOpenableExt> on D { } } - /// Scopes a closure that conditionally deletes the DHTOpenable on exit + /// Scopes a closure that conditionally deletes the DHTCloseable on exit Future maybeDeleteScope( bool delete, Future Function(D) scopeFunction) async { if (delete) { diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart b/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart index 6c61075..16f9970 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart @@ -1,4 +1,4 @@ -export 'dht_openable.dart'; +export 'dht_closeable.dart'; export 'dht_random_read.dart'; export 'dht_random_write.dart'; export 'exceptions.dart'; From 11be8bb70532f4266dc3f1790f2038c6f258218b Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 21 May 2024 19:48:36 -0400 Subject: [PATCH 102/270] lock updates --- macos/Podfile.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 7ca005d..faa2836 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,6 +1,6 @@ PODS: - FlutterMacOS (1.0.0) - - mobile_scanner (3.5.6): + - mobile_scanner (5.1.1): - FlutterMacOS - pasteboard (0.0.1): - FlutterMacOS @@ -68,15 +68,15 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - mobile_scanner: 54ceceae0c8da2457e26a362a6be5c61154b1829 + mobile_scanner: 1efac1e53c294b24e3bb55bcc7f4deee0233a86b pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99 - path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf - shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 smart_auth: b38e3ab4bfe089eacb1e233aca1a2340f96c28e9 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec - url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 + url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 veilid: a54f57b7bcf0e4e072fe99272d76ca126b2026d0 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 From 70e5012d37501a3a39e85092649dfcff02843aaf Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 21 May 2024 20:07:43 -0400 Subject: [PATCH 103/270] use newer protoc --- dev-setup/install_protoc_linux.sh | 2 +- dev-setup/setup_windows.bat | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev-setup/install_protoc_linux.sh b/dev-setup/install_protoc_linux.sh index d01a780..8f5052e 100755 --- a/dev-setup/install_protoc_linux.sh +++ b/dev-setup/install_protoc_linux.sh @@ -1,6 +1,6 @@ #!/bin/bash SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -PROTOC_VERSION="24.3" # Keep in sync with veilid-core/build.rs +PROTOC_VERSION="25.3" UNAME_M=$(uname -m) if [[ "$UNAME_M" == "x86_64" ]]; then diff --git a/dev-setup/setup_windows.bat b/dev-setup/setup_windows.bat index b7e3ef6..7338b58 100644 --- a/dev-setup/setup_windows.bat +++ b/dev-setup/setup_windows.bat @@ -17,7 +17,7 @@ IF NOT DEFINED ProgramFiles(x86) ( FOR %%X IN (protoc.exe) DO (SET PROTOC_FOUND=%%~$PATH:X) IF NOT DEFINED PROTOC_FOUND ( echo protobuf compiler ^(protoc^) is required but it's not installed. Install protoc 23.2 or higher. Ensure it is in your path. Aborting. - echo protoc is available here: https://github.com/protocolbuffers/protobuf/releases/download/v23.2/protoc-23.2-win64.zip + echo protoc is available here: https://github.com/protocolbuffers/protobuf/releases/download/v25.3/protoc-25.3-win64.zip goto end ) From 7d80bf59bd08acb74f83ad543fad04be148b5f7d Mon Sep 17 00:00:00 2001 From: Ethan Hindmarsh Loves Veilid Date: Sat, 1 Jun 2024 20:55:08 -0500 Subject: [PATCH 104/270] bump2version integration --- .bumpversion.cfg | 23 +++++++++++++++++++++++ pubspec.yaml | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 .bumpversion.cfg diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 0000000..c20a4e4 --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,23 @@ +[bumpversion] +current_version = 0.1.2+5 +commit = False +tag = False +parse = (?P\d+)\.(?P\d+)\.(?P\d+)\+(?P\d+) +serialize = + {major}.{minor}.{patch}+{buildcode} + +[bumpversion:file:pubspec.yaml] +search = version: {current_version} +replace = version: {new_version} + +[bumpversion:part:major] +first_value = 0 + +[bumpversion:part:minor] +first_value = 0 + +[bumpversion:part:patch] +first_value = 0 + +[bumpversion:part:buildcode] +first_value = 1 diff --git a/pubspec.yaml b/pubspec.yaml index f3bc02b..3a1401c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: veilidchat description: VeilidChat publish_to: 'none' -version: 0.1.2+4 +version: 0.1.2+5 environment: sdk: '>=3.2.0 <4.0.0' From 9395a997876e3dc45c6dfe66063de0f970019f50 Mon Sep 17 00:00:00 2001 From: Ethan Hindmarsh Loves Veilid Date: Sun, 2 Jun 2024 13:37:07 -0500 Subject: [PATCH 105/270] version_bump.sh --- .bumpversion.cfg | 3 --- version_bump.sh | 68 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 3 deletions(-) create mode 100755 version_bump.sh diff --git a/.bumpversion.cfg b/.bumpversion.cfg index c20a4e4..34b6ef7 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -18,6 +18,3 @@ first_value = 0 [bumpversion:part:patch] first_value = 0 - -[bumpversion:part:buildcode] -first_value = 1 diff --git a/version_bump.sh b/version_bump.sh new file mode 100755 index 0000000..51e8ac4 --- /dev/null +++ b/version_bump.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +# Fail out if any step has an error +set -e + +if [ "$1" == "patch" ]; then + echo Bumping patch version + PART=patch +elif [ "$1" == "minor" ]; then + echo Bumping minor version + PART=minor +elif [ "$1" == "major" ]; then + echo Bumping major version + PART=major +else + echo Unsupported part! Specify 'patch', 'minor', or 'major' + exit 1 +fi + +# Function to increment the build code +increment_buildcode() { + local current_version=$1 + local major_minor_patch=${current_version%+*} + local buildcode=${current_version#*+} + local new_buildcode=$((buildcode + 1)) + echo "${major_minor_patch}+${new_buildcode}" +} + +# Function to get the current version from pubspec.yaml +get_current_version() { + grep -oP '(?<=version: ).*' pubspec.yaml +} + +# Function to update the version in pubspec.yaml +update_version() { + local new_version=$1 + sed -i "s/version: .*/version: ${new_version}/" pubspec.yaml +} + +# I pray none of this errors! - I think it should popup an error should that happen.. + +current_version=$(get_current_version) + +echo "Current Version: $current_version" + +# Bump the major, minor, or patch version using bump2version +bump2version $PART + +# Get the new version after bump2version +new_version=$(get_current_version) + +# Preserve the current build code +buildcode=${current_version#*+} +new_version="${new_version%+*}+${buildcode}" + +# Increment the build code +final_version=$(increment_buildcode $new_version) + +# Update pubspec.yaml with the final version +update_version $final_version + +# Print the final version +echo "New Version: $final_version" + +#git add pubspec.yaml +#git commit -m "Bump version to $final_version" +#git tag "v$final_version" + From 7ba9264647ecce4abfb43d8d579e1d0199bb6d0b Mon Sep 17 00:00:00 2001 From: Ethan Hindmarsh Loves Veilid Date: Sun, 2 Jun 2024 13:48:45 -0500 Subject: [PATCH 106/270] version_bump.sh (pls squash) --- version_bump.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version_bump.sh b/version_bump.sh index 51e8ac4..85f9196 100755 --- a/version_bump.sh +++ b/version_bump.sh @@ -44,7 +44,7 @@ current_version=$(get_current_version) echo "Current Version: $current_version" # Bump the major, minor, or patch version using bump2version -bump2version $PART +bump2version --current-version $current_version $PART # Get the new version after bump2version new_version=$(get_current_version) From d851acc7e98a093fdffab26095013dd8c8c6c6e9 Mon Sep 17 00:00:00 2001 From: Ethan Hindmarsh Loves Veilid Date: Sun, 2 Jun 2024 13:50:47 -0500 Subject: [PATCH 107/270] grep -> awk (version_bump.sh) --- version_bump.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version_bump.sh b/version_bump.sh index 85f9196..5ada2c3 100755 --- a/version_bump.sh +++ b/version_bump.sh @@ -28,7 +28,7 @@ increment_buildcode() { # Function to get the current version from pubspec.yaml get_current_version() { - grep -oP '(?<=version: ).*' pubspec.yaml + awk '/^version: / { print $2 }' pubspec.yaml } # Function to update the version in pubspec.yaml From ed72de952aeaa835fb27715180c9ee936208fc7b Mon Sep 17 00:00:00 2001 From: Ethan Hindmarsh Loves Veilid Date: Sun, 2 Jun 2024 14:03:10 -0500 Subject: [PATCH 108/270] darwin-specific sed --- version_bump.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/version_bump.sh b/version_bump.sh index 5ada2c3..cd17b37 100755 --- a/version_bump.sh +++ b/version_bump.sh @@ -34,7 +34,13 @@ get_current_version() { # Function to update the version in pubspec.yaml update_version() { local new_version=$1 - sed -i "s/version: .*/version: ${new_version}/" pubspec.yaml + + if [[ "$OSTYPE" == "darwin"* ]]; then + SED_CMD="sed -i ''" + else + SED_CMD="sed -i" + fi + eval "$SED_CMD 's/version: .*/version: ${new_version}/' pubspec.yaml" } # I pray none of this errors! - I think it should popup an error should that happen.. From e607a141b367458f2db2d7193b0cb7d04a317c1c Mon Sep 17 00:00:00 2001 From: TC Date: Sun, 2 Jun 2024 20:12:03 +0000 Subject: [PATCH 109/270] Update pubspec.yaml to actual current version and buildcode. --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 3a1401c..1cc893f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: veilidchat description: VeilidChat publish_to: 'none' -version: 0.1.2+5 +version: 0.2.0+10 environment: sdk: '>=3.2.0 <4.0.0' From 83c87157428b6512caf73b0fa65ce64409aef17a Mon Sep 17 00:00:00 2001 From: TC Date: Sun, 2 Jun 2024 20:13:03 +0000 Subject: [PATCH 110/270] Update .bumpversion.cfg to actual current version and buildcode --- .bumpversion.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 34b6ef7..85f2f9d 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.1.2+5 +current_version = 0.2.0+10 commit = False tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)\+(?P\d+) From 5d89de9bfec6c8021d7602e280d3544fad3dccd2 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 25 May 2024 22:46:43 -0400 Subject: [PATCH 111/270] tabledb array work --- .../models/active_account_info.dart | 6 +- .../cubits/single_contact_messages_cubit.dart | 220 ++- lib/chat/models/message_state.dart | 9 +- lib/chat/models/message_state.freezed.dart | 85 +- lib/chat/models/message_state.g.dart | 6 +- lib/chat_list/cubits/chat_list_cubit.dart | 2 +- .../cubits/contact_invitation_list_cubit.dart | 4 +- .../cubits/contact_request_inbox_cubit.dart | 2 +- lib/contacts/cubits/conversation_cubit.dart | 8 +- lib/proto/extensions.dart | 12 + lib/proto/proto.dart | 2 + lib/proto/veilidchat.pb.dart | 1626 +++++++++++++---- lib/proto/veilidchat.pbenum.dart | 55 +- lib/proto/veilidchat.pbjson.dart | 419 +++-- lib/proto/veilidchat.proto | 385 ++-- lib/theme/views/widget_helpers.dart | 2 +- .../example/integration_test/app_test.dart | 12 + .../integration_test/test_dht_log.dart | 7 +- .../integration_test/test_table_db_array.dart | 134 ++ .../lib/dht_support/src/dht_log/dht_log.dart | 23 +- .../src/dht_log/dht_log_cubit.dart | 8 +- .../dht_support/src/dht_log/dht_log_read.dart | 4 +- ...dht_log_append.dart => dht_log_write.dart} | 42 +- .../dht_support/src/dht_record/barrel.dart | 1 - .../src/dht_record/dht_record.dart | 45 +- .../src/dht_record/dht_record_crypto.dart | 53 - .../src/dht_record/dht_record_pool.dart | 14 +- .../src/dht_short_array/dht_short_array.dart | 15 +- .../dht_short_array_cubit.dart | 7 +- .../dht_short_array/dht_short_array_read.dart | 4 +- .../dht_short_array_write.dart | 10 +- .../src/interfaces/dht_append.dart | 41 + .../src/interfaces/dht_append_truncate.dart | 51 - .../dht_support/src/interfaces/dht_clear.dart | 7 + .../src/interfaces/dht_insert_remove.dart | 60 + .../src/interfaces/dht_random_read.dart | 13 +- .../src/interfaces/dht_random_write.dart | 73 +- .../src/interfaces/dht_truncate.dart | 8 + .../src/interfaces/interfaces.dart | 4 + packages/veilid_support/lib/src/identity.dart | 4 +- .../lib/src/table_db_array.dart | 517 ++++++ .../veilid_support/lib/src/veilid_crypto.dart | 52 + .../veilid_support/lib/veilid_support.dart | 2 + packages/veilid_support/pubspec.lock | 2 +- packages/veilid_support/pubspec.yaml | 1 + 45 files changed, 3022 insertions(+), 1035 deletions(-) create mode 100644 lib/proto/extensions.dart create mode 100644 packages/veilid_support/example/integration_test/test_table_db_array.dart rename packages/veilid_support/lib/dht_support/src/dht_log/{dht_log_append.dart => dht_log_write.dart} (67%) delete mode 100644 packages/veilid_support/lib/dht_support/src/dht_record/dht_record_crypto.dart create mode 100644 packages/veilid_support/lib/dht_support/src/interfaces/dht_append.dart delete mode 100644 packages/veilid_support/lib/dht_support/src/interfaces/dht_append_truncate.dart create mode 100644 packages/veilid_support/lib/dht_support/src/interfaces/dht_clear.dart create mode 100644 packages/veilid_support/lib/dht_support/src/interfaces/dht_insert_remove.dart create mode 100644 packages/veilid_support/lib/dht_support/src/interfaces/dht_truncate.dart create mode 100644 packages/veilid_support/lib/src/table_db_array.dart create mode 100644 packages/veilid_support/lib/src/veilid_crypto.dart diff --git a/lib/account_manager/models/active_account_info.dart b/lib/account_manager/models/active_account_info.dart index 7a1437b..2997434 100644 --- a/lib/account_manager/models/active_account_info.dart +++ b/lib/account_manager/models/active_account_info.dart @@ -24,7 +24,7 @@ class ActiveAccountInfo { return KeyPair(key: identityKey, secret: identitySecret.value); } - Future makeConversationCrypto( + Future makeConversationCrypto( TypedKey remoteIdentityPublicKey) async { final identitySecret = userLogin.identitySecret; final cs = await Veilid.instance.getCryptoSystem(identitySecret.kind); @@ -33,8 +33,8 @@ class ActiveAccountInfo { identitySecret.value, utf8.encode('VeilidChat Conversation')); - final messagesCrypto = await DHTRecordCryptoPrivate.fromSecret( - identitySecret.kind, sharedSecret); + final messagesCrypto = + await VeilidCryptoPrivate.fromSecret(identitySecret.kind, sharedSecret); return messagesCrypto; } diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 72c820e..e018e79 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -120,9 +120,8 @@ class SingleContactMessagesCubit extends Cubit { Future _initSentMessagesCubit() async { final writer = _activeAccountInfo.conversationWriter; - _sentMessagesCubit = DHTShortArrayCubit( - open: () async => DHTShortArray.openWrite( - _localMessagesRecordKey, writer, + _sentMessagesCubit = DHTLogCubit( + open: () async => DHTLog.openWrite(_localMessagesRecordKey, writer, debugName: 'SingleContactMessagesCubit::_initSentMessagesCubit::' 'SentMessages', parent: _localConversationRecordKey, @@ -135,8 +134,8 @@ class SingleContactMessagesCubit extends Cubit { // Open remote messages key Future _initRcvdMessagesCubit() async { - _rcvdMessagesCubit = DHTShortArrayCubit( - open: () async => DHTShortArray.openRead(_remoteMessagesRecordKey, + _rcvdMessagesCubit = DHTLogCubit( + open: () async => DHTLog.openRead(_remoteMessagesRecordKey, debugName: 'SingleContactMessagesCubit::_initRcvdMessagesCubit::' 'RcvdMessages', parent: _remoteConversationRecordKey, @@ -152,8 +151,8 @@ class SingleContactMessagesCubit extends Cubit { final accountRecordKey = _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - _reconciledMessagesCubit = DHTShortArrayCubit( - open: () async => DHTShortArray.openOwned(_reconciledChatRecord, + _reconciledMessagesCubit = DHTLogCubit( + open: () async => DHTLog.openOwned(_reconciledChatRecord, debugName: 'SingleContactMessagesCubit::_initReconciledMessagesCubit::' 'ReconciledMessages', @@ -166,10 +165,24 @@ class SingleContactMessagesCubit extends Cubit { //////////////////////////////////////////////////////////////////////////// + // Set the tail position of the log for pagination. + // If tail is 0, the end of the log is used. + // If tail is negative, the position is subtracted from the current log + // length. + // If tail is positive, the position is absolute from the head of the log + // If follow is enabled, the tail offset will update when the log changes + Future setWindow( + {int? tail, int? count, bool? follow, bool forceRefresh = false}) async { + await _initWait(); + await _reconciledMessagesCubit!.setWindow( + tail: tail, count: count, follow: follow, forceRefresh: forceRefresh); + } + + //////////////////////////////////////////////////////////////////////////// + // Called when the sent messages cubit gets a change // This will re-render when messages are sent from another machine - void _updateSentMessagesState( - DHTShortArrayBusyState avmessages) { + void _updateSentMessagesState(DHTLogBusyState avmessages) { final sentMessages = avmessages.state.asData?.value; if (sentMessages == null) { return; @@ -182,27 +195,52 @@ class SingleContactMessagesCubit extends Cubit { } // Called when the received messages cubit gets a change - void _updateRcvdMessagesState( - DHTShortArrayBusyState avmessages) { + void _updateRcvdMessagesState(DHTLogBusyState avmessages) { final rcvdMessages = avmessages.state.asData?.value; if (rcvdMessages == null) { return; } - // Add remote messages updates to queue to process asynchronously - // Ignore offline state because remote messages are always fully delivered - // This may happen once per client but should be idempotent - _unreconciledMessagesQueue.addAllSync(rcvdMessages.map((x) => x.value)); + singleFuture(_rcvdMessagesCubit!, () async { + // Get the timestamp of our most recent reconciled message + final lastReconciledMessageTs = + await _reconciledMessagesCubit!.operate((r) async { + final len = r.length; + if (len == 0) { + return null; + } else { + final lastMessage = + await r.getItemProtobuf(proto.Message.fromBuffer, len - 1); + if (lastMessage == null) { + throw StateError('should have gotten last message'); + } + return lastMessage.timestamp; + } + }); - // Update the view - _renderState(); + // Find oldest message we have not yet reconciled + + // // Go through all the ones from the cubit state first since we've already + // // gotten them from the DHT + // for (var rn = rcvdMessages.elements.length; rn >= 0; rn--) { + // // + // } + + // // Add remote messages updates to queue to process asynchronously + // // Ignore offline state because remote messages are always fully delivered + // // This may happen once per client but should be idempotent + // _unreconciledMessagesQueue.addAllSync(rcvdMessages.map((x) => x.value)); + + // Update the view + _renderState(); + }); } // Called when the reconciled messages list gets a change // This can happen when multiple clients for the same identity are // reading and reconciling the same remote chat void _updateReconciledMessagesState( - DHTShortArrayBusyState avmessages) { + DHTLogBusyState avmessages) { // Update the view _renderState(); } @@ -210,85 +248,85 @@ class SingleContactMessagesCubit extends Cubit { // Async process to reconcile messages sent or received in the background Future _processUnreconciledMessages( IList messages) async { - await _reconciledMessagesCubit! - .operateWrite((reconciledMessagesWriter) async { - await _reconcileMessagesInner( - reconciledMessagesWriter: reconciledMessagesWriter, - messages: messages); - }); + // await _reconciledMessagesCubit! + // .operateAppendEventual((reconciledMessagesWriter) async { + // await _reconcileMessagesInner( + // reconciledMessagesWriter: reconciledMessagesWriter, + // messages: messages); + // }); } // Async process to send messages in the background Future _processSendingMessages(IList messages) async { - for (final message in messages) { - await _sentMessagesCubit!.operateWriteEventual( - (writer) => writer.tryAddItem(message.writeToBuffer())); - } + await _sentMessagesCubit!.operateAppendEventual((writer) => + writer.tryAddItems(messages.map((m) => m.writeToBuffer()).toList())); } Future _reconcileMessagesInner( - {required DHTRandomReadWrite reconciledMessagesWriter, + {required DHTLogWriteOperations reconciledMessagesWriter, required IList messages}) async { - // Ensure remoteMessages is sorted by timestamp - final newMessages = messages - .sort((a, b) => a.timestamp.compareTo(b.timestamp)) - .removeDuplicates(); + // // 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 = await reconciledMessagesWriter - .getItemRangeProtobuf(proto.Message.fromBuffer, 0); - if (existingMessages == null) { - throw Exception( - 'Could not load existing reconciled messages at this time'); - } + // // Existing messages will always be sorted by timestamp so merging is easy + // final existingMessages = await reconciledMessagesWriter + // .getItemRangeProtobuf(proto.Message.fromBuffer, 0); + // if (existingMessages == null) { + // throw Exception( + // 'Could not load existing reconciled messages at this time'); + // } - var ePos = 0; - var nPos = 0; - while (ePos < existingMessages.length && nPos < newMessages.length) { - final existingMessage = existingMessages[ePos]; - final newMessage = newMessages[nPos]; + // 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 + // // 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 reconciledMessagesWriter.tryInsertItem( - ePos, newMessage.writeToBuffer()); - // Insert into local copy as well for this operation - existingMessages.insert(ePos, newMessage); + // // Insert into dht backing array + // await reconciledMessagesWriter.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]; + // // 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 reconciledMessagesWriter.tryAddItem(newMessage.writeToBuffer()); - // Insert into local copy as well for this operation - existingMessages.add(newMessage); + // // Append to dht backing array + // await reconciledMessagesWriter.tryAddItem(newMessage.writeToBuffer()); + // // Insert into local copy as well for this operation + // existingMessages.add(newMessage); - nPos++; - } + // nPos++; + // } } // Produce a state for this cubit from the input cubits and queues void _renderState() { + // xxx move into a singlefuture + // Get all reconciled messages final reconciledMessages = _reconciledMessagesCubit?.state.state.asData?.value; @@ -307,14 +345,14 @@ class SingleContactMessagesCubit extends Cubit { // Generate state for each message final sentMessagesMap = - IMap>.fromValues( + IMap>.fromValues( keyMapper: (x) => x.value.timestamp, - values: sentMessages, + values: sentMessages.elements, ); final reconciledMessagesMap = - IMap>.fromValues( + IMap>.fromValues( keyMapper: (x) => x.value.timestamp, - values: reconciledMessages, + values: reconciledMessages.elements, ); final sendingMessagesMap = IMap.fromValues( keyMapper: (x) => x.timestamp, @@ -372,9 +410,8 @@ class SingleContactMessagesCubit extends Cubit { .sort((x, y) => x.key.compareTo(y.key)); final renderedState = messageKeys .map((x) => MessageState( - author: x.value.message.author.toVeilid(), + content: x.value.message, timestamp: Timestamp.fromInt64(x.key), - text: x.value.message.text, sendState: x.value.sendState)) .toIList(); @@ -400,17 +437,16 @@ class SingleContactMessagesCubit extends Cubit { final TypedKey _remoteMessagesRecordKey; final OwnedDHTRecordPointer _reconciledChatRecord; - late final DHTRecordCrypto _messagesCrypto; + late final VeilidCrypto _messagesCrypto; - DHTShortArrayCubit? _sentMessagesCubit; - DHTShortArrayCubit? _rcvdMessagesCubit; - DHTShortArrayCubit? _reconciledMessagesCubit; + DHTLogCubit? _sentMessagesCubit; + DHTLogCubit? _rcvdMessagesCubit; + DHTLogCubit? _reconciledMessagesCubit; late final PersistentQueue _unreconciledMessagesQueue; late final PersistentQueue _sendingMessagesQueue; - StreamSubscription>? _sentSubscription; - StreamSubscription>? _rcvdSubscription; - StreamSubscription>? - _reconciledSubscription; + StreamSubscription>? _sentSubscription; + StreamSubscription>? _rcvdSubscription; + StreamSubscription>? _reconciledSubscription; } diff --git a/lib/chat/models/message_state.dart b/lib/chat/models/message_state.dart index c4c6ca5..8618054 100644 --- a/lib/chat/models/message_state.dart +++ b/lib/chat/models/message_state.dart @@ -3,6 +3,8 @@ import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:veilid_support/veilid_support.dart'; +import '../../proto/proto.dart' as proto; + part 'message_state.freezed.dart'; part 'message_state.g.dart'; @@ -23,9 +25,12 @@ enum MessageSendState { @freezed class MessageState with _$MessageState { const factory MessageState({ - required TypedKey author, + // Content of the message + @JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + required proto.Message content, + // Received or delivered timestamp required Timestamp timestamp, - required String text, + // The state of the mssage required MessageSendState? sendState, }) = _MessageState; diff --git a/lib/chat/models/message_state.freezed.dart b/lib/chat/models/message_state.freezed.dart index 3d76551..ec8195b 100644 --- a/lib/chat/models/message_state.freezed.dart +++ b/lib/chat/models/message_state.freezed.dart @@ -20,9 +20,12 @@ MessageState _$MessageStateFromJson(Map json) { /// @nodoc mixin _$MessageState { - Typed get author => throw _privateConstructorUsedError; - Timestamp get timestamp => throw _privateConstructorUsedError; - String get text => throw _privateConstructorUsedError; +// Content of the message + @JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + proto.Message get content => + throw _privateConstructorUsedError; // Received or delivered timestamp + Timestamp get timestamp => + throw _privateConstructorUsedError; // The state of the mssage MessageSendState? get sendState => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @@ -38,9 +41,9 @@ abstract class $MessageStateCopyWith<$Res> { _$MessageStateCopyWithImpl<$Res, MessageState>; @useResult $Res call( - {Typed author, + {@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + proto.Message content, Timestamp timestamp, - String text, MessageSendState? sendState}); } @@ -57,24 +60,19 @@ class _$MessageStateCopyWithImpl<$Res, $Val extends MessageState> @pragma('vm:prefer-inline') @override $Res call({ - Object? author = null, + Object? content = null, Object? timestamp = null, - Object? text = null, Object? sendState = freezed, }) { return _then(_value.copyWith( - author: null == author - ? _value.author - : author // ignore: cast_nullable_to_non_nullable - as Typed, + content: null == content + ? _value.content + : content // ignore: cast_nullable_to_non_nullable + as proto.Message, timestamp: null == timestamp ? _value.timestamp : timestamp // ignore: cast_nullable_to_non_nullable as Timestamp, - text: null == text - ? _value.text - : text // ignore: cast_nullable_to_non_nullable - as String, sendState: freezed == sendState ? _value.sendState : sendState // ignore: cast_nullable_to_non_nullable @@ -92,9 +90,9 @@ abstract class _$$MessageStateImplCopyWith<$Res> @override @useResult $Res call( - {Typed author, + {@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + proto.Message content, Timestamp timestamp, - String text, MessageSendState? sendState}); } @@ -109,24 +107,19 @@ class __$$MessageStateImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? author = null, + Object? content = null, Object? timestamp = null, - Object? text = null, Object? sendState = freezed, }) { return _then(_$MessageStateImpl( - author: null == author - ? _value.author - : author // ignore: cast_nullable_to_non_nullable - as Typed, + content: null == content + ? _value.content + : content // ignore: cast_nullable_to_non_nullable + as proto.Message, timestamp: null == timestamp ? _value.timestamp : timestamp // ignore: cast_nullable_to_non_nullable as Timestamp, - text: null == text - ? _value.text - : text // ignore: cast_nullable_to_non_nullable - as String, sendState: freezed == sendState ? _value.sendState : sendState // ignore: cast_nullable_to_non_nullable @@ -139,26 +132,28 @@ class __$$MessageStateImplCopyWithImpl<$Res> @JsonSerializable() class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { const _$MessageStateImpl( - {required this.author, + {@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + required this.content, required this.timestamp, - required this.text, required this.sendState}); factory _$MessageStateImpl.fromJson(Map json) => _$$MessageStateImplFromJson(json); +// Content of the message @override - final Typed author; + @JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + final proto.Message content; +// Received or delivered timestamp @override final Timestamp timestamp; - @override - final String text; +// The state of the mssage @override final MessageSendState? sendState; @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'MessageState(author: $author, timestamp: $timestamp, text: $text, sendState: $sendState)'; + return 'MessageState(content: $content, timestamp: $timestamp, sendState: $sendState)'; } @override @@ -166,9 +161,8 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('type', 'MessageState')) - ..add(DiagnosticsProperty('author', author)) + ..add(DiagnosticsProperty('content', content)) ..add(DiagnosticsProperty('timestamp', timestamp)) - ..add(DiagnosticsProperty('text', text)) ..add(DiagnosticsProperty('sendState', sendState)); } @@ -177,18 +171,16 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { return identical(this, other) || (other.runtimeType == runtimeType && other is _$MessageStateImpl && - (identical(other.author, author) || other.author == author) && + (identical(other.content, content) || other.content == content) && (identical(other.timestamp, timestamp) || other.timestamp == timestamp) && - (identical(other.text, text) || other.text == text) && (identical(other.sendState, sendState) || other.sendState == sendState)); } @JsonKey(ignore: true) @override - int get hashCode => - Object.hash(runtimeType, author, timestamp, text, sendState); + int get hashCode => Object.hash(runtimeType, content, timestamp, sendState); @JsonKey(ignore: true) @override @@ -206,21 +198,20 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { abstract class _MessageState implements MessageState { const factory _MessageState( - {required final Typed author, + {@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + required final proto.Message content, required final Timestamp timestamp, - required final String text, required final MessageSendState? sendState}) = _$MessageStateImpl; factory _MessageState.fromJson(Map json) = _$MessageStateImpl.fromJson; - @override - Typed get author; - @override + @override // Content of the message + @JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + proto.Message get content; + @override // Received or delivered timestamp Timestamp get timestamp; - @override - String get text; - @override + @override // The state of the mssage MessageSendState? get sendState; @override @JsonKey(ignore: true) diff --git a/lib/chat/models/message_state.g.dart b/lib/chat/models/message_state.g.dart index 5324b93..3471720 100644 --- a/lib/chat/models/message_state.g.dart +++ b/lib/chat/models/message_state.g.dart @@ -8,9 +8,8 @@ part of 'message_state.dart'; _$MessageStateImpl _$$MessageStateImplFromJson(Map json) => _$MessageStateImpl( - author: Typed.fromJson(json['author']), + content: messageFromJson(json['content'] as Map), timestamp: Timestamp.fromJson(json['timestamp']), - text: json['text'] as String, sendState: json['send_state'] == null ? null : MessageSendState.fromJson(json['send_state']), @@ -18,8 +17,7 @@ _$MessageStateImpl _$$MessageStateImplFromJson(Map json) => Map _$$MessageStateImplToJson(_$MessageStateImpl instance) => { - 'author': instance.author.toJson(), + 'content': messageToJson(instance.content), 'timestamp': instance.timestamp.toJson(), - 'text': instance.text, 'send_state': instance.sendState?.toJson(), }; diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index 9204a0a..d04b008 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -65,7 +65,7 @@ class ChatListCubit extends DHTShortArrayCubit .userLogin.accountRecordInfo.accountRecord.recordKey; // Make a record that can store the reconciled version of the chat - final reconciledChatRecord = await (await DHTShortArray.create( + final reconciledChatRecord = await (await DHTLog.create( debugName: 'ChatListCubit::getOrCreateChatSingleContact::ReconciledChat', parent: accountRecordKey)) diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index afe91c0..b76eaee 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -121,7 +121,7 @@ class ContactInvitationListCubit schema: DHTSchema.smpl(oCnt: 1, members: [ DHTSchemaMember(mCnt: 1, mKey: contactRequestWriter.key) ]), - crypto: const DHTRecordCryptoPublic())) + crypto: const VeilidCryptoPublic())) .deleteScope((contactRequestInbox) async { // Store ContactRequest in owner subkey await contactRequestInbox.eventualWriteProtobuf(creq); @@ -129,7 +129,7 @@ class ContactInvitationListCubit await contactRequestInbox.eventualWriteBytes(Uint8List(0), subkey: 1, writer: contactRequestWriter, - crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair( + crypto: await VeilidCryptoPrivate.fromTypedKeyPair( TypedKeyPair.fromKeyPair( contactRequestInbox.key.kind, contactRequestWriter))); diff --git a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart index a376881..a4d0b8a 100644 --- a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart +++ b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart @@ -37,7 +37,7 @@ class ContactRequestInboxCubit return pool.openRecordRead(recordKey, debugName: 'ContactRequestInboxCubit::_open::' 'ContactRequestInbox', - crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer), + crypto: await VeilidCryptoPrivate.fromTypedKeyPair(writer), parent: accountRecordKey, defaultSubkey: 1); } diff --git a/lib/contacts/cubits/conversation_cubit.dart b/lib/contacts/cubits/conversation_cubit.dart index b4d8ee9..ef7e6ec 100644 --- a/lib/contacts/cubits/conversation_cubit.dart +++ b/lib/contacts/cubits/conversation_cubit.dart @@ -285,13 +285,13 @@ class ConversationCubit extends Cubit> { required ActiveAccountInfo activeAccountInfo, required TypedKey remoteIdentityPublicKey, required TypedKey localConversationKey, - required FutureOr Function(DHTShortArray) callback, + required FutureOr Function(DHTLog) callback, }) async { final crypto = await activeAccountInfo.makeConversationCrypto(remoteIdentityPublicKey); final writer = activeAccountInfo.conversationWriter; - return (await DHTShortArray.create( + return (await DHTLog.create( debugName: 'ConversationCubit::initLocalMessages::LocalMessages', parent: localConversationKey, crypto: crypto, @@ -327,7 +327,7 @@ class ConversationCubit extends Cubit> { return update; } - Future _cachedConversationCrypto() async { + Future _cachedConversationCrypto() async { var conversationCrypto = _conversationCrypto; if (conversationCrypto != null) { return conversationCrypto; @@ -350,6 +350,6 @@ class ConversationCubit extends Cubit> { ConversationState _incrementalState = const ConversationState( localConversation: null, remoteConversation: null); // - DHTRecordCrypto? _conversationCrypto; + VeilidCrypto? _conversationCrypto; final WaitSet _initWait = WaitSet(); } diff --git a/lib/proto/extensions.dart b/lib/proto/extensions.dart new file mode 100644 index 0000000..64fabf6 --- /dev/null +++ b/lib/proto/extensions.dart @@ -0,0 +1,12 @@ +import 'proto.dart' as proto; + +proto.Message messageFromJson(Map j) => + proto.Message.create()..mergeFromJsonMap(j); + +Map messageToJson(proto.Message m) => m.writeToJsonMap(); + +proto.ReconciledMessage reconciledMessageFromJson(Map j) => + proto.ReconciledMessage.create()..mergeFromJsonMap(j); + +Map reconciledMessageToJson(proto.ReconciledMessage m) => + m.writeToJsonMap(); diff --git a/lib/proto/proto.dart b/lib/proto/proto.dart index cfccda3..6ad8432 100644 --- a/lib/proto/proto.dart +++ b/lib/proto/proto.dart @@ -1,5 +1,7 @@ export 'package:veilid_support/dht_support/proto/proto.dart'; export 'package:veilid_support/proto/proto.dart'; + +export 'extensions.dart'; export 'veilidchat.pb.dart'; export 'veilidchat.pbenum.dart'; export 'veilidchat.pbjson.dart'; diff --git a/lib/proto/veilidchat.pb.dart b/lib/proto/veilidchat.pb.dart index 1503c57..164c117 100644 --- a/lib/proto/veilidchat.pb.dart +++ b/lib/proto/veilidchat.pb.dart @@ -14,24 +14,31 @@ import 'dart:core' as $core; import 'package:fixnum/fixnum.dart' as $fixnum; import 'package:protobuf/protobuf.dart' as $pb; -import 'package:veilid_support/proto/dht.pb.dart' as $0; -import 'package:veilid_support/proto/veilid.pb.dart' as $1; +import 'package:veilid_support/proto/dht.pb.dart' as $1; +import 'package:veilid_support/proto/veilid.pb.dart' as $0; import 'veilidchat.pbenum.dart'; export 'veilidchat.pbenum.dart'; +enum Attachment_Kind { + media, + notSet +} + 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); + static const $core.Map<$core.int, Attachment_Kind> _Attachment_KindByTag = { + 1 : Attachment_Kind.media, + 0 : Attachment_Kind.notSet + }; 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) + ..oo(0, [1]) + ..aOM(1, _omitFieldNames ? '' : 'media', subBuilder: AttachmentMedia.create) + ..aOM<$0.Signature>(2, _omitFieldNames ? '' : 'signature', subBuilder: $0.Signature.create) ..hasRequiredFields = false ; @@ -56,54 +63,684 @@ class Attachment extends $pb.GeneratedMessage { static Attachment getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Attachment? _defaultInstance; + Attachment_Kind whichKind() => _Attachment_KindByTag[$_whichOneof(0)]!; + void clearKind() => clearField($_whichOneof(0)); + @$pb.TagNumber(1) - AttachmentKind get kind => $_getN(0); + AttachmentMedia get media => $_getN(0); @$pb.TagNumber(1) - set kind(AttachmentKind v) { setField(1, v); } + set media(AttachmentMedia v) { setField(1, v); } @$pb.TagNumber(1) - $core.bool hasKind() => $_has(0); + $core.bool hasMedia() => $_has(0); @$pb.TagNumber(1) - void clearKind() => clearField(1); + void clearMedia() => clearField(1); + @$pb.TagNumber(1) + AttachmentMedia ensureMedia() => $_ensure(0); @$pb.TagNumber(2) - $core.String get mime => $_getSZ(1); + $0.Signature get signature => $_getN(1); @$pb.TagNumber(2) - set mime($core.String v) { $_setString(1, v); } + set signature($0.Signature v) { setField(2, v); } @$pb.TagNumber(2) - $core.bool hasMime() => $_has(1); + $core.bool hasSignature() => $_has(1); @$pb.TagNumber(2) - void clearMime() => clearField(2); + void clearSignature() => clearField(2); + @$pb.TagNumber(2) + $0.Signature ensureSignature() => $_ensure(1); +} + +class AttachmentMedia extends $pb.GeneratedMessage { + factory AttachmentMedia() => create(); + AttachmentMedia._() : super(); + factory AttachmentMedia.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory AttachmentMedia.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AttachmentMedia', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'mime') + ..aOS(2, _omitFieldNames ? '' : 'name') + ..aOM<$1.DataReference>(3, _omitFieldNames ? '' : 'content', subBuilder: $1.DataReference.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') + AttachmentMedia clone() => AttachmentMedia()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + AttachmentMedia copyWith(void Function(AttachmentMedia) updates) => super.copyWith((message) => updates(message as AttachmentMedia)) as AttachmentMedia; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static AttachmentMedia create() => AttachmentMedia._(); + AttachmentMedia createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static AttachmentMedia getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static AttachmentMedia? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get mime => $_getSZ(0); + @$pb.TagNumber(1) + set mime($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasMime() => $_has(0); + @$pb.TagNumber(1) + void clearMime() => clearField(1); + + @$pb.TagNumber(2) + $core.String get name => $_getSZ(1); + @$pb.TagNumber(2) + set name($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasName() => $_has(1); + @$pb.TagNumber(2) + void clearName() => clearField(2); @$pb.TagNumber(3) - $core.String get name => $_getSZ(2); + $1.DataReference get content => $_getN(2); @$pb.TagNumber(3) - set name($core.String v) { $_setString(2, v); } + set content($1.DataReference v) { setField(3, v); } @$pb.TagNumber(3) - $core.bool hasName() => $_has(2); + $core.bool hasContent() => $_has(2); @$pb.TagNumber(3) - void clearName() => clearField(3); + void clearContent() => clearField(3); + @$pb.TagNumber(3) + $1.DataReference ensureContent() => $_ensure(2); +} + +class Permissions extends $pb.GeneratedMessage { + factory Permissions() => create(); + Permissions._() : super(); + factory Permissions.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Permissions.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Permissions', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'canAddMembers', $pb.PbFieldType.OE, defaultOrMaker: Scope.WATCHERS, valueOf: Scope.valueOf, enumValues: Scope.values) + ..e(2, _omitFieldNames ? '' : 'canEditInfo', $pb.PbFieldType.OE, defaultOrMaker: Scope.WATCHERS, valueOf: Scope.valueOf, enumValues: Scope.values) + ..aOB(3, _omitFieldNames ? '' : 'moderated') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Permissions clone() => Permissions()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Permissions copyWith(void Function(Permissions) updates) => super.copyWith((message) => updates(message as Permissions)) as Permissions; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Permissions create() => Permissions._(); + Permissions createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Permissions getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Permissions? _defaultInstance; + + @$pb.TagNumber(1) + Scope get canAddMembers => $_getN(0); + @$pb.TagNumber(1) + set canAddMembers(Scope v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasCanAddMembers() => $_has(0); + @$pb.TagNumber(1) + void clearCanAddMembers() => clearField(1); + + @$pb.TagNumber(2) + Scope get canEditInfo => $_getN(1); + @$pb.TagNumber(2) + set canEditInfo(Scope v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasCanEditInfo() => $_has(1); + @$pb.TagNumber(2) + void clearCanEditInfo() => clearField(2); + + @$pb.TagNumber(3) + $core.bool get moderated => $_getBF(2); + @$pb.TagNumber(3) + set moderated($core.bool v) { $_setBool(2, v); } + @$pb.TagNumber(3) + $core.bool hasModerated() => $_has(2); + @$pb.TagNumber(3) + void clearModerated() => clearField(3); +} + +class Membership extends $pb.GeneratedMessage { + factory Membership() => create(); + Membership._() : super(); + factory Membership.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Membership.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Membership', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..pc<$0.TypedKey>(1, _omitFieldNames ? '' : 'watchers', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create) + ..pc<$0.TypedKey>(2, _omitFieldNames ? '' : 'moderated', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create) + ..pc<$0.TypedKey>(3, _omitFieldNames ? '' : 'talkers', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create) + ..pc<$0.TypedKey>(4, _omitFieldNames ? '' : 'moderators', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create) + ..pc<$0.TypedKey>(5, _omitFieldNames ? '' : 'admins', $pb.PbFieldType.PM, subBuilder: $0.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') + Membership clone() => Membership()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Membership copyWith(void Function(Membership) updates) => super.copyWith((message) => updates(message as Membership)) as Membership; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Membership create() => Membership._(); + Membership createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Membership getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Membership? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$0.TypedKey> get watchers => $_getList(0); + + @$pb.TagNumber(2) + $core.List<$0.TypedKey> get moderated => $_getList(1); + + @$pb.TagNumber(3) + $core.List<$0.TypedKey> get talkers => $_getList(2); @$pb.TagNumber(4) - $0.DataReference get content => $_getN(3); - @$pb.TagNumber(4) - set content($0.DataReference v) { setField(4, v); } - @$pb.TagNumber(4) - $core.bool hasContent() => $_has(3); - @$pb.TagNumber(4) - void clearContent() => clearField(4); - @$pb.TagNumber(4) - $0.DataReference ensureContent() => $_ensure(3); + $core.List<$0.TypedKey> get moderators => $_getList(3); @$pb.TagNumber(5) - $1.Signature get signature => $_getN(4); + $core.List<$0.TypedKey> get admins => $_getList(4); +} + +class ChatSettings extends $pb.GeneratedMessage { + factory ChatSettings() => create(); + ChatSettings._() : super(); + factory ChatSettings.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory ChatSettings.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ChatSettings', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'title') + ..aOS(2, _omitFieldNames ? '' : 'description') + ..aOM<$1.DataReference>(3, _omitFieldNames ? '' : 'icon', subBuilder: $1.DataReference.create) + ..a<$fixnum.Int64>(4, _omitFieldNames ? '' : 'defaultExpiration', $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') + ChatSettings clone() => ChatSettings()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ChatSettings copyWith(void Function(ChatSettings) updates) => super.copyWith((message) => updates(message as ChatSettings)) as ChatSettings; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ChatSettings create() => ChatSettings._(); + ChatSettings createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ChatSettings getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ChatSettings? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get title => $_getSZ(0); + @$pb.TagNumber(1) + set title($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasTitle() => $_has(0); + @$pb.TagNumber(1) + void clearTitle() => clearField(1); + + @$pb.TagNumber(2) + $core.String get description => $_getSZ(1); + @$pb.TagNumber(2) + set description($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasDescription() => $_has(1); + @$pb.TagNumber(2) + void clearDescription() => clearField(2); + + @$pb.TagNumber(3) + $1.DataReference get icon => $_getN(2); + @$pb.TagNumber(3) + set icon($1.DataReference v) { setField(3, v); } + @$pb.TagNumber(3) + $core.bool hasIcon() => $_has(2); + @$pb.TagNumber(3) + void clearIcon() => clearField(3); + @$pb.TagNumber(3) + $1.DataReference ensureIcon() => $_ensure(2); + + @$pb.TagNumber(4) + $fixnum.Int64 get defaultExpiration => $_getI64(3); + @$pb.TagNumber(4) + set defaultExpiration($fixnum.Int64 v) { $_setInt64(3, v); } + @$pb.TagNumber(4) + $core.bool hasDefaultExpiration() => $_has(3); + @$pb.TagNumber(4) + void clearDefaultExpiration() => clearField(4); +} + +class Message_Text extends $pb.GeneratedMessage { + factory Message_Text() => create(); + Message_Text._() : super(); + factory Message_Text.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Message_Text.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.Text', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'text') + ..aOS(2, _omitFieldNames ? '' : 'topic') + ..aOM<$0.TypedKey>(3, _omitFieldNames ? '' : 'replyId', subBuilder: $0.TypedKey.create) + ..a<$fixnum.Int64>(4, _omitFieldNames ? '' : 'expiration', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..a<$fixnum.Int64>(5, _omitFieldNames ? '' : 'viewLimit', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..pc(6, _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') + Message_Text clone() => Message_Text()..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_Text copyWith(void Function(Message_Text) updates) => super.copyWith((message) => updates(message as Message_Text)) as Message_Text; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Message_Text create() => Message_Text._(); + Message_Text createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Message_Text getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Message_Text? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get text => $_getSZ(0); + @$pb.TagNumber(1) + set text($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasText() => $_has(0); + @$pb.TagNumber(1) + void clearText() => clearField(1); + + @$pb.TagNumber(2) + $core.String get topic => $_getSZ(1); + @$pb.TagNumber(2) + set topic($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasTopic() => $_has(1); + @$pb.TagNumber(2) + void clearTopic() => clearField(2); + + @$pb.TagNumber(3) + $0.TypedKey get replyId => $_getN(2); + @$pb.TagNumber(3) + set replyId($0.TypedKey v) { setField(3, v); } + @$pb.TagNumber(3) + $core.bool hasReplyId() => $_has(2); + @$pb.TagNumber(3) + void clearReplyId() => clearField(3); + @$pb.TagNumber(3) + $0.TypedKey ensureReplyId() => $_ensure(2); + + @$pb.TagNumber(4) + $fixnum.Int64 get expiration => $_getI64(3); + @$pb.TagNumber(4) + set expiration($fixnum.Int64 v) { $_setInt64(3, v); } + @$pb.TagNumber(4) + $core.bool hasExpiration() => $_has(3); + @$pb.TagNumber(4) + void clearExpiration() => clearField(4); + @$pb.TagNumber(5) - set signature($1.Signature v) { setField(5, v); } + $fixnum.Int64 get viewLimit => $_getI64(4); @$pb.TagNumber(5) - $core.bool hasSignature() => $_has(4); + set viewLimit($fixnum.Int64 v) { $_setInt64(4, v); } @$pb.TagNumber(5) - void clearSignature() => clearField(5); + $core.bool hasViewLimit() => $_has(4); @$pb.TagNumber(5) - $1.Signature ensureSignature() => $_ensure(4); + void clearViewLimit() => clearField(5); + + @$pb.TagNumber(6) + $core.List get attachments => $_getList(5); +} + +class Message_Secret extends $pb.GeneratedMessage { + factory Message_Secret() => create(); + Message_Secret._() : super(); + factory Message_Secret.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Message_Secret.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.Secret', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'ciphertext', $pb.PbFieldType.OY) + ..a<$fixnum.Int64>(2, _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') + Message_Secret clone() => Message_Secret()..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_Secret copyWith(void Function(Message_Secret) updates) => super.copyWith((message) => updates(message as Message_Secret)) as Message_Secret; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Message_Secret create() => Message_Secret._(); + Message_Secret createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Message_Secret getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Message_Secret? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get ciphertext => $_getN(0); + @$pb.TagNumber(1) + set ciphertext($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasCiphertext() => $_has(0); + @$pb.TagNumber(1) + void clearCiphertext() => clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get expiration => $_getI64(1); + @$pb.TagNumber(2) + set expiration($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(2) + $core.bool hasExpiration() => $_has(1); + @$pb.TagNumber(2) + void clearExpiration() => clearField(2); +} + +class Message_ControlDelete extends $pb.GeneratedMessage { + factory Message_ControlDelete() => create(); + Message_ControlDelete._() : super(); + factory Message_ControlDelete.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Message_ControlDelete.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.ControlDelete', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..pc<$0.TypedKey>(1, _omitFieldNames ? '' : 'ids', $pb.PbFieldType.PM, subBuilder: $0.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') + Message_ControlDelete clone() => Message_ControlDelete()..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_ControlDelete copyWith(void Function(Message_ControlDelete) updates) => super.copyWith((message) => updates(message as Message_ControlDelete)) as Message_ControlDelete; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Message_ControlDelete create() => Message_ControlDelete._(); + Message_ControlDelete createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Message_ControlDelete getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Message_ControlDelete? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$0.TypedKey> get ids => $_getList(0); +} + +class Message_ControlClear extends $pb.GeneratedMessage { + factory Message_ControlClear() => create(); + Message_ControlClear._() : super(); + factory Message_ControlClear.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Message_ControlClear.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.ControlClear', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..a<$fixnum.Int64>(1, _omitFieldNames ? '' : 'timestamp', $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') + Message_ControlClear clone() => Message_ControlClear()..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_ControlClear copyWith(void Function(Message_ControlClear) updates) => super.copyWith((message) => updates(message as Message_ControlClear)) as Message_ControlClear; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Message_ControlClear create() => Message_ControlClear._(); + Message_ControlClear createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Message_ControlClear getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Message_ControlClear? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get timestamp => $_getI64(0); + @$pb.TagNumber(1) + set timestamp($fixnum.Int64 v) { $_setInt64(0, v); } + @$pb.TagNumber(1) + $core.bool hasTimestamp() => $_has(0); + @$pb.TagNumber(1) + void clearTimestamp() => clearField(1); +} + +class Message_ControlSettings extends $pb.GeneratedMessage { + factory Message_ControlSettings() => create(); + Message_ControlSettings._() : super(); + factory Message_ControlSettings.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Message_ControlSettings.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.ControlSettings', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'settings', subBuilder: ChatSettings.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') + Message_ControlSettings clone() => Message_ControlSettings()..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_ControlSettings copyWith(void Function(Message_ControlSettings) updates) => super.copyWith((message) => updates(message as Message_ControlSettings)) as Message_ControlSettings; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Message_ControlSettings create() => Message_ControlSettings._(); + Message_ControlSettings createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Message_ControlSettings getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Message_ControlSettings? _defaultInstance; + + @$pb.TagNumber(1) + ChatSettings get settings => $_getN(0); + @$pb.TagNumber(1) + set settings(ChatSettings v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasSettings() => $_has(0); + @$pb.TagNumber(1) + void clearSettings() => clearField(1); + @$pb.TagNumber(1) + ChatSettings ensureSettings() => $_ensure(0); +} + +class Message_ControlPermissions extends $pb.GeneratedMessage { + factory Message_ControlPermissions() => create(); + Message_ControlPermissions._() : super(); + factory Message_ControlPermissions.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Message_ControlPermissions.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.ControlPermissions', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'permissions', subBuilder: Permissions.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') + Message_ControlPermissions clone() => Message_ControlPermissions()..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_ControlPermissions copyWith(void Function(Message_ControlPermissions) updates) => super.copyWith((message) => updates(message as Message_ControlPermissions)) as Message_ControlPermissions; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Message_ControlPermissions create() => Message_ControlPermissions._(); + Message_ControlPermissions createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Message_ControlPermissions getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Message_ControlPermissions? _defaultInstance; + + @$pb.TagNumber(1) + Permissions get permissions => $_getN(0); + @$pb.TagNumber(1) + set permissions(Permissions v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasPermissions() => $_has(0); + @$pb.TagNumber(1) + void clearPermissions() => clearField(1); + @$pb.TagNumber(1) + Permissions ensurePermissions() => $_ensure(0); +} + +class Message_ControlMembership extends $pb.GeneratedMessage { + factory Message_ControlMembership() => create(); + Message_ControlMembership._() : super(); + factory Message_ControlMembership.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Message_ControlMembership.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.ControlMembership', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'membership', subBuilder: Membership.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') + Message_ControlMembership clone() => Message_ControlMembership()..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_ControlMembership copyWith(void Function(Message_ControlMembership) updates) => super.copyWith((message) => updates(message as Message_ControlMembership)) as Message_ControlMembership; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Message_ControlMembership create() => Message_ControlMembership._(); + Message_ControlMembership createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Message_ControlMembership getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Message_ControlMembership? _defaultInstance; + + @$pb.TagNumber(1) + Membership get membership => $_getN(0); + @$pb.TagNumber(1) + set membership(Membership v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasMembership() => $_has(0); + @$pb.TagNumber(1) + void clearMembership() => clearField(1); + @$pb.TagNumber(1) + Membership ensureMembership() => $_ensure(0); +} + +class Message_ControlModeration extends $pb.GeneratedMessage { + factory Message_ControlModeration() => create(); + Message_ControlModeration._() : super(); + factory Message_ControlModeration.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Message_ControlModeration.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.ControlModeration', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..pc<$0.TypedKey>(1, _omitFieldNames ? '' : 'acceptedIds', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create) + ..pc<$0.TypedKey>(2, _omitFieldNames ? '' : 'rejectedIds', $pb.PbFieldType.PM, subBuilder: $0.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') + Message_ControlModeration clone() => Message_ControlModeration()..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_ControlModeration copyWith(void Function(Message_ControlModeration) updates) => super.copyWith((message) => updates(message as Message_ControlModeration)) as Message_ControlModeration; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Message_ControlModeration create() => Message_ControlModeration._(); + Message_ControlModeration createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Message_ControlModeration getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Message_ControlModeration? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$0.TypedKey> get acceptedIds => $_getList(0); + + @$pb.TagNumber(2) + $core.List<$0.TypedKey> get rejectedIds => $_getList(1); +} + +enum Message_Kind { + text, + secret, + delete, + clear_7, + settings, + permissions, + membership, + moderation, + notSet } class Message extends $pb.GeneratedMessage { @@ -112,12 +749,31 @@ class Message extends $pb.GeneratedMessage { 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 const $core.Map<$core.int, Message_Kind> _Message_KindByTag = { + 4 : Message_Kind.text, + 5 : Message_Kind.secret, + 6 : Message_Kind.delete, + 7 : Message_Kind.clear_7, + 8 : Message_Kind.settings, + 9 : Message_Kind.permissions, + 10 : Message_Kind.membership, + 11 : Message_Kind.moderation, + 0 : Message_Kind.notSet + }; 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) + ..oo(0, [4, 5, 6, 7, 8, 9, 10, 11]) + ..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'id', subBuilder: $0.TypedKey.create) + ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'author', subBuilder: $0.TypedKey.create) + ..a<$fixnum.Int64>(3, _omitFieldNames ? '' : 'timestamp', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..aOM(4, _omitFieldNames ? '' : 'text', subBuilder: Message_Text.create) + ..aOM(5, _omitFieldNames ? '' : 'secret', subBuilder: Message_Secret.create) + ..aOM(6, _omitFieldNames ? '' : 'delete', subBuilder: Message_ControlDelete.create) + ..aOM(7, _omitFieldNames ? '' : 'clear', subBuilder: Message_ControlClear.create) + ..aOM(8, _omitFieldNames ? '' : 'settings', subBuilder: Message_ControlSettings.create) + ..aOM(9, _omitFieldNames ? '' : 'permissions', subBuilder: Message_ControlPermissions.create) + ..aOM(10, _omitFieldNames ? '' : 'membership', subBuilder: Message_ControlMembership.create) + ..aOM(11, _omitFieldNames ? '' : 'moderation', subBuilder: Message_ControlModeration.create) + ..aOM<$0.Signature>(12, _omitFieldNames ? '' : 'signature', subBuilder: $0.Signature.create) ..hasRequiredFields = false ; @@ -142,48 +798,192 @@ class Message extends $pb.GeneratedMessage { static Message getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Message? _defaultInstance; + Message_Kind whichKind() => _Message_KindByTag[$_whichOneof(0)]!; + void clearKind() => clearField($_whichOneof(0)); + @$pb.TagNumber(1) - $1.TypedKey get author => $_getN(0); + $0.TypedKey get id => $_getN(0); @$pb.TagNumber(1) - set author($1.TypedKey v) { setField(1, v); } + set id($0.TypedKey v) { setField(1, v); } @$pb.TagNumber(1) - $core.bool hasAuthor() => $_has(0); + $core.bool hasId() => $_has(0); @$pb.TagNumber(1) - void clearAuthor() => clearField(1); + void clearId() => clearField(1); @$pb.TagNumber(1) - $1.TypedKey ensureAuthor() => $_ensure(0); + $0.TypedKey ensureId() => $_ensure(0); @$pb.TagNumber(2) - $fixnum.Int64 get timestamp => $_getI64(1); + $0.TypedKey get author => $_getN(1); @$pb.TagNumber(2) - set timestamp($fixnum.Int64 v) { $_setInt64(1, v); } + set author($0.TypedKey v) { setField(2, v); } @$pb.TagNumber(2) - $core.bool hasTimestamp() => $_has(1); + $core.bool hasAuthor() => $_has(1); @$pb.TagNumber(2) - void clearTimestamp() => clearField(2); + void clearAuthor() => clearField(2); + @$pb.TagNumber(2) + $0.TypedKey ensureAuthor() => $_ensure(1); @$pb.TagNumber(3) - $core.String get text => $_getSZ(2); + $fixnum.Int64 get timestamp => $_getI64(2); @$pb.TagNumber(3) - set text($core.String v) { $_setString(2, v); } + set timestamp($fixnum.Int64 v) { $_setInt64(2, v); } @$pb.TagNumber(3) - $core.bool hasText() => $_has(2); + $core.bool hasTimestamp() => $_has(2); @$pb.TagNumber(3) - void clearText() => clearField(3); + void clearTimestamp() => clearField(3); @$pb.TagNumber(4) - $1.Signature get signature => $_getN(3); + Message_Text get text => $_getN(3); @$pb.TagNumber(4) - set signature($1.Signature v) { setField(4, v); } + set text(Message_Text v) { setField(4, v); } @$pb.TagNumber(4) - $core.bool hasSignature() => $_has(3); + $core.bool hasText() => $_has(3); @$pb.TagNumber(4) - void clearSignature() => clearField(4); + void clearText() => clearField(4); @$pb.TagNumber(4) - $1.Signature ensureSignature() => $_ensure(3); + Message_Text ensureText() => $_ensure(3); @$pb.TagNumber(5) - $core.List get attachments => $_getList(4); + Message_Secret get secret => $_getN(4); + @$pb.TagNumber(5) + set secret(Message_Secret v) { setField(5, v); } + @$pb.TagNumber(5) + $core.bool hasSecret() => $_has(4); + @$pb.TagNumber(5) + void clearSecret() => clearField(5); + @$pb.TagNumber(5) + Message_Secret ensureSecret() => $_ensure(4); + + @$pb.TagNumber(6) + Message_ControlDelete get delete => $_getN(5); + @$pb.TagNumber(6) + set delete(Message_ControlDelete v) { setField(6, v); } + @$pb.TagNumber(6) + $core.bool hasDelete() => $_has(5); + @$pb.TagNumber(6) + void clearDelete() => clearField(6); + @$pb.TagNumber(6) + Message_ControlDelete ensureDelete() => $_ensure(5); + + @$pb.TagNumber(7) + Message_ControlClear get clear_7 => $_getN(6); + @$pb.TagNumber(7) + set clear_7(Message_ControlClear v) { setField(7, v); } + @$pb.TagNumber(7) + $core.bool hasClear_7() => $_has(6); + @$pb.TagNumber(7) + void clearClear_7() => clearField(7); + @$pb.TagNumber(7) + Message_ControlClear ensureClear_7() => $_ensure(6); + + @$pb.TagNumber(8) + Message_ControlSettings get settings => $_getN(7); + @$pb.TagNumber(8) + set settings(Message_ControlSettings v) { setField(8, v); } + @$pb.TagNumber(8) + $core.bool hasSettings() => $_has(7); + @$pb.TagNumber(8) + void clearSettings() => clearField(8); + @$pb.TagNumber(8) + Message_ControlSettings ensureSettings() => $_ensure(7); + + @$pb.TagNumber(9) + Message_ControlPermissions get permissions => $_getN(8); + @$pb.TagNumber(9) + set permissions(Message_ControlPermissions v) { setField(9, v); } + @$pb.TagNumber(9) + $core.bool hasPermissions() => $_has(8); + @$pb.TagNumber(9) + void clearPermissions() => clearField(9); + @$pb.TagNumber(9) + Message_ControlPermissions ensurePermissions() => $_ensure(8); + + @$pb.TagNumber(10) + Message_ControlMembership get membership => $_getN(9); + @$pb.TagNumber(10) + set membership(Message_ControlMembership v) { setField(10, v); } + @$pb.TagNumber(10) + $core.bool hasMembership() => $_has(9); + @$pb.TagNumber(10) + void clearMembership() => clearField(10); + @$pb.TagNumber(10) + Message_ControlMembership ensureMembership() => $_ensure(9); + + @$pb.TagNumber(11) + Message_ControlModeration get moderation => $_getN(10); + @$pb.TagNumber(11) + set moderation(Message_ControlModeration v) { setField(11, v); } + @$pb.TagNumber(11) + $core.bool hasModeration() => $_has(10); + @$pb.TagNumber(11) + void clearModeration() => clearField(11); + @$pb.TagNumber(11) + Message_ControlModeration ensureModeration() => $_ensure(10); + + @$pb.TagNumber(12) + $0.Signature get signature => $_getN(11); + @$pb.TagNumber(12) + set signature($0.Signature v) { setField(12, v); } + @$pb.TagNumber(12) + $core.bool hasSignature() => $_has(11); + @$pb.TagNumber(12) + void clearSignature() => clearField(12); + @$pb.TagNumber(12) + $0.Signature ensureSignature() => $_ensure(11); +} + +class ReconciledMessage extends $pb.GeneratedMessage { + factory ReconciledMessage() => create(); + ReconciledMessage._() : super(); + factory ReconciledMessage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory ReconciledMessage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ReconciledMessage', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'content', subBuilder: Message.create) + ..a<$fixnum.Int64>(2, _omitFieldNames ? '' : 'reconciledTime', $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') + ReconciledMessage clone() => ReconciledMessage()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ReconciledMessage copyWith(void Function(ReconciledMessage) updates) => super.copyWith((message) => updates(message as ReconciledMessage)) as ReconciledMessage; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ReconciledMessage create() => ReconciledMessage._(); + ReconciledMessage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ReconciledMessage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ReconciledMessage? _defaultInstance; + + @$pb.TagNumber(1) + Message get content => $_getN(0); + @$pb.TagNumber(1) + set content(Message v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasContent() => $_has(0); + @$pb.TagNumber(1) + void clearContent() => clearField(1); + @$pb.TagNumber(1) + Message ensureContent() => $_ensure(0); + + @$pb.TagNumber(2) + $fixnum.Int64 get reconciledTime => $_getI64(1); + @$pb.TagNumber(2) + set reconciledTime($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(2) + $core.bool hasReconciledTime() => $_has(1); + @$pb.TagNumber(2) + void clearReconciledTime() => clearField(2); } class Conversation extends $pb.GeneratedMessage { @@ -195,7 +995,7 @@ class Conversation extends $pb.GeneratedMessage { 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) + ..aOM<$0.TypedKey>(3, _omitFieldNames ? '' : 'messages', subBuilder: $0.TypedKey.create) ..hasRequiredFields = false ; @@ -241,15 +1041,349 @@ class Conversation extends $pb.GeneratedMessage { void clearIdentityMasterJson() => clearField(2); @$pb.TagNumber(3) - $1.TypedKey get messages => $_getN(2); + $0.TypedKey get messages => $_getN(2); @$pb.TagNumber(3) - set messages($1.TypedKey v) { setField(3, v); } + set messages($0.TypedKey v) { setField(3, v); } @$pb.TagNumber(3) $core.bool hasMessages() => $_has(2); @$pb.TagNumber(3) void clearMessages() => clearField(3); @$pb.TagNumber(3) - $1.TypedKey ensureMessages() => $_ensure(2); + $0.TypedKey ensureMessages() => $_ensure(2); +} + +class Chat extends $pb.GeneratedMessage { + factory Chat() => create(); + Chat._() : super(); + factory Chat.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Chat.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Chat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'settings', subBuilder: ChatSettings.create) + ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create) + ..aOM<$0.TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Chat clone() => Chat()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Chat copyWith(void Function(Chat) updates) => super.copyWith((message) => updates(message as Chat)) as Chat; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Chat create() => Chat._(); + Chat createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Chat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Chat? _defaultInstance; + + @$pb.TagNumber(1) + ChatSettings get settings => $_getN(0); + @$pb.TagNumber(1) + set settings(ChatSettings v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasSettings() => $_has(0); + @$pb.TagNumber(1) + void clearSettings() => clearField(1); + @$pb.TagNumber(1) + ChatSettings ensureSettings() => $_ensure(0); + + @$pb.TagNumber(2) + $0.TypedKey get localConversationRecordKey => $_getN(1); + @$pb.TagNumber(2) + set localConversationRecordKey($0.TypedKey v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasLocalConversationRecordKey() => $_has(1); + @$pb.TagNumber(2) + void clearLocalConversationRecordKey() => clearField(2); + @$pb.TagNumber(2) + $0.TypedKey ensureLocalConversationRecordKey() => $_ensure(1); + + @$pb.TagNumber(3) + $0.TypedKey get remoteConversationRecordKey => $_getN(2); + @$pb.TagNumber(3) + set remoteConversationRecordKey($0.TypedKey v) { setField(3, v); } + @$pb.TagNumber(3) + $core.bool hasRemoteConversationRecordKey() => $_has(2); + @$pb.TagNumber(3) + void clearRemoteConversationRecordKey() => clearField(3); + @$pb.TagNumber(3) + $0.TypedKey ensureRemoteConversationRecordKey() => $_ensure(2); +} + +class GroupChat extends $pb.GeneratedMessage { + factory GroupChat() => create(); + GroupChat._() : super(); + factory GroupChat.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory GroupChat.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'GroupChat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'settings', subBuilder: ChatSettings.create) + ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create) + ..pc<$0.TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationRecordKeys', $pb.PbFieldType.PM, subBuilder: $0.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') + GroupChat clone() => GroupChat()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + GroupChat copyWith(void Function(GroupChat) updates) => super.copyWith((message) => updates(message as GroupChat)) as GroupChat; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static GroupChat create() => GroupChat._(); + GroupChat createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static GroupChat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static GroupChat? _defaultInstance; + + @$pb.TagNumber(1) + ChatSettings get settings => $_getN(0); + @$pb.TagNumber(1) + set settings(ChatSettings v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasSettings() => $_has(0); + @$pb.TagNumber(1) + void clearSettings() => clearField(1); + @$pb.TagNumber(1) + ChatSettings ensureSettings() => $_ensure(0); + + @$pb.TagNumber(2) + $0.TypedKey get localConversationRecordKey => $_getN(1); + @$pb.TagNumber(2) + set localConversationRecordKey($0.TypedKey v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasLocalConversationRecordKey() => $_has(1); + @$pb.TagNumber(2) + void clearLocalConversationRecordKey() => clearField(2); + @$pb.TagNumber(2) + $0.TypedKey ensureLocalConversationRecordKey() => $_ensure(1); + + @$pb.TagNumber(3) + $core.List<$0.TypedKey> get remoteConversationRecordKeys => $_getList(2); +} + +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); + + 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 ? '' : 'about') + ..aOS(4, _omitFieldNames ? '' : 'status') + ..e(5, _omitFieldNames ? '' : 'availability', $pb.PbFieldType.OE, defaultOrMaker: Availability.AVAILABILITY_UNSPECIFIED, valueOf: Availability.valueOf, enumValues: Availability.values) + ..aOM<$0.TypedKey>(6, _omitFieldNames ? '' : 'avatar', subBuilder: $0.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') + 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; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Profile create() => Profile._(); + Profile createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + 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); } + @$pb.TagNumber(1) + $core.bool hasName() => $_has(0); + @$pb.TagNumber(1) + void clearName() => clearField(1); + + @$pb.TagNumber(2) + $core.String get pronouns => $_getSZ(1); + @$pb.TagNumber(2) + set pronouns($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasPronouns() => $_has(1); + @$pb.TagNumber(2) + void clearPronouns() => clearField(2); + + @$pb.TagNumber(3) + $core.String get about => $_getSZ(2); + @$pb.TagNumber(3) + set about($core.String v) { $_setString(2, v); } + @$pb.TagNumber(3) + $core.bool hasAbout() => $_has(2); + @$pb.TagNumber(3) + void clearAbout() => clearField(3); + + @$pb.TagNumber(4) + $core.String get status => $_getSZ(3); + @$pb.TagNumber(4) + set status($core.String v) { $_setString(3, v); } + @$pb.TagNumber(4) + $core.bool hasStatus() => $_has(3); + @$pb.TagNumber(4) + void clearStatus() => clearField(4); + + @$pb.TagNumber(5) + Availability get availability => $_getN(4); + @$pb.TagNumber(5) + set availability(Availability v) { setField(5, v); } + @$pb.TagNumber(5) + $core.bool hasAvailability() => $_has(4); + @$pb.TagNumber(5) + void clearAvailability() => clearField(5); + + @$pb.TagNumber(6) + $0.TypedKey get avatar => $_getN(5); + @$pb.TagNumber(6) + set avatar($0.TypedKey v) { setField(6, v); } + @$pb.TagNumber(6) + $core.bool hasAvatar() => $_has(5); + @$pb.TagNumber(6) + void clearAvatar() => clearField(6); + @$pb.TagNumber(6) + $0.TypedKey ensureAvatar() => $_ensure(5); +} + +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); + + 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<$1.OwnedDHTRecordPointer>(4, _omitFieldNames ? '' : 'contactList', subBuilder: $1.OwnedDHTRecordPointer.create) + ..aOM<$1.OwnedDHTRecordPointer>(5, _omitFieldNames ? '' : 'contactInvitationRecords', subBuilder: $1.OwnedDHTRecordPointer.create) + ..aOM<$1.OwnedDHTRecordPointer>(6, _omitFieldNames ? '' : 'chatList', subBuilder: $1.OwnedDHTRecordPointer.create) + ..aOM<$1.OwnedDHTRecordPointer>(7, _omitFieldNames ? '' : 'groupChatList', subBuilder: $1.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') + 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; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Account create() => Account._(); + Account createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + 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); } + @$pb.TagNumber(1) + $core.bool hasProfile() => $_has(0); + @$pb.TagNumber(1) + void clearProfile() => clearField(1); + @$pb.TagNumber(1) + Profile ensureProfile() => $_ensure(0); + + @$pb.TagNumber(2) + $core.bool get invisible => $_getBF(1); + @$pb.TagNumber(2) + set invisible($core.bool v) { $_setBool(1, v); } + @$pb.TagNumber(2) + $core.bool hasInvisible() => $_has(1); + @$pb.TagNumber(2) + void clearInvisible() => clearField(2); + + @$pb.TagNumber(3) + $core.int get autoAwayTimeoutSec => $_getIZ(2); + @$pb.TagNumber(3) + set autoAwayTimeoutSec($core.int v) { $_setUnsignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasAutoAwayTimeoutSec() => $_has(2); + @$pb.TagNumber(3) + void clearAutoAwayTimeoutSec() => clearField(3); + + @$pb.TagNumber(4) + $1.OwnedDHTRecordPointer get contactList => $_getN(3); + @$pb.TagNumber(4) + set contactList($1.OwnedDHTRecordPointer v) { setField(4, v); } + @$pb.TagNumber(4) + $core.bool hasContactList() => $_has(3); + @$pb.TagNumber(4) + void clearContactList() => clearField(4); + @$pb.TagNumber(4) + $1.OwnedDHTRecordPointer ensureContactList() => $_ensure(3); + + @$pb.TagNumber(5) + $1.OwnedDHTRecordPointer get contactInvitationRecords => $_getN(4); + @$pb.TagNumber(5) + set contactInvitationRecords($1.OwnedDHTRecordPointer v) { setField(5, v); } + @$pb.TagNumber(5) + $core.bool hasContactInvitationRecords() => $_has(4); + @$pb.TagNumber(5) + void clearContactInvitationRecords() => clearField(5); + @$pb.TagNumber(5) + $1.OwnedDHTRecordPointer ensureContactInvitationRecords() => $_ensure(4); + + @$pb.TagNumber(6) + $1.OwnedDHTRecordPointer get chatList => $_getN(5); + @$pb.TagNumber(6) + set chatList($1.OwnedDHTRecordPointer v) { setField(6, v); } + @$pb.TagNumber(6) + $core.bool hasChatList() => $_has(5); + @$pb.TagNumber(6) + void clearChatList() => clearField(6); + @$pb.TagNumber(6) + $1.OwnedDHTRecordPointer ensureChatList() => $_ensure(5); + + @$pb.TagNumber(7) + $1.OwnedDHTRecordPointer get groupChatList => $_getN(6); + @$pb.TagNumber(7) + set groupChatList($1.OwnedDHTRecordPointer v) { setField(7, v); } + @$pb.TagNumber(7) + $core.bool hasGroupChatList() => $_has(6); + @$pb.TagNumber(7) + void clearGroupChatList() => clearField(7); + @$pb.TagNumber(7) + $1.OwnedDHTRecordPointer ensureGroupChatList() => $_ensure(6); } class Contact extends $pb.GeneratedMessage { @@ -262,9 +1396,9 @@ class Contact extends $pb.GeneratedMessage { ..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<$0.TypedKey>(4, _omitFieldNames ? '' : 'identityPublicKey', subBuilder: $0.TypedKey.create) + ..aOM<$0.TypedKey>(5, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create) + ..aOM<$0.TypedKey>(6, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create) ..aOB(7, _omitFieldNames ? '' : 'showAvailability') ..hasRequiredFields = false ; @@ -322,37 +1456,37 @@ class Contact extends $pb.GeneratedMessage { void clearIdentityMasterJson() => clearField(3); @$pb.TagNumber(4) - $1.TypedKey get identityPublicKey => $_getN(3); + $0.TypedKey get identityPublicKey => $_getN(3); @$pb.TagNumber(4) - set identityPublicKey($1.TypedKey v) { setField(4, v); } + set identityPublicKey($0.TypedKey v) { setField(4, v); } @$pb.TagNumber(4) $core.bool hasIdentityPublicKey() => $_has(3); @$pb.TagNumber(4) void clearIdentityPublicKey() => clearField(4); @$pb.TagNumber(4) - $1.TypedKey ensureIdentityPublicKey() => $_ensure(3); + $0.TypedKey ensureIdentityPublicKey() => $_ensure(3); @$pb.TagNumber(5) - $1.TypedKey get remoteConversationRecordKey => $_getN(4); + $0.TypedKey get remoteConversationRecordKey => $_getN(4); @$pb.TagNumber(5) - set remoteConversationRecordKey($1.TypedKey v) { setField(5, v); } + set remoteConversationRecordKey($0.TypedKey v) { setField(5, v); } @$pb.TagNumber(5) $core.bool hasRemoteConversationRecordKey() => $_has(4); @$pb.TagNumber(5) void clearRemoteConversationRecordKey() => clearField(5); @$pb.TagNumber(5) - $1.TypedKey ensureRemoteConversationRecordKey() => $_ensure(4); + $0.TypedKey ensureRemoteConversationRecordKey() => $_ensure(4); @$pb.TagNumber(6) - $1.TypedKey get localConversationRecordKey => $_getN(5); + $0.TypedKey get localConversationRecordKey => $_getN(5); @$pb.TagNumber(6) - set localConversationRecordKey($1.TypedKey v) { setField(6, v); } + set localConversationRecordKey($0.TypedKey v) { setField(6, v); } @$pb.TagNumber(6) $core.bool hasLocalConversationRecordKey() => $_has(5); @$pb.TagNumber(6) void clearLocalConversationRecordKey() => clearField(6); @$pb.TagNumber(6) - $1.TypedKey ensureLocalConversationRecordKey() => $_ensure(5); + $0.TypedKey ensureLocalConversationRecordKey() => $_ensure(5); @$pb.TagNumber(7) $core.bool get showAvailability => $_getBF(6); @@ -364,256 +1498,6 @@ class Contact extends $pb.GeneratedMessage { void clearShowAvailability() => clearField(7); } -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); - - 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 - ; - - @$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; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static Profile create() => Profile._(); - Profile createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - 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); } - @$pb.TagNumber(1) - $core.bool hasName() => $_has(0); - @$pb.TagNumber(1) - void clearName() => clearField(1); - - @$pb.TagNumber(2) - $core.String get pronouns => $_getSZ(1); - @$pb.TagNumber(2) - set pronouns($core.String v) { $_setString(1, v); } - @$pb.TagNumber(2) - $core.bool hasPronouns() => $_has(1); - @$pb.TagNumber(2) - void clearPronouns() => clearField(2); - - @$pb.TagNumber(3) - $core.String get status => $_getSZ(2); - @$pb.TagNumber(3) - set status($core.String v) { $_setString(2, v); } - @$pb.TagNumber(3) - $core.bool hasStatus() => $_has(2); - @$pb.TagNumber(3) - void clearStatus() => clearField(3); - - @$pb.TagNumber(4) - Availability get availability => $_getN(3); - @$pb.TagNumber(4) - set availability(Availability v) { setField(4, v); } - @$pb.TagNumber(4) - $core.bool hasAvailability() => $_has(3); - @$pb.TagNumber(4) - void clearAvailability() => clearField(4); - - @$pb.TagNumber(5) - $1.TypedKey get avatar => $_getN(4); - @$pb.TagNumber(5) - set avatar($1.TypedKey v) { setField(5, v); } - @$pb.TagNumber(5) - $core.bool hasAvatar() => $_has(4); - @$pb.TagNumber(5) - void clearAvatar() => clearField(5); - @$pb.TagNumber(5) - $1.TypedKey ensureAvatar() => $_ensure(4); -} - -class Chat extends $pb.GeneratedMessage { - factory Chat() => create(); - Chat._() : super(); - factory Chat.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory Chat.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Chat', 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 ? '' : 'remoteConversationRecordKey', subBuilder: $1.TypedKey.create) - ..aOM<$0.OwnedDHTRecordPointer>(3, _omitFieldNames ? '' : 'reconciledChatRecord', 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') - Chat clone() => Chat()..mergeFromMessage(this); - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Chat copyWith(void Function(Chat) updates) => super.copyWith((message) => updates(message as Chat)) as Chat; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static Chat create() => Chat._(); - Chat createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static Chat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static Chat? _defaultInstance; - - @$pb.TagNumber(1) - ChatType get type => $_getN(0); - @$pb.TagNumber(1) - set type(ChatType v) { setField(1, v); } - @$pb.TagNumber(1) - $core.bool hasType() => $_has(0); - @$pb.TagNumber(1) - void clearType() => clearField(1); - - @$pb.TagNumber(2) - $1.TypedKey get remoteConversationRecordKey => $_getN(1); - @$pb.TagNumber(2) - set remoteConversationRecordKey($1.TypedKey v) { setField(2, v); } - @$pb.TagNumber(2) - $core.bool hasRemoteConversationRecordKey() => $_has(1); - @$pb.TagNumber(2) - void clearRemoteConversationRecordKey() => clearField(2); - @$pb.TagNumber(2) - $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 { - 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); - - 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 - ; - - @$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; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static Account create() => Account._(); - Account createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - 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); } - @$pb.TagNumber(1) - $core.bool hasProfile() => $_has(0); - @$pb.TagNumber(1) - void clearProfile() => clearField(1); - @$pb.TagNumber(1) - Profile ensureProfile() => $_ensure(0); - - @$pb.TagNumber(2) - $core.bool get invisible => $_getBF(1); - @$pb.TagNumber(2) - set invisible($core.bool v) { $_setBool(1, v); } - @$pb.TagNumber(2) - $core.bool hasInvisible() => $_has(1); - @$pb.TagNumber(2) - void clearInvisible() => clearField(2); - - @$pb.TagNumber(3) - $core.int get autoAwayTimeoutSec => $_getIZ(2); - @$pb.TagNumber(3) - set autoAwayTimeoutSec($core.int v) { $_setUnsignedInt32(2, v); } - @$pb.TagNumber(3) - $core.bool hasAutoAwayTimeoutSec() => $_has(2); - @$pb.TagNumber(3) - void clearAutoAwayTimeoutSec() => clearField(3); - - @$pb.TagNumber(4) - $0.OwnedDHTRecordPointer get contactList => $_getN(3); - @$pb.TagNumber(4) - set contactList($0.OwnedDHTRecordPointer v) { setField(4, v); } - @$pb.TagNumber(4) - $core.bool hasContactList() => $_has(3); - @$pb.TagNumber(4) - void clearContactList() => clearField(4); - @$pb.TagNumber(4) - $0.OwnedDHTRecordPointer ensureContactList() => $_ensure(3); - - @$pb.TagNumber(5) - $0.OwnedDHTRecordPointer get contactInvitationRecords => $_getN(4); - @$pb.TagNumber(5) - set contactInvitationRecords($0.OwnedDHTRecordPointer v) { setField(5, v); } - @$pb.TagNumber(5) - $core.bool hasContactInvitationRecords() => $_has(4); - @$pb.TagNumber(5) - void clearContactInvitationRecords() => clearField(5); - @$pb.TagNumber(5) - $0.OwnedDHTRecordPointer ensureContactInvitationRecords() => $_ensure(4); - - @$pb.TagNumber(6) - $0.OwnedDHTRecordPointer get chatList => $_getN(5); - @$pb.TagNumber(6) - set chatList($0.OwnedDHTRecordPointer v) { setField(6, v); } - @$pb.TagNumber(6) - $core.bool hasChatList() => $_has(5); - @$pb.TagNumber(6) - void clearChatList() => clearField(6); - @$pb.TagNumber(6) - $0.OwnedDHTRecordPointer ensureChatList() => $_ensure(5); -} - class ContactInvitation extends $pb.GeneratedMessage { factory ContactInvitation() => create(); ContactInvitation._() : super(); @@ -621,7 +1505,7 @@ class ContactInvitation extends $pb.GeneratedMessage { factory ContactInvitation.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactInvitation', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..aOM<$1.TypedKey>(1, _omitFieldNames ? '' : 'contactRequestInboxKey', subBuilder: $1.TypedKey.create) + ..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'contactRequestInboxKey', subBuilder: $0.TypedKey.create) ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'writerSecret', $pb.PbFieldType.OY) ..hasRequiredFields = false ; @@ -648,15 +1532,15 @@ class ContactInvitation extends $pb.GeneratedMessage { static ContactInvitation? _defaultInstance; @$pb.TagNumber(1) - $1.TypedKey get contactRequestInboxKey => $_getN(0); + $0.TypedKey get contactRequestInboxKey => $_getN(0); @$pb.TagNumber(1) - set contactRequestInboxKey($1.TypedKey v) { setField(1, v); } + set contactRequestInboxKey($0.TypedKey v) { setField(1, v); } @$pb.TagNumber(1) $core.bool hasContactRequestInboxKey() => $_has(0); @$pb.TagNumber(1) void clearContactRequestInboxKey() => clearField(1); @$pb.TagNumber(1) - $1.TypedKey ensureContactRequestInboxKey() => $_ensure(0); + $0.TypedKey ensureContactRequestInboxKey() => $_ensure(0); @$pb.TagNumber(2) $core.List<$core.int> get writerSecret => $_getN(1); @@ -676,7 +1560,7 @@ class SignedContactInvitation extends $pb.GeneratedMessage { 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) + ..aOM<$0.Signature>(2, _omitFieldNames ? '' : 'identitySignature', subBuilder: $0.Signature.create) ..hasRequiredFields = false ; @@ -711,15 +1595,15 @@ class SignedContactInvitation extends $pb.GeneratedMessage { void clearContactInvitation() => clearField(1); @$pb.TagNumber(2) - $1.Signature get identitySignature => $_getN(1); + $0.Signature get identitySignature => $_getN(1); @$pb.TagNumber(2) - set identitySignature($1.Signature v) { setField(2, v); } + set identitySignature($0.Signature v) { setField(2, v); } @$pb.TagNumber(2) $core.bool hasIdentitySignature() => $_has(1); @$pb.TagNumber(2) void clearIdentitySignature() => clearField(2); @$pb.TagNumber(2) - $1.Signature ensureIdentitySignature() => $_ensure(1); + $0.Signature ensureIdentitySignature() => $_ensure(1); } class ContactRequest extends $pb.GeneratedMessage { @@ -781,10 +1665,10 @@ class ContactRequestPrivate extends $pb.GeneratedMessage { 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<$0.CryptoKey>(1, _omitFieldNames ? '' : 'writerKey', subBuilder: $0.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) + ..aOM<$0.TypedKey>(3, _omitFieldNames ? '' : 'identityMasterRecordKey', subBuilder: $0.TypedKey.create) + ..aOM<$0.TypedKey>(4, _omitFieldNames ? '' : 'chatRecordKey', subBuilder: $0.TypedKey.create) ..a<$fixnum.Int64>(5, _omitFieldNames ? '' : 'expiration', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) ..hasRequiredFields = false ; @@ -811,15 +1695,15 @@ class ContactRequestPrivate extends $pb.GeneratedMessage { static ContactRequestPrivate? _defaultInstance; @$pb.TagNumber(1) - $1.CryptoKey get writerKey => $_getN(0); + $0.CryptoKey get writerKey => $_getN(0); @$pb.TagNumber(1) - set writerKey($1.CryptoKey v) { setField(1, v); } + set writerKey($0.CryptoKey v) { setField(1, v); } @$pb.TagNumber(1) $core.bool hasWriterKey() => $_has(0); @$pb.TagNumber(1) void clearWriterKey() => clearField(1); @$pb.TagNumber(1) - $1.CryptoKey ensureWriterKey() => $_ensure(0); + $0.CryptoKey ensureWriterKey() => $_ensure(0); @$pb.TagNumber(2) Profile get profile => $_getN(1); @@ -833,26 +1717,26 @@ class ContactRequestPrivate extends $pb.GeneratedMessage { Profile ensureProfile() => $_ensure(1); @$pb.TagNumber(3) - $1.TypedKey get identityMasterRecordKey => $_getN(2); + $0.TypedKey get identityMasterRecordKey => $_getN(2); @$pb.TagNumber(3) - set identityMasterRecordKey($1.TypedKey v) { setField(3, v); } + set identityMasterRecordKey($0.TypedKey v) { setField(3, v); } @$pb.TagNumber(3) $core.bool hasIdentityMasterRecordKey() => $_has(2); @$pb.TagNumber(3) void clearIdentityMasterRecordKey() => clearField(3); @$pb.TagNumber(3) - $1.TypedKey ensureIdentityMasterRecordKey() => $_ensure(2); + $0.TypedKey ensureIdentityMasterRecordKey() => $_ensure(2); @$pb.TagNumber(4) - $1.TypedKey get chatRecordKey => $_getN(3); + $0.TypedKey get chatRecordKey => $_getN(3); @$pb.TagNumber(4) - set chatRecordKey($1.TypedKey v) { setField(4, v); } + set chatRecordKey($0.TypedKey v) { setField(4, v); } @$pb.TagNumber(4) $core.bool hasChatRecordKey() => $_has(3); @$pb.TagNumber(4) void clearChatRecordKey() => clearField(4); @$pb.TagNumber(4) - $1.TypedKey ensureChatRecordKey() => $_ensure(3); + $0.TypedKey ensureChatRecordKey() => $_ensure(3); @$pb.TagNumber(5) $fixnum.Int64 get expiration => $_getI64(4); @@ -872,8 +1756,8 @@ class ContactResponse extends $pb.GeneratedMessage { 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) + ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'identityMasterRecordKey', subBuilder: $0.TypedKey.create) + ..aOM<$0.TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create) ..hasRequiredFields = false ; @@ -908,26 +1792,26 @@ class ContactResponse extends $pb.GeneratedMessage { void clearAccept() => clearField(1); @$pb.TagNumber(2) - $1.TypedKey get identityMasterRecordKey => $_getN(1); + $0.TypedKey get identityMasterRecordKey => $_getN(1); @$pb.TagNumber(2) - set identityMasterRecordKey($1.TypedKey v) { setField(2, v); } + set identityMasterRecordKey($0.TypedKey v) { setField(2, v); } @$pb.TagNumber(2) $core.bool hasIdentityMasterRecordKey() => $_has(1); @$pb.TagNumber(2) void clearIdentityMasterRecordKey() => clearField(2); @$pb.TagNumber(2) - $1.TypedKey ensureIdentityMasterRecordKey() => $_ensure(1); + $0.TypedKey ensureIdentityMasterRecordKey() => $_ensure(1); @$pb.TagNumber(3) - $1.TypedKey get remoteConversationRecordKey => $_getN(2); + $0.TypedKey get remoteConversationRecordKey => $_getN(2); @$pb.TagNumber(3) - set remoteConversationRecordKey($1.TypedKey v) { setField(3, v); } + set remoteConversationRecordKey($0.TypedKey v) { setField(3, v); } @$pb.TagNumber(3) $core.bool hasRemoteConversationRecordKey() => $_has(2); @$pb.TagNumber(3) void clearRemoteConversationRecordKey() => clearField(3); @$pb.TagNumber(3) - $1.TypedKey ensureRemoteConversationRecordKey() => $_ensure(2); + $0.TypedKey ensureRemoteConversationRecordKey() => $_ensure(2); } class SignedContactResponse extends $pb.GeneratedMessage { @@ -938,7 +1822,7 @@ class SignedContactResponse extends $pb.GeneratedMessage { 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) + ..aOM<$0.Signature>(2, _omitFieldNames ? '' : 'identitySignature', subBuilder: $0.Signature.create) ..hasRequiredFields = false ; @@ -973,15 +1857,15 @@ class SignedContactResponse extends $pb.GeneratedMessage { void clearContactResponse() => clearField(1); @$pb.TagNumber(2) - $1.Signature get identitySignature => $_getN(1); + $0.Signature get identitySignature => $_getN(1); @$pb.TagNumber(2) - set identitySignature($1.Signature v) { setField(2, v); } + set identitySignature($0.Signature v) { setField(2, v); } @$pb.TagNumber(2) $core.bool hasIdentitySignature() => $_has(1); @$pb.TagNumber(2) void clearIdentitySignature() => clearField(2); @$pb.TagNumber(2) - $1.Signature ensureIdentitySignature() => $_ensure(1); + $0.Signature ensureIdentitySignature() => $_ensure(1); } class ContactInvitationRecord extends $pb.GeneratedMessage { @@ -991,10 +1875,10 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { factory ContactInvitationRecord.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactInvitationRecord', 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) + ..aOM<$1.OwnedDHTRecordPointer>(1, _omitFieldNames ? '' : 'contactRequestInbox', subBuilder: $1.OwnedDHTRecordPointer.create) + ..aOM<$0.CryptoKey>(2, _omitFieldNames ? '' : 'writerKey', subBuilder: $0.CryptoKey.create) + ..aOM<$0.CryptoKey>(3, _omitFieldNames ? '' : 'writerSecret', subBuilder: $0.CryptoKey.create) + ..aOM<$0.TypedKey>(4, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.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') @@ -1023,48 +1907,48 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { static ContactInvitationRecord? _defaultInstance; @$pb.TagNumber(1) - $0.OwnedDHTRecordPointer get contactRequestInbox => $_getN(0); + $1.OwnedDHTRecordPointer get contactRequestInbox => $_getN(0); @$pb.TagNumber(1) - set contactRequestInbox($0.OwnedDHTRecordPointer v) { setField(1, v); } + set contactRequestInbox($1.OwnedDHTRecordPointer v) { setField(1, v); } @$pb.TagNumber(1) $core.bool hasContactRequestInbox() => $_has(0); @$pb.TagNumber(1) void clearContactRequestInbox() => clearField(1); @$pb.TagNumber(1) - $0.OwnedDHTRecordPointer ensureContactRequestInbox() => $_ensure(0); + $1.OwnedDHTRecordPointer ensureContactRequestInbox() => $_ensure(0); @$pb.TagNumber(2) - $1.CryptoKey get writerKey => $_getN(1); + $0.CryptoKey get writerKey => $_getN(1); @$pb.TagNumber(2) - set writerKey($1.CryptoKey v) { setField(2, v); } + set writerKey($0.CryptoKey v) { setField(2, v); } @$pb.TagNumber(2) $core.bool hasWriterKey() => $_has(1); @$pb.TagNumber(2) void clearWriterKey() => clearField(2); @$pb.TagNumber(2) - $1.CryptoKey ensureWriterKey() => $_ensure(1); + $0.CryptoKey ensureWriterKey() => $_ensure(1); @$pb.TagNumber(3) - $1.CryptoKey get writerSecret => $_getN(2); + $0.CryptoKey get writerSecret => $_getN(2); @$pb.TagNumber(3) - set writerSecret($1.CryptoKey v) { setField(3, v); } + set writerSecret($0.CryptoKey v) { setField(3, v); } @$pb.TagNumber(3) $core.bool hasWriterSecret() => $_has(2); @$pb.TagNumber(3) void clearWriterSecret() => clearField(3); @$pb.TagNumber(3) - $1.CryptoKey ensureWriterSecret() => $_ensure(2); + $0.CryptoKey ensureWriterSecret() => $_ensure(2); @$pb.TagNumber(4) - $1.TypedKey get localConversationRecordKey => $_getN(3); + $0.TypedKey get localConversationRecordKey => $_getN(3); @$pb.TagNumber(4) - set localConversationRecordKey($1.TypedKey v) { setField(4, v); } + set localConversationRecordKey($0.TypedKey v) { setField(4, v); } @$pb.TagNumber(4) $core.bool hasLocalConversationRecordKey() => $_has(3); @$pb.TagNumber(4) void clearLocalConversationRecordKey() => clearField(4); @$pb.TagNumber(4) - $1.TypedKey ensureLocalConversationRecordKey() => $_ensure(3); + $0.TypedKey ensureLocalConversationRecordKey() => $_ensure(3); @$pb.TagNumber(5) $fixnum.Int64 get expiration => $_getI64(4); diff --git a/lib/proto/veilidchat.pbenum.dart b/lib/proto/veilidchat.pbenum.dart index 7bef00f..9133788 100644 --- a/lib/proto/veilidchat.pbenum.dart +++ b/lib/proto/veilidchat.pbenum.dart @@ -13,23 +13,6 @@ import 'dart:core' as $core; import 'package:protobuf/protobuf.dart' as $pb; -class AttachmentKind extends $pb.ProtobufEnum { - static const AttachmentKind ATTACHMENT_KIND_UNSPECIFIED = AttachmentKind._(0, _omitEnumNames ? '' : 'ATTACHMENT_KIND_UNSPECIFIED'); - static const AttachmentKind ATTACHMENT_KIND_FILE = AttachmentKind._(1, _omitEnumNames ? '' : 'ATTACHMENT_KIND_FILE'); - static const AttachmentKind ATTACHMENT_KIND_IMAGE = AttachmentKind._(2, _omitEnumNames ? '' : 'ATTACHMENT_KIND_IMAGE'); - - static const $core.List values = [ - ATTACHMENT_KIND_UNSPECIFIED, - ATTACHMENT_KIND_FILE, - ATTACHMENT_KIND_IMAGE, - ]; - - static final $core.Map<$core.int, AttachmentKind> _byValue = $pb.ProtobufEnum.initByValue(values); - static AttachmentKind? valueOf($core.int value) => _byValue[value]; - - const AttachmentKind._($core.int v, $core.String n) : super(v, n); -} - class Availability extends $pb.ProtobufEnum { static const Availability AVAILABILITY_UNSPECIFIED = Availability._(0, _omitEnumNames ? '' : 'AVAILABILITY_UNSPECIFIED'); static const Availability AVAILABILITY_OFFLINE = Availability._(1, _omitEnumNames ? '' : 'AVAILABILITY_OFFLINE'); @@ -51,23 +34,6 @@ class Availability extends $pb.ProtobufEnum { const Availability._($core.int v, $core.String n) : super(v, n); } -class ChatType extends $pb.ProtobufEnum { - static const ChatType CHAT_TYPE_UNSPECIFIED = ChatType._(0, _omitEnumNames ? '' : 'CHAT_TYPE_UNSPECIFIED'); - static const ChatType SINGLE_CONTACT = ChatType._(1, _omitEnumNames ? '' : 'SINGLE_CONTACT'); - static const ChatType GROUP = ChatType._(2, _omitEnumNames ? '' : 'GROUP'); - - static const $core.List values = [ - CHAT_TYPE_UNSPECIFIED, - SINGLE_CONTACT, - GROUP, - ]; - - static final $core.Map<$core.int, ChatType> _byValue = $pb.ProtobufEnum.initByValue(values); - static ChatType? valueOf($core.int value) => _byValue[value]; - - const ChatType._($core.int v, $core.String n) : super(v, n); -} - class EncryptionKeyType extends $pb.ProtobufEnum { static const EncryptionKeyType ENCRYPTION_KEY_TYPE_UNSPECIFIED = EncryptionKeyType._(0, _omitEnumNames ? '' : 'ENCRYPTION_KEY_TYPE_UNSPECIFIED'); static const EncryptionKeyType ENCRYPTION_KEY_TYPE_NONE = EncryptionKeyType._(1, _omitEnumNames ? '' : 'ENCRYPTION_KEY_TYPE_NONE'); @@ -87,5 +53,26 @@ class EncryptionKeyType extends $pb.ProtobufEnum { const EncryptionKeyType._($core.int v, $core.String n) : super(v, n); } +class Scope extends $pb.ProtobufEnum { + static const Scope WATCHERS = Scope._(0, _omitEnumNames ? '' : 'WATCHERS'); + static const Scope MODERATED = Scope._(1, _omitEnumNames ? '' : 'MODERATED'); + static const Scope TALKERS = Scope._(2, _omitEnumNames ? '' : 'TALKERS'); + static const Scope MODERATORS = Scope._(3, _omitEnumNames ? '' : 'MODERATORS'); + static const Scope ADMINS = Scope._(4, _omitEnumNames ? '' : 'ADMINS'); + + static const $core.List values = [ + WATCHERS, + MODERATED, + TALKERS, + MODERATORS, + ADMINS, + ]; + + static final $core.Map<$core.int, Scope> _byValue = $pb.ProtobufEnum.initByValue(values); + static Scope? valueOf($core.int value) => _byValue[value]; + + const Scope._($core.int v, $core.String n) : super(v, n); +} + const _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/lib/proto/veilidchat.pbjson.dart b/lib/proto/veilidchat.pbjson.dart index 7aea1fb..2aaecb2 100644 --- a/lib/proto/veilidchat.pbjson.dart +++ b/lib/proto/veilidchat.pbjson.dart @@ -13,21 +13,6 @@ import 'dart:convert' as $convert; import 'dart:core' as $core; import 'dart:typed_data' as $typed_data; -@$core.Deprecated('Use attachmentKindDescriptor instead') -const AttachmentKind$json = { - '1': 'AttachmentKind', - '2': [ - {'1': 'ATTACHMENT_KIND_UNSPECIFIED', '2': 0}, - {'1': 'ATTACHMENT_KIND_FILE', '2': 1}, - {'1': 'ATTACHMENT_KIND_IMAGE', '2': 2}, - ], -}; - -/// Descriptor for `AttachmentKind`. Decode as a `google.protobuf.EnumDescriptorProto`. -final $typed_data.Uint8List attachmentKindDescriptor = $convert.base64Decode( - 'Cg5BdHRhY2htZW50S2luZBIfChtBVFRBQ0hNRU5UX0tJTkRfVU5TUEVDSUZJRUQQABIYChRBVF' - 'RBQ0hNRU5UX0tJTkRfRklMRRABEhkKFUFUVEFDSE1FTlRfS0lORF9JTUFHRRAC'); - @$core.Deprecated('Use availabilityDescriptor instead') const Availability$json = { '1': 'Availability', @@ -46,21 +31,6 @@ final $typed_data.Uint8List availabilityDescriptor = $convert.base64Decode( 'lMSVRZX09GRkxJTkUQARIVChFBVkFJTEFCSUxJVFlfRlJFRRACEhUKEUFWQUlMQUJJTElUWV9C' 'VVNZEAMSFQoRQVZBSUxBQklMSVRZX0FXQVkQBA=='); -@$core.Deprecated('Use chatTypeDescriptor instead') -const ChatType$json = { - '1': 'ChatType', - '2': [ - {'1': 'CHAT_TYPE_UNSPECIFIED', '2': 0}, - {'1': 'SINGLE_CONTACT', '2': 1}, - {'1': 'GROUP', '2': 2}, - ], -}; - -/// Descriptor for `ChatType`. Decode as a `google.protobuf.EnumDescriptorProto`. -final $typed_data.Uint8List chatTypeDescriptor = $convert.base64Decode( - 'CghDaGF0VHlwZRIZChVDSEFUX1RZUEVfVU5TUEVDSUZJRUQQABISCg5TSU5HTEVfQ09OVEFDVB' - 'ABEgkKBUdST1VQEAI='); - @$core.Deprecated('Use encryptionKeyTypeDescriptor instead') const EncryptionKeyType$json = { '1': 'EncryptionKeyType', @@ -78,43 +48,249 @@ final $typed_data.Uint8List encryptionKeyTypeDescriptor = $convert.base64Decode( 'ASHAoYRU5DUllQVElPTl9LRVlfVFlQRV9OT05FEAESGwoXRU5DUllQVElPTl9LRVlfVFlQRV9Q' 'SU4QAhIgChxFTkNSWVBUSU9OX0tFWV9UWVBFX1BBU1NXT1JEEAM='); +@$core.Deprecated('Use scopeDescriptor instead') +const Scope$json = { + '1': 'Scope', + '2': [ + {'1': 'WATCHERS', '2': 0}, + {'1': 'MODERATED', '2': 1}, + {'1': 'TALKERS', '2': 2}, + {'1': 'MODERATORS', '2': 3}, + {'1': 'ADMINS', '2': 4}, + ], +}; + +/// Descriptor for `Scope`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List scopeDescriptor = $convert.base64Decode( + 'CgVTY29wZRIMCghXQVRDSEVSUxAAEg0KCU1PREVSQVRFRBABEgsKB1RBTEtFUlMQAhIOCgpNT0' + 'RFUkFUT1JTEAMSCgoGQURNSU5TEAQ='); + @$core.Deprecated('Use attachmentDescriptor instead') const Attachment$json = { '1': 'Attachment', '2': [ - {'1': 'kind', '3': 1, '4': 1, '5': 14, '6': '.veilidchat.AttachmentKind', '10': 'kind'}, - {'1': 'mime', '3': 2, '4': 1, '5': 9, '10': 'mime'}, - {'1': 'name', '3': 3, '4': 1, '5': 9, '10': 'name'}, - {'1': 'content', '3': 4, '4': 1, '5': 11, '6': '.dht.DataReference', '10': 'content'}, - {'1': 'signature', '3': 5, '4': 1, '5': 11, '6': '.veilid.Signature', '10': 'signature'}, + {'1': 'media', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.AttachmentMedia', '9': 0, '10': 'media'}, + {'1': 'signature', '3': 2, '4': 1, '5': 11, '6': '.veilid.Signature', '10': 'signature'}, + ], + '8': [ + {'1': 'kind'}, ], }; /// Descriptor for `Attachment`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List attachmentDescriptor = $convert.base64Decode( - 'CgpBdHRhY2htZW50Ei4KBGtpbmQYASABKA4yGi52ZWlsaWRjaGF0LkF0dGFjaG1lbnRLaW5kUg' - 'RraW5kEhIKBG1pbWUYAiABKAlSBG1pbWUSEgoEbmFtZRgDIAEoCVIEbmFtZRIsCgdjb250ZW50' - 'GAQgASgLMhIuZGh0LkRhdGFSZWZlcmVuY2VSB2NvbnRlbnQSLwoJc2lnbmF0dXJlGAUgASgLMh' - 'EudmVpbGlkLlNpZ25hdHVyZVIJc2lnbmF0dXJl'); + 'CgpBdHRhY2htZW50EjMKBW1lZGlhGAEgASgLMhsudmVpbGlkY2hhdC5BdHRhY2htZW50TWVkaW' + 'FIAFIFbWVkaWESLwoJc2lnbmF0dXJlGAIgASgLMhEudmVpbGlkLlNpZ25hdHVyZVIJc2lnbmF0' + 'dXJlQgYKBGtpbmQ='); + +@$core.Deprecated('Use attachmentMediaDescriptor instead') +const AttachmentMedia$json = { + '1': 'AttachmentMedia', + '2': [ + {'1': 'mime', '3': 1, '4': 1, '5': 9, '10': 'mime'}, + {'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'}, + {'1': 'content', '3': 3, '4': 1, '5': 11, '6': '.dht.DataReference', '10': 'content'}, + ], +}; + +/// Descriptor for `AttachmentMedia`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List attachmentMediaDescriptor = $convert.base64Decode( + 'Cg9BdHRhY2htZW50TWVkaWESEgoEbWltZRgBIAEoCVIEbWltZRISCgRuYW1lGAIgASgJUgRuYW' + '1lEiwKB2NvbnRlbnQYAyABKAsyEi5kaHQuRGF0YVJlZmVyZW5jZVIHY29udGVudA=='); + +@$core.Deprecated('Use permissionsDescriptor instead') +const Permissions$json = { + '1': 'Permissions', + '2': [ + {'1': 'can_add_members', '3': 1, '4': 1, '5': 14, '6': '.veilidchat.Scope', '10': 'canAddMembers'}, + {'1': 'can_edit_info', '3': 2, '4': 1, '5': 14, '6': '.veilidchat.Scope', '10': 'canEditInfo'}, + {'1': 'moderated', '3': 3, '4': 1, '5': 8, '10': 'moderated'}, + ], +}; + +/// Descriptor for `Permissions`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List permissionsDescriptor = $convert.base64Decode( + 'CgtQZXJtaXNzaW9ucxI5Cg9jYW5fYWRkX21lbWJlcnMYASABKA4yES52ZWlsaWRjaGF0LlNjb3' + 'BlUg1jYW5BZGRNZW1iZXJzEjUKDWNhbl9lZGl0X2luZm8YAiABKA4yES52ZWlsaWRjaGF0LlNj' + 'b3BlUgtjYW5FZGl0SW5mbxIcCgltb2RlcmF0ZWQYAyABKAhSCW1vZGVyYXRlZA=='); + +@$core.Deprecated('Use membershipDescriptor instead') +const Membership$json = { + '1': 'Membership', + '2': [ + {'1': 'watchers', '3': 1, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'watchers'}, + {'1': 'moderated', '3': 2, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'moderated'}, + {'1': 'talkers', '3': 3, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'talkers'}, + {'1': 'moderators', '3': 4, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'moderators'}, + {'1': 'admins', '3': 5, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'admins'}, + ], +}; + +/// Descriptor for `Membership`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List membershipDescriptor = $convert.base64Decode( + 'CgpNZW1iZXJzaGlwEiwKCHdhdGNoZXJzGAEgAygLMhAudmVpbGlkLlR5cGVkS2V5Ugh3YXRjaG' + 'VycxIuCgltb2RlcmF0ZWQYAiADKAsyEC52ZWlsaWQuVHlwZWRLZXlSCW1vZGVyYXRlZBIqCgd0' + 'YWxrZXJzGAMgAygLMhAudmVpbGlkLlR5cGVkS2V5Ugd0YWxrZXJzEjAKCm1vZGVyYXRvcnMYBC' + 'ADKAsyEC52ZWlsaWQuVHlwZWRLZXlSCm1vZGVyYXRvcnMSKAoGYWRtaW5zGAUgAygLMhAudmVp' + 'bGlkLlR5cGVkS2V5UgZhZG1pbnM='); + +@$core.Deprecated('Use chatSettingsDescriptor instead') +const ChatSettings$json = { + '1': 'ChatSettings', + '2': [ + {'1': 'title', '3': 1, '4': 1, '5': 9, '10': 'title'}, + {'1': 'description', '3': 2, '4': 1, '5': 9, '10': 'description'}, + {'1': 'icon', '3': 3, '4': 1, '5': 11, '6': '.dht.DataReference', '9': 0, '10': 'icon', '17': true}, + {'1': 'default_expiration', '3': 4, '4': 1, '5': 4, '10': 'defaultExpiration'}, + ], + '8': [ + {'1': '_icon'}, + ], +}; + +/// Descriptor for `ChatSettings`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List chatSettingsDescriptor = $convert.base64Decode( + 'CgxDaGF0U2V0dGluZ3MSFAoFdGl0bGUYASABKAlSBXRpdGxlEiAKC2Rlc2NyaXB0aW9uGAIgAS' + 'gJUgtkZXNjcmlwdGlvbhIrCgRpY29uGAMgASgLMhIuZGh0LkRhdGFSZWZlcmVuY2VIAFIEaWNv' + 'bogBARItChJkZWZhdWx0X2V4cGlyYXRpb24YBCABKARSEWRlZmF1bHRFeHBpcmF0aW9uQgcKBV' + '9pY29u'); @$core.Deprecated('Use messageDescriptor instead') const Message$json = { '1': 'Message', '2': [ - {'1': 'author', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'author'}, - {'1': 'timestamp', '3': 2, '4': 1, '5': 4, '10': 'timestamp'}, - {'1': 'text', '3': 3, '4': 1, '5': 9, '10': 'text'}, - {'1': 'signature', '3': 4, '4': 1, '5': 11, '6': '.veilid.Signature', '10': 'signature'}, - {'1': 'attachments', '3': 5, '4': 3, '5': 11, '6': '.veilidchat.Attachment', '10': 'attachments'}, + {'1': 'id', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'id'}, + {'1': 'author', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'author'}, + {'1': 'timestamp', '3': 3, '4': 1, '5': 4, '10': 'timestamp'}, + {'1': 'text', '3': 4, '4': 1, '5': 11, '6': '.veilidchat.Message.Text', '9': 0, '10': 'text'}, + {'1': 'secret', '3': 5, '4': 1, '5': 11, '6': '.veilidchat.Message.Secret', '9': 0, '10': 'secret'}, + {'1': 'delete', '3': 6, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlDelete', '9': 0, '10': 'delete'}, + {'1': 'clear', '3': 7, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlClear', '9': 0, '10': 'clear'}, + {'1': 'settings', '3': 8, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlSettings', '9': 0, '10': 'settings'}, + {'1': 'permissions', '3': 9, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlPermissions', '9': 0, '10': 'permissions'}, + {'1': 'membership', '3': 10, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlMembership', '9': 0, '10': 'membership'}, + {'1': 'moderation', '3': 11, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlModeration', '9': 0, '10': 'moderation'}, + {'1': 'signature', '3': 12, '4': 1, '5': 11, '6': '.veilid.Signature', '10': 'signature'}, + ], + '3': [Message_Text$json, Message_Secret$json, Message_ControlDelete$json, Message_ControlClear$json, Message_ControlSettings$json, Message_ControlPermissions$json, Message_ControlMembership$json, Message_ControlModeration$json], + '8': [ + {'1': 'kind'}, + ], +}; + +@$core.Deprecated('Use messageDescriptor instead') +const Message_Text$json = { + '1': 'Text', + '2': [ + {'1': 'text', '3': 1, '4': 1, '5': 9, '10': 'text'}, + {'1': 'topic', '3': 2, '4': 1, '5': 9, '10': 'topic'}, + {'1': 'reply_id', '3': 3, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'replyId'}, + {'1': 'expiration', '3': 4, '4': 1, '5': 4, '10': 'expiration'}, + {'1': 'view_limit', '3': 5, '4': 1, '5': 4, '10': 'viewLimit'}, + {'1': 'attachments', '3': 6, '4': 3, '5': 11, '6': '.veilidchat.Attachment', '10': 'attachments'}, + ], +}; + +@$core.Deprecated('Use messageDescriptor instead') +const Message_Secret$json = { + '1': 'Secret', + '2': [ + {'1': 'ciphertext', '3': 1, '4': 1, '5': 12, '10': 'ciphertext'}, + {'1': 'expiration', '3': 2, '4': 1, '5': 4, '10': 'expiration'}, + ], +}; + +@$core.Deprecated('Use messageDescriptor instead') +const Message_ControlDelete$json = { + '1': 'ControlDelete', + '2': [ + {'1': 'ids', '3': 1, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'ids'}, + ], +}; + +@$core.Deprecated('Use messageDescriptor instead') +const Message_ControlClear$json = { + '1': 'ControlClear', + '2': [ + {'1': 'timestamp', '3': 1, '4': 1, '5': 4, '10': 'timestamp'}, + ], +}; + +@$core.Deprecated('Use messageDescriptor instead') +const Message_ControlSettings$json = { + '1': 'ControlSettings', + '2': [ + {'1': 'settings', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.ChatSettings', '10': 'settings'}, + ], +}; + +@$core.Deprecated('Use messageDescriptor instead') +const Message_ControlPermissions$json = { + '1': 'ControlPermissions', + '2': [ + {'1': 'permissions', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.Permissions', '10': 'permissions'}, + ], +}; + +@$core.Deprecated('Use messageDescriptor instead') +const Message_ControlMembership$json = { + '1': 'ControlMembership', + '2': [ + {'1': 'membership', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.Membership', '10': 'membership'}, + ], +}; + +@$core.Deprecated('Use messageDescriptor instead') +const Message_ControlModeration$json = { + '1': 'ControlModeration', + '2': [ + {'1': 'accepted_ids', '3': 1, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'acceptedIds'}, + {'1': 'rejected_ids', '3': 2, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'rejectedIds'}, ], }; /// Descriptor for `Message`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List messageDescriptor = $convert.base64Decode( - 'CgdNZXNzYWdlEigKBmF1dGhvchgBIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIGYXV0aG9yEhwKCX' - 'RpbWVzdGFtcBgCIAEoBFIJdGltZXN0YW1wEhIKBHRleHQYAyABKAlSBHRleHQSLwoJc2lnbmF0' - 'dXJlGAQgASgLMhEudmVpbGlkLlNpZ25hdHVyZVIJc2lnbmF0dXJlEjgKC2F0dGFjaG1lbnRzGA' - 'UgAygLMhYudmVpbGlkY2hhdC5BdHRhY2htZW50UgthdHRhY2htZW50cw=='); + 'CgdNZXNzYWdlEiAKAmlkGAEgASgLMhAudmVpbGlkLlR5cGVkS2V5UgJpZBIoCgZhdXRob3IYAi' + 'ABKAsyEC52ZWlsaWQuVHlwZWRLZXlSBmF1dGhvchIcCgl0aW1lc3RhbXAYAyABKARSCXRpbWVz' + 'dGFtcBIuCgR0ZXh0GAQgASgLMhgudmVpbGlkY2hhdC5NZXNzYWdlLlRleHRIAFIEdGV4dBI0Cg' + 'ZzZWNyZXQYBSABKAsyGi52ZWlsaWRjaGF0Lk1lc3NhZ2UuU2VjcmV0SABSBnNlY3JldBI7CgZk' + 'ZWxldGUYBiABKAsyIS52ZWlsaWRjaGF0Lk1lc3NhZ2UuQ29udHJvbERlbGV0ZUgAUgZkZWxldG' + 'USOAoFY2xlYXIYByABKAsyIC52ZWlsaWRjaGF0Lk1lc3NhZ2UuQ29udHJvbENsZWFySABSBWNs' + 'ZWFyEkEKCHNldHRpbmdzGAggASgLMiMudmVpbGlkY2hhdC5NZXNzYWdlLkNvbnRyb2xTZXR0aW' + '5nc0gAUghzZXR0aW5ncxJKCgtwZXJtaXNzaW9ucxgJIAEoCzImLnZlaWxpZGNoYXQuTWVzc2Fn' + 'ZS5Db250cm9sUGVybWlzc2lvbnNIAFILcGVybWlzc2lvbnMSRwoKbWVtYmVyc2hpcBgKIAEoCz' + 'IlLnZlaWxpZGNoYXQuTWVzc2FnZS5Db250cm9sTWVtYmVyc2hpcEgAUgptZW1iZXJzaGlwEkcK' + 'Cm1vZGVyYXRpb24YCyABKAsyJS52ZWlsaWRjaGF0Lk1lc3NhZ2UuQ29udHJvbE1vZGVyYXRpb2' + '5IAFIKbW9kZXJhdGlvbhIvCglzaWduYXR1cmUYDCABKAsyES52ZWlsaWQuU2lnbmF0dXJlUglz' + 'aWduYXR1cmUa1gEKBFRleHQSEgoEdGV4dBgBIAEoCVIEdGV4dBIUCgV0b3BpYxgCIAEoCVIFdG' + '9waWMSKwoIcmVwbHlfaWQYAyABKAsyEC52ZWlsaWQuVHlwZWRLZXlSB3JlcGx5SWQSHgoKZXhw' + 'aXJhdGlvbhgEIAEoBFIKZXhwaXJhdGlvbhIdCgp2aWV3X2xpbWl0GAUgASgEUgl2aWV3TGltaX' + 'QSOAoLYXR0YWNobWVudHMYBiADKAsyFi52ZWlsaWRjaGF0LkF0dGFjaG1lbnRSC2F0dGFjaG1l' + 'bnRzGkgKBlNlY3JldBIeCgpjaXBoZXJ0ZXh0GAEgASgMUgpjaXBoZXJ0ZXh0Eh4KCmV4cGlyYX' + 'Rpb24YAiABKARSCmV4cGlyYXRpb24aMwoNQ29udHJvbERlbGV0ZRIiCgNpZHMYASADKAsyEC52' + 'ZWlsaWQuVHlwZWRLZXlSA2lkcxosCgxDb250cm9sQ2xlYXISHAoJdGltZXN0YW1wGAEgASgEUg' + 'l0aW1lc3RhbXAaRwoPQ29udHJvbFNldHRpbmdzEjQKCHNldHRpbmdzGAEgASgLMhgudmVpbGlk' + 'Y2hhdC5DaGF0U2V0dGluZ3NSCHNldHRpbmdzGk8KEkNvbnRyb2xQZXJtaXNzaW9ucxI5CgtwZX' + 'JtaXNzaW9ucxgBIAEoCzIXLnZlaWxpZGNoYXQuUGVybWlzc2lvbnNSC3Blcm1pc3Npb25zGksK' + 'EUNvbnRyb2xNZW1iZXJzaGlwEjYKCm1lbWJlcnNoaXAYASABKAsyFi52ZWlsaWRjaGF0Lk1lbW' + 'JlcnNoaXBSCm1lbWJlcnNoaXAafQoRQ29udHJvbE1vZGVyYXRpb24SMwoMYWNjZXB0ZWRfaWRz' + 'GAEgAygLMhAudmVpbGlkLlR5cGVkS2V5UgthY2NlcHRlZElkcxIzCgxyZWplY3RlZF9pZHMYAi' + 'ADKAsyEC52ZWlsaWQuVHlwZWRLZXlSC3JlamVjdGVkSWRzQgYKBGtpbmQ='); + +@$core.Deprecated('Use reconciledMessageDescriptor instead') +const ReconciledMessage$json = { + '1': 'ReconciledMessage', + '2': [ + {'1': 'content', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.Message', '10': 'content'}, + {'1': 'reconciled_time', '3': 2, '4': 1, '5': 4, '10': 'reconciledTime'}, + ], +}; + +/// Descriptor for `ReconciledMessage`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List reconciledMessageDescriptor = $convert.base64Decode( + 'ChFSZWNvbmNpbGVkTWVzc2FnZRItCgdjb250ZW50GAEgASgLMhMudmVpbGlkY2hhdC5NZXNzYW' + 'dlUgdjb250ZW50EicKD3JlY29uY2lsZWRfdGltZRgCIAEoBFIOcmVjb25jaWxlZFRpbWU='); @$core.Deprecated('Use conversationDescriptor instead') const Conversation$json = { @@ -132,6 +308,91 @@ final $typed_data.Uint8List conversationDescriptor = $convert.base64Decode( 'JvZmlsZRIwChRpZGVudGl0eV9tYXN0ZXJfanNvbhgCIAEoCVISaWRlbnRpdHlNYXN0ZXJKc29u' 'EiwKCG1lc3NhZ2VzGAMgASgLMhAudmVpbGlkLlR5cGVkS2V5UghtZXNzYWdlcw=='); +@$core.Deprecated('Use chatDescriptor instead') +const Chat$json = { + '1': 'Chat', + '2': [ + {'1': 'settings', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.ChatSettings', '10': 'settings'}, + {'1': 'local_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'}, + {'1': 'remote_conversation_record_key', '3': 3, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'}, + ], +}; + +/// Descriptor for `Chat`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List chatDescriptor = $convert.base64Decode( + 'CgRDaGF0EjQKCHNldHRpbmdzGAEgASgLMhgudmVpbGlkY2hhdC5DaGF0U2V0dGluZ3NSCHNldH' + 'RpbmdzElMKHWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAIgASgLMhAudmVpbGlkLlR5' + 'cGVkS2V5Uhpsb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRJVCh5yZW1vdGVfY29udmVyc2F0aW' + '9uX3JlY29yZF9rZXkYAyABKAsyEC52ZWlsaWQuVHlwZWRLZXlSG3JlbW90ZUNvbnZlcnNhdGlv' + 'blJlY29yZEtleQ=='); + +@$core.Deprecated('Use groupChatDescriptor instead') +const GroupChat$json = { + '1': 'GroupChat', + '2': [ + {'1': 'settings', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.ChatSettings', '10': 'settings'}, + {'1': 'local_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'}, + {'1': 'remote_conversation_record_keys', '3': 3, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKeys'}, + ], +}; + +/// Descriptor for `GroupChat`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List groupChatDescriptor = $convert.base64Decode( + 'CglHcm91cENoYXQSNAoIc2V0dGluZ3MYASABKAsyGC52ZWlsaWRjaGF0LkNoYXRTZXR0aW5nc1' + 'IIc2V0dGluZ3MSUwodbG9jYWxfY29udmVyc2F0aW9uX3JlY29yZF9rZXkYAiABKAsyEC52ZWls' + 'aWQuVHlwZWRLZXlSGmxvY2FsQ29udmVyc2F0aW9uUmVjb3JkS2V5ElcKH3JlbW90ZV9jb252ZX' + 'JzYXRpb25fcmVjb3JkX2tleXMYAyADKAsyEC52ZWlsaWQuVHlwZWRLZXlSHHJlbW90ZUNvbnZl' + 'cnNhdGlvblJlY29yZEtleXM='); + +@$core.Deprecated('Use profileDescriptor instead') +const Profile$json = { + '1': 'Profile', + '2': [ + {'1': 'name', '3': 1, '4': 1, '5': 9, '10': 'name'}, + {'1': 'pronouns', '3': 2, '4': 1, '5': 9, '10': 'pronouns'}, + {'1': 'about', '3': 3, '4': 1, '5': 9, '10': 'about'}, + {'1': 'status', '3': 4, '4': 1, '5': 9, '10': 'status'}, + {'1': 'availability', '3': 5, '4': 1, '5': 14, '6': '.veilidchat.Availability', '10': 'availability'}, + {'1': 'avatar', '3': 6, '4': 1, '5': 11, '6': '.veilid.TypedKey', '9': 0, '10': 'avatar', '17': true}, + ], + '8': [ + {'1': '_avatar'}, + ], +}; + +/// Descriptor for `Profile`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List profileDescriptor = $convert.base64Decode( + 'CgdQcm9maWxlEhIKBG5hbWUYASABKAlSBG5hbWUSGgoIcHJvbm91bnMYAiABKAlSCHByb25vdW' + '5zEhQKBWFib3V0GAMgASgJUgVhYm91dBIWCgZzdGF0dXMYBCABKAlSBnN0YXR1cxI8CgxhdmFp' + 'bGFiaWxpdHkYBSABKA4yGC52ZWlsaWRjaGF0LkF2YWlsYWJpbGl0eVIMYXZhaWxhYmlsaXR5Ei' + '0KBmF2YXRhchgGIAEoCzIQLnZlaWxpZC5UeXBlZEtleUgAUgZhdmF0YXKIAQFCCQoHX2F2YXRh' + 'cg=='); + +@$core.Deprecated('Use accountDescriptor instead') +const Account$json = { + '1': 'Account', + '2': [ + {'1': 'profile', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'profile'}, + {'1': 'invisible', '3': 2, '4': 1, '5': 8, '10': 'invisible'}, + {'1': 'auto_away_timeout_sec', '3': 3, '4': 1, '5': 13, '10': 'autoAwayTimeoutSec'}, + {'1': 'contact_list', '3': 4, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'contactList'}, + {'1': 'contact_invitation_records', '3': 5, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'contactInvitationRecords'}, + {'1': 'chat_list', '3': 6, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'chatList'}, + {'1': 'group_chat_list', '3': 7, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'groupChatList'}, + ], +}; + +/// Descriptor for `Account`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List accountDescriptor = $convert.base64Decode( + 'CgdBY2NvdW50Ei0KB3Byb2ZpbGUYASABKAsyEy52ZWlsaWRjaGF0LlByb2ZpbGVSB3Byb2ZpbG' + 'USHAoJaW52aXNpYmxlGAIgASgIUglpbnZpc2libGUSMQoVYXV0b19hd2F5X3RpbWVvdXRfc2Vj' + 'GAMgASgNUhJhdXRvQXdheVRpbWVvdXRTZWMSPQoMY29udGFjdF9saXN0GAQgASgLMhouZGh0Lk' + '93bmVkREhUUmVjb3JkUG9pbnRlclILY29udGFjdExpc3QSWAoaY29udGFjdF9pbnZpdGF0aW9u' + 'X3JlY29yZHMYBSABKAsyGi5kaHQuT3duZWRESFRSZWNvcmRQb2ludGVyUhhjb250YWN0SW52aX' + 'RhdGlvblJlY29yZHMSNwoJY2hhdF9saXN0GAYgASgLMhouZGh0Lk93bmVkREhUUmVjb3JkUG9p' + 'bnRlclIIY2hhdExpc3QSQgoPZ3JvdXBfY2hhdF9saXN0GAcgASgLMhouZGh0Lk93bmVkREhUUm' + 'Vjb3JkUG9pbnRlclINZ3JvdXBDaGF0TGlzdA=='); + @$core.Deprecated('Use contactDescriptor instead') const Contact$json = { '1': 'Contact', @@ -158,68 +419,6 @@ final $typed_data.Uint8List contactDescriptor = $convert.base64Decode( 'lSGmxvY2FsQ29udmVyc2F0aW9uUmVjb3JkS2V5EisKEXNob3dfYXZhaWxhYmlsaXR5GAcgASgI' 'UhBzaG93QXZhaWxhYmlsaXR5'); -@$core.Deprecated('Use profileDescriptor instead') -const Profile$json = { - '1': 'Profile', - '2': [ - {'1': 'name', '3': 1, '4': 1, '5': 9, '10': 'name'}, - {'1': 'pronouns', '3': 2, '4': 1, '5': 9, '10': 'pronouns'}, - {'1': 'status', '3': 3, '4': 1, '5': 9, '10': 'status'}, - {'1': 'availability', '3': 4, '4': 1, '5': 14, '6': '.veilidchat.Availability', '10': 'availability'}, - {'1': 'avatar', '3': 5, '4': 1, '5': 11, '6': '.veilid.TypedKey', '9': 0, '10': 'avatar', '17': true}, - ], - '8': [ - {'1': '_avatar'}, - ], -}; - -/// Descriptor for `Profile`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List profileDescriptor = $convert.base64Decode( - 'CgdQcm9maWxlEhIKBG5hbWUYASABKAlSBG5hbWUSGgoIcHJvbm91bnMYAiABKAlSCHByb25vdW' - '5zEhYKBnN0YXR1cxgDIAEoCVIGc3RhdHVzEjwKDGF2YWlsYWJpbGl0eRgEIAEoDjIYLnZlaWxp' - 'ZGNoYXQuQXZhaWxhYmlsaXR5UgxhdmFpbGFiaWxpdHkSLQoGYXZhdGFyGAUgASgLMhAudmVpbG' - 'lkLlR5cGVkS2V5SABSBmF2YXRhcogBAUIJCgdfYXZhdGFy'); - -@$core.Deprecated('Use chatDescriptor instead') -const Chat$json = { - '1': 'Chat', - '2': [ - {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.veilidchat.ChatType', '10': 'type'}, - {'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( - 'CgRDaGF0EigKBHR5cGUYASABKA4yFC52ZWlsaWRjaGF0LkNoYXRUeXBlUgR0eXBlElUKHnJlbW' - '90ZV9jb252ZXJzYXRpb25fcmVjb3JkX2tleRgCIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIbcmVt' - 'b3RlQ29udmVyc2F0aW9uUmVjb3JkS2V5ElAKFnJlY29uY2lsZWRfY2hhdF9yZWNvcmQYAyABKA' - 'syGi5kaHQuT3duZWRESFRSZWNvcmRQb2ludGVyUhRyZWNvbmNpbGVkQ2hhdFJlY29yZA=='); - -@$core.Deprecated('Use accountDescriptor instead') -const Account$json = { - '1': 'Account', - '2': [ - {'1': 'profile', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'profile'}, - {'1': 'invisible', '3': 2, '4': 1, '5': 8, '10': 'invisible'}, - {'1': 'auto_away_timeout_sec', '3': 3, '4': 1, '5': 13, '10': 'autoAwayTimeoutSec'}, - {'1': 'contact_list', '3': 4, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'contactList'}, - {'1': 'contact_invitation_records', '3': 5, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'contactInvitationRecords'}, - {'1': 'chat_list', '3': 6, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'chatList'}, - ], -}; - -/// Descriptor for `Account`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List accountDescriptor = $convert.base64Decode( - 'CgdBY2NvdW50Ei0KB3Byb2ZpbGUYASABKAsyEy52ZWlsaWRjaGF0LlByb2ZpbGVSB3Byb2ZpbG' - 'USHAoJaW52aXNpYmxlGAIgASgIUglpbnZpc2libGUSMQoVYXV0b19hd2F5X3RpbWVvdXRfc2Vj' - 'GAMgASgNUhJhdXRvQXdheVRpbWVvdXRTZWMSPQoMY29udGFjdF9saXN0GAQgASgLMhouZGh0Lk' - '93bmVkREhUUmVjb3JkUG9pbnRlclILY29udGFjdExpc3QSWAoaY29udGFjdF9pbnZpdGF0aW9u' - 'X3JlY29yZHMYBSABKAsyGi5kaHQuT3duZWRESFRSZWNvcmRQb2ludGVyUhhjb250YWN0SW52aX' - 'RhdGlvblJlY29yZHMSNwoJY2hhdF9saXN0GAYgASgLMhouZGh0Lk93bmVkREhUUmVjb3JkUG9p' - 'bnRlclIIY2hhdExpc3Q='); - @$core.Deprecated('Use contactInvitationDescriptor instead') const ContactInvitation$json = { '1': 'ContactInvitation', diff --git a/lib/proto/veilidchat.proto b/lib/proto/veilidchat.proto index 42692ac..eb6d08a 100644 --- a/lib/proto/veilidchat.proto +++ b/lib/proto/veilidchat.proto @@ -1,51 +1,230 @@ +//////////////////////////////////////////////////////////////////////////////////// +// VeilidChat Protocol Buffer Definitions +// +// * Timestamps are in microseconds (us) since epoch +// * Durations are in microseconds (us) +//////////////////////////////////////////////////////////////////////////////////// + syntax = "proto3"; package veilidchat; import "veilid.proto"; import "dht.proto"; -// AttachmentKind -// Enumeration of well-known attachment types -enum AttachmentKind { - ATTACHMENT_KIND_UNSPECIFIED = 0; - ATTACHMENT_KIND_FILE = 1; - ATTACHMENT_KIND_IMAGE = 2; +//////////////////////////////////////////////////////////////////////////////////// +// Enumerations +//////////////////////////////////////////////////////////////////////////////////// + +// Contact availability +enum Availability { + AVAILABILITY_UNSPECIFIED = 0; + AVAILABILITY_OFFLINE = 1; + AVAILABILITY_FREE = 2; + AVAILABILITY_BUSY = 3; + AVAILABILITY_AWAY = 4; } +// Encryption used on secret keys +enum EncryptionKeyType { + ENCRYPTION_KEY_TYPE_UNSPECIFIED = 0; + ENCRYPTION_KEY_TYPE_NONE = 1; + ENCRYPTION_KEY_TYPE_PIN = 2; + ENCRYPTION_KEY_TYPE_PASSWORD = 3; +} + +// Scope of a chat +enum Scope { + // Can read chats but not send messages + WATCHERS = 0; + // Can send messages subject to moderation + // If moderation is disabled, this is equivalent to WATCHERS + MODERATED = 1; + // Can send messages without moderation + TALKERS = 2; + // Can moderate messages sent my members if moderation is enabled + MODERATORS = 3; + // Can perform all actions + ADMINS = 4; +} + +//////////////////////////////////////////////////////////////////////////////////// +// Attachments +//////////////////////////////////////////////////////////////////////////////////// + // A single attachment message Attachment { - // Type of the data - AttachmentKind kind = 1; - // MIME type of the data - string mime = 2; - // Title or filename - string name = 3; - // Pointer to the data content - dht.DataReference content = 4; + oneof kind { + AttachmentMedia media = 1; + } // Author signature over all attachment fields and content fields and bytes - veilid.Signature signature = 5; + veilid.Signature signature = 2; } +// A file, audio, image, or video attachment +message AttachmentMedia { + // MIME type of the data + string mime = 1; + // Title or filename + string name = 2; + // Pointer to the data content + dht.DataReference content = 3; +} + + +//////////////////////////////////////////////////////////////////////////////////// +// Chat room controls +//////////////////////////////////////////////////////////////////////////////////// + +// Permissions of a chat +message Permissions { + // Parties in this scope or higher can add members to their own group or lower + Scope can_add_members = 1; + // Parties in this scope or higher can change the 'info' of a group + Scope can_edit_info = 2; + // If moderation is enabled or not. + bool moderated = 3; +} + +// The membership of a chat +message Membership { + // Conversation keys for parties in the 'watchers' group + repeated veilid.TypedKey watchers = 1; + // Conversation keys for parties in the 'moderated' group + repeated veilid.TypedKey moderated = 2; + // Conversation keys for parties in the 'talkers' group + repeated veilid.TypedKey talkers = 3; + // Conversation keys for parties in the 'moderators' group + repeated veilid.TypedKey moderators = 4; + // Conversation keys for parties in the 'admins' group + repeated veilid.TypedKey admins = 5; +} + +// The chat settings +message ChatSettings { + // Title for the chat + string title = 1; + // Description for the chat + string description = 2; + // Icon for the chat + optional dht.DataReference icon = 3; + // Default message expiration duration (in us) + uint64 default_expiration = 4; +} + +//////////////////////////////////////////////////////////////////////////////////// +// Messages +//////////////////////////////////////////////////////////////////////////////////// + // A single message as part of a series of messages message Message { - // Author of the message - veilid.TypedKey author = 1; - // Time the message was sent (us since epoch) - uint64 timestamp = 2; - // Text of the message - string text = 3; + + // A text message + message Text { + // Text of the message + string text = 1; + // Topic of the message / Content warning + string topic = 2; + // Message id replied to + veilid.TypedKey reply_id = 3; + // Message expiration timestamp + uint64 expiration = 4; + // Message view limit before deletion + uint64 view_limit = 5; + // Attachments on the message + repeated Attachment attachments = 6; + } + + // A secret message + message Secret { + // Text message protobuf encrypted by a key + bytes ciphertext = 1; + // Secret expiration timestamp + // This is the time after which an un-revealed secret will get deleted + uint64 expiration = 2; + } + + // A 'delete' control message + // Deletes a set of messages by their ids + message ControlDelete { + repeated veilid.TypedKey ids = 1; + } + // A 'clear' control message + // Deletes a set of messages from before some timestamp + message ControlClear { + // The latest timestamp to delete messages before + // If this is zero then all messages are cleared + uint64 timestamp = 1; + } + // A 'change settings' control message + message ControlSettings { + ChatSettings settings = 1; + } + + // A 'change permissions' control message + // Changes the permissions of a chat + message ControlPermissions { + Permissions permissions = 1; + } + + // A 'change membership' control message + // Changes the + message ControlMembership { + Membership membership = 1; + } + + // A 'moderation' control message + // Accepts or rejects a set of messages + message ControlModeration { + repeated veilid.TypedKey accepted_ids = 1; + repeated veilid.TypedKey rejected_ids = 2; + } + + ////////////////////////////////////////////////////////////////////////// + + // Hash of previous message from the same author, + // including its previous hash. + // Also serves as a unique key for the message. + veilid.TypedKey id = 1; + // Author of the message (identity public key) + veilid.TypedKey author = 2; + // Time the message was sent according to sender + uint64 timestamp = 3; + + // Message kind + oneof kind { + Text text = 4; + Secret secret = 5; + ControlDelete delete = 6; + ControlClear clear = 7; + ControlSettings settings = 8; + ControlPermissions permissions = 9; + ControlMembership membership = 10; + ControlModeration moderation = 11; + } + // Author signature over all of the fields and attachment signatures - veilid.Signature signature = 4; - // Attachments on the message - repeated Attachment attachments = 5; + veilid.Signature signature = 12; } +// Locally stored messages for chats +message ReconciledMessage { + // The message as sent + Message content = 1; + // The timestamp the message was reconciled + uint64 reconciled_time = 2; +} + +//////////////////////////////////////////////////////////////////////////////////// +// Chats +//////////////////////////////////////////////////////////////////////////////////// + // 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 +// * Group chat messages // // DHT Schema: SMPL(0,1,[identityPublicKey]) // DHT Key (UnicastOutbox): localConversation @@ -54,12 +233,84 @@ message Message { message Conversation { // Profile to publish to friend Profile profile = 1; - // Identity master (JSON) to publish to friend + // Identity master (JSON) to publish to friend or chat room string identity_master_json = 2; - // Messages DHTLog (xxx for now DHTShortArray) + // Messages DHTLog veilid.TypedKey messages = 3; } +// Either a 1-1 conversation or a group chat +// Privately encrypted, this is the local user's copy of the chat +message Chat { + // Settings + ChatSettings settings = 1; + // Conversation key for this user + veilid.TypedKey local_conversation_record_key = 2; + // Conversation key for the other party + veilid.TypedKey remote_conversation_record_key = 3; +} + +// A group chat +// Privately encrypted, this is the local user's copy of the chat +message GroupChat { + // Settings + ChatSettings settings = 1; + // Conversation key for this user + veilid.TypedKey local_conversation_record_key = 2; + // Conversation keys for the other parties + repeated veilid.TypedKey remote_conversation_record_keys = 3; +} + +//////////////////////////////////////////////////////////////////////////////////// +// Accounts +//////////////////////////////////////////////////////////////////////////////////// + +// Publicly shared profile information for both contacts and accounts +// Contains: +// Name - Friendly name +// Pronouns - Pronouns of user +// Icon - Little picture to represent user in contact list +message Profile { + // Friendy name + string name = 1; + // Pronouns of user + string pronouns = 2; + // Description of the user + string about = 3; + // Status/away message + string status = 4; + // Availability + Availability availability = 5; + // Avatar DHTData + optional veilid.TypedKey avatar = 6; +} + +// A record of an individual account +// Pointed to by the identity account map in the identity key +// +// DHT Schema: DFLT(1) +// DHT Private: accountSecretKey +message Account { + // The user's profile that gets shared with contacts + Profile profile = 1; + // Invisibility makes you always look 'Offline' + bool invisible = 2; + // Auto-away sets 'away' mode after an inactivity time + uint32 auto_away_timeout_sec = 3; + // The contacts DHTList for this account + // DHT Private + dht.OwnedDHTRecordPointer contact_list = 4; + // The ContactInvitationRecord DHTShortArray for this account + // DHT Private + dht.OwnedDHTRecordPointer contact_invitation_records = 5; + // The Chats DHTList for this account + // DHT Private + dht.OwnedDHTRecordPointer chat_list = 6; + // The GroupChats DHTList for this account + // DHT Private + dht.OwnedDHTRecordPointer group_chat_list = 7; +} + // A record of a contact that has accepted a contact invitation // Contains a copy of the most recent remote profile as well as // a locally edited profile. @@ -80,87 +331,13 @@ message Contact { veilid.TypedKey remote_conversation_record_key = 5; // Our conversation key for friend to sync veilid.TypedKey local_conversation_record_key = 6; - // Show availability + // Show availability to this contact bool show_availability = 7; } -// Contact availability -enum Availability { - AVAILABILITY_UNSPECIFIED = 0; - AVAILABILITY_OFFLINE = 1; - AVAILABILITY_FREE = 2; - AVAILABILITY_BUSY = 3; - AVAILABILITY_AWAY = 4; -} - -// Publicly shared profile information for both contacts and accounts -// Contains: -// Name - Friendly name -// Pronouns - Pronouns of user -// Icon - Little picture to represent user in contact list -message Profile { - // Friendy name - string name = 1; - // Pronouns of user - string pronouns = 2; - // Status/away message - string status = 3; - // Availability - Availability availability = 4; - // Avatar DHTData - optional veilid.TypedKey avatar = 5; -} - - -enum ChatType { - CHAT_TYPE_UNSPECIFIED = 0; - SINGLE_CONTACT = 1; - GROUP = 2; -} - -// 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; - // 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 -// Pointed to by the identity account map in the identity key -// -// DHT Schema: DFLT(1) -// DHT Private: accountSecretKey -message Account { - // The user's profile that gets shared with contacts - Profile profile = 1; - // Invisibility makes you always look 'Offline' - bool invisible = 2; - // Auto-away sets 'away' mode after an inactivity time - uint32 auto_away_timeout_sec = 3; - // The contacts DHTList for this account - // DHT Private - dht.OwnedDHTRecordPointer contact_list = 4; - // The ContactInvitationRecord DHTShortArray for this account - // DHT Private - dht.OwnedDHTRecordPointer contact_invitation_records = 5; - // The chats DHTList for this account - // DHT Private - dht.OwnedDHTRecordPointer chat_list = 6; - -} - -// EncryptionKeyType -// Encryption of secret -enum EncryptionKeyType { - ENCRYPTION_KEY_TYPE_UNSPECIFIED = 0; - ENCRYPTION_KEY_TYPE_NONE = 1; - ENCRYPTION_KEY_TYPE_PIN = 2; - ENCRYPTION_KEY_TYPE_PASSWORD = 3; -} +//////////////////////////////////////////////////////////////////////////////////// +// Invitations +//////////////////////////////////////////////////////////////////////////////////// // Invitation that is shared for VeilidChat contact connections // serialized to QR code or data blob, not send over DHT, out of band. diff --git a/lib/theme/views/widget_helpers.dart b/lib/theme/views/widget_helpers.dart index 6edca24..52f26ac 100644 --- a/lib/theme/views/widget_helpers.dart +++ b/lib/theme/views/widget_helpers.dart @@ -44,7 +44,7 @@ Widget waitingPage({String? text}) => Builder(builder: (context) { final theme = Theme.of(context); final scale = theme.extension()!; return ColoredBox( - color: scale.tertiaryScale.primaryText, + color: scale.tertiaryScale.appBackground, child: Center( child: Column(children: [ buildProgressIndicator().expanded(), diff --git a/packages/veilid_support/example/integration_test/app_test.dart b/packages/veilid_support/example/integration_test/app_test.dart index 83e3dc8..1577d7a 100644 --- a/packages/veilid_support/example/integration_test/app_test.dart +++ b/packages/veilid_support/example/integration_test/app_test.dart @@ -7,6 +7,7 @@ import 'fixtures/fixtures.dart'; import 'test_dht_log.dart'; import 'test_dht_record_pool.dart'; import 'test_dht_short_array.dart'; +import 'test_table_db_array.dart'; void main() { final startTime = DateTime.now(); @@ -34,6 +35,17 @@ void main() { setUpAll(veilidFixture.attach); tearDownAll(veilidFixture.detach); + group('TableDB Tests', () { + group('TableDBArray Tests', () { + test('create TableDBArray', makeTestTableDBArrayCreateDelete()); + test( + timeout: const Timeout(Duration(seconds: 480)), + 'add/truncate TableDBArray', + makeTestDHTLogAddTruncate(), + ); + }); + }); + group('DHT Support Tests', () { setUpAll(updateProcessorFixture.setUp); setUpAll(tickerFixture.setUp); diff --git a/packages/veilid_support/example/integration_test/test_dht_log.dart b/packages/veilid_support/example/integration_test/test_dht_log.dart index 0e5829c..0c06c87 100644 --- a/packages/veilid_support/example/integration_test/test_dht_log.dart +++ b/packages/veilid_support/example/integration_test/test_dht_log.dart @@ -64,8 +64,7 @@ Future Function() makeTestDHTLogAddTruncate({required int stride}) => const chunk = 25; for (var n = 0; n < dataset.length; n += chunk) { print('$n-${n + chunk - 1} '); - final success = - await w.tryAppendItems(dataset.sublist(n, n + chunk)); + final success = await w.tryAddItems(dataset.sublist(n, n + chunk)); expect(success, isTrue); } }); @@ -94,7 +93,7 @@ Future Function() makeTestDHTLogAddTruncate({required int stride}) => } print('truncate\n'); { - await dlog.operateAppend((w) async => w.truncate(5)); + await dlog.operateAppend((w) async => w.truncate(w.length - 5)); } { final dataset6 = await dlog @@ -103,7 +102,7 @@ Future Function() makeTestDHTLogAddTruncate({required int stride}) => } print('truncate 2\n'); { - await dlog.operateAppend((w) async => w.truncate(251)); + await dlog.operateAppend((w) async => w.truncate(w.length - 251)); } { final dataset7 = await dlog diff --git a/packages/veilid_support/example/integration_test/test_table_db_array.dart b/packages/veilid_support/example/integration_test/test_table_db_array.dart new file mode 100644 index 0000000..e9087f6 --- /dev/null +++ b/packages/veilid_support/example/integration_test/test_table_db_array.dart @@ -0,0 +1,134 @@ +import 'dart:convert'; + +import 'package:test/test.dart'; +import 'package:veilid_support/veilid_support.dart'; + +Future Function() makeTestTableDBArrayCreateDelete() => () async { + // Close before delete + { + final arr = await TableDBArray( + table: 'test', crypto: const VeilidCryptoPublic()); + + expect(await arr.operate((r) async => r.length), isZero); + expect(arr.isOpen, isTrue); + await arr.close(); + expect(arr.isOpen, isFalse); + await arr.delete(); + // Operate should fail + await expectLater(() async => arr.operate((r) async => r.length), + throwsA(isA())); + } + + // Close after delete + { + final arr = await DHTShortArray.create( + debugName: 'sa_create_delete 2 stride $stride', stride: stride); + await arr.delete(); + // Operate should still succeed because things aren't closed + expect(await arr.operate((r) async => r.length), isZero); + await arr.close(); + // Operate should fail + await expectLater(() async => arr.operate((r) async => r.length), + throwsA(isA())); + } + + // Close after delete multiple + // Okay to request delete multiple times before close + { + final arr = await DHTShortArray.create( + debugName: 'sa_create_delete 3 stride $stride', stride: stride); + await arr.delete(); + await arr.delete(); + // Operate should still succeed because things aren't closed + expect(await arr.operate((r) async => r.length), isZero); + await arr.close(); + await expectLater(() async => arr.close(), throwsA(isA())); + // Operate should fail + await expectLater(() async => arr.operate((r) async => r.length), + throwsA(isA())); + } + }; + +Future Function() makeTestTableDBArrayAdd({required int stride}) => + () async { + final arr = await DHTShortArray.create( + debugName: 'sa_add 1 stride $stride', stride: stride); + + final dataset = Iterable.generate(256) + .map((n) => utf8.encode('elem $n')) + .toList(); + + print('adding singles\n'); + { + final res = await arr.operateWrite((w) async { + for (var n = 4; n < 8; n++) { + print('$n '); + final success = await w.tryAddItem(dataset[n]); + expect(success, isTrue); + } + }); + expect(res, isNull); + } + + print('adding batch\n'); + { + final res = await arr.operateWrite((w) async { + print('${dataset.length ~/ 2}-${dataset.length}'); + final success = await w.tryAddItems( + dataset.sublist(dataset.length ~/ 2, dataset.length)); + expect(success, isTrue); + }); + expect(res, isNull); + } + + print('inserting singles\n'); + { + final res = await arr.operateWrite((w) async { + for (var n = 0; n < 4; n++) { + print('$n '); + final success = await w.tryInsertItem(n, dataset[n]); + expect(success, isTrue); + } + }); + expect(res, isNull); + } + + print('inserting batch\n'); + { + final res = await arr.operateWrite((w) async { + print('8-${dataset.length ~/ 2}'); + final success = await w.tryInsertItems( + 8, dataset.sublist(8, dataset.length ~/ 2)); + expect(success, isTrue); + }); + expect(res, isNull); + } + + //print('get all\n'); + { + final dataset2 = await arr.operate((r) async => r.getItemRange(0)); + expect(dataset2, equals(dataset)); + } + { + final dataset3 = + await arr.operate((r) async => r.getItemRange(64, length: 128)); + expect(dataset3, equals(dataset.sublist(64, 64 + 128))); + } + + //print('clear\n'); + { + await arr.operateWriteEventual((w) async { + await w.clear(); + return true; + }); + } + + //print('get all\n'); + { + final dataset4 = await arr.operate((r) async => r.getItemRange(0)); + expect(dataset4, isEmpty); + } + + await arr.delete(); + await arr.close(); + }; diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart index 3f561ff..cba15f4 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart @@ -9,11 +9,11 @@ import 'package:meta/meta.dart'; import '../../../veilid_support.dart'; import '../../proto/proto.dart' as proto; -import '../interfaces/dht_append_truncate.dart'; +import '../interfaces/dht_append.dart'; part 'dht_log_spine.dart'; part 'dht_log_read.dart'; -part 'dht_log_append.dart'; +part 'dht_log_write.dart'; /////////////////////////////////////////////////////////////////////// @@ -60,7 +60,7 @@ class DHTLog implements DHTDeleteable { int stride = DHTShortArray.maxElements, VeilidRoutingContext? routingContext, TypedKey? parent, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, KeyPair? writer}) async { assert(stride <= DHTShortArray.maxElements, 'stride too long'); final pool = DHTRecordPool.instance; @@ -102,7 +102,7 @@ class DHTLog implements DHTDeleteable { {required String debugName, VeilidRoutingContext? routingContext, TypedKey? parent, - DHTRecordCrypto? crypto}) async { + VeilidCrypto? crypto}) async { final spineRecord = await DHTRecordPool.instance.openRecordRead( logRecordKey, debugName: debugName, @@ -125,7 +125,7 @@ class DHTLog implements DHTDeleteable { required String debugName, VeilidRoutingContext? routingContext, TypedKey? parent, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, }) async { final spineRecord = await DHTRecordPool.instance.openRecordWrite( logRecordKey, writer, @@ -148,7 +148,7 @@ class DHTLog implements DHTDeleteable { required String debugName, required TypedKey parent, VeilidRoutingContext? routingContext, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, }) => openWrite( ownedLogRecordPointer.recordKey, @@ -209,7 +209,8 @@ class DHTLog implements DHTDeleteable { OwnedDHTRecordPointer get recordPointer => _spine.recordPointer; /// Runs a closure allowing read-only access to the log - Future operate(Future Function(DHTRandomRead) closure) async { + Future operate( + Future Function(DHTLogReadOperations) closure) async { if (!isOpen) { throw StateError('log is not open"'); } @@ -226,13 +227,13 @@ class DHTLog implements DHTDeleteable { /// Throws DHTOperateException if the write could not be performed /// at this time Future operateAppend( - Future Function(DHTAppendTruncateRandomRead) closure) async { + Future Function(DHTLogWriteOperations) closure) async { if (!isOpen) { throw StateError('log is not open"'); } return _spine.operateAppend((spine) async { - final writer = _DHTLogAppend._(spine); + final writer = _DHTLogWrite._(spine); return closure(writer); }); } @@ -244,14 +245,14 @@ class DHTLog implements DHTDeleteable { /// succeeded, returning false will trigger another eventual consistency /// attempt. Future operateAppendEventual( - Future Function(DHTAppendTruncateRandomRead) closure, + Future Function(DHTLogWriteOperations) closure, {Duration? timeout}) async { if (!isOpen) { throw StateError('log is not open"'); } return _spine.operateAppendEventual((spine) async { - final writer = _DHTLogAppend._(spine); + final writer = _DHTLogWrite._(spine); return closure(writer); }, timeout: timeout); } diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart index 30bac27..3c054fc 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart @@ -8,7 +8,6 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:meta/meta.dart'; import '../../../veilid_support.dart'; -import '../interfaces/dht_append_truncate.dart'; @immutable class DHTLogElementState extends Equatable { @@ -184,19 +183,20 @@ class DHTLogCubit extends Cubit> await super.close(); } - Future operate(Future Function(DHTRandomRead) closure) async { + Future operate( + Future Function(DHTLogReadOperations) closure) async { await _initWait(); return _log.operate(closure); } Future operateAppend( - Future Function(DHTAppendTruncateRandomRead) closure) async { + Future Function(DHTLogWriteOperations) closure) async { await _initWait(); return _log.operateAppend(closure); } Future operateAppendEventual( - Future Function(DHTAppendTruncateRandomRead) closure, + Future Function(DHTLogWriteOperations) closure, {Duration? timeout}) async { await _initWait(); return _log.operateAppendEventual(closure, timeout: timeout); diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart index 3618abd..0a66a01 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart @@ -3,7 +3,9 @@ part of 'dht_log.dart'; //////////////////////////////////////////////////////////////////////////// // Reader-only implementation -class _DHTLogRead implements DHTRandomRead { +abstract class DHTLogReadOperations implements DHTRandomRead {} + +class _DHTLogRead implements DHTLogReadOperations { _DHTLogRead._(_DHTLogSpine spine) : _spine = spine; @override diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_append.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart similarity index 67% rename from packages/veilid_support/lib/dht_support/src/dht_log/dht_log_append.dart rename to packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart index c184032..5503051 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_append.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart @@ -1,13 +1,32 @@ part of 'dht_log.dart'; //////////////////////////////////////////////////////////////////////////// -// Append/truncate implementation +// Writer implementation -class _DHTLogAppend extends _DHTLogRead implements DHTAppendTruncateRandomRead { - _DHTLogAppend._(super.spine) : super._(); +abstract class DHTLogWriteOperations + implements DHTRandomRead, DHTRandomWrite, DHTAdd, DHTTruncate, DHTClear {} + +class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { + _DHTLogWrite._(super.spine) : super._(); @override - Future tryAppendItem(Uint8List value) async { + Future tryWriteItem(int pos, Uint8List newValue, + {Output? output}) async { + if (pos < 0 || pos >= _spine.length) { + throw IndexError.withLength(pos, _spine.length); + } + final lookup = await _spine.lookupPosition(pos); + if (lookup == null) { + throw StateError("can't write to dht log"); + } + + // Write item to the segment + return lookup.scope((sa) => sa.operateWrite((write) async => + write.tryWriteItem(lookup.pos, newValue, output: output))); + } + + @override + Future tryAddItem(Uint8List value) async { // Allocate empty index at the end of the list final insertPos = _spine.length; _spine.allocateTail(1); @@ -30,7 +49,7 @@ class _DHTLogAppend extends _DHTLogRead implements DHTAppendTruncateRandomRead { } @override - Future tryAppendItems(List values) async { + Future tryAddItems(List values) async { // Allocate empty index at the end of the list final insertPos = _spine.length; _spine.allocateTail(values.length); @@ -76,15 +95,14 @@ class _DHTLogAppend extends _DHTLogRead implements DHTAppendTruncateRandomRead { } @override - Future truncate(int count) async { - count = min(count, _spine.length); - if (count == 0) { + Future truncate(int newLength) async { + if (newLength < 0) { + throw StateError('can not truncate to negative length'); + } + if (newLength >= _spine.length) { return; } - if (count < 0) { - throw StateError('can not remove negative items'); - } - await _spine.releaseHead(count); + await _spine.releaseHead(_spine.length - newLength); } @override 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 index 2b6736e..06933be 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/barrel.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/barrel.dart @@ -1,4 +1,3 @@ export 'default_dht_record_cubit.dart'; -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/dht_record.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record.dart index 80b68ad..521bf1f 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 @@ -42,7 +42,7 @@ class DHTRecord implements DHTDeleteable { required SharedDHTRecordData sharedDHTRecordData, required int defaultSubkey, required KeyPair? writer, - required DHTRecordCrypto crypto, + required VeilidCrypto crypto, required this.debugName}) : _crypto = crypto, _routingContext = routingContext, @@ -104,7 +104,7 @@ class DHTRecord implements DHTDeleteable { int get subkeyCount => _sharedDHTRecordData.recordDescriptor.schema.subkeyCount(); KeyPair? get writer => _writer; - DHTRecordCrypto get crypto => _crypto; + VeilidCrypto get crypto => _crypto; OwnedDHTRecordPointer get ownedDHTRecordPointer => OwnedDHTRecordPointer(recordKey: key, owner: ownerKeyPair!); int subkeyOrDefault(int subkey) => (subkey == -1) ? _defaultSubkey : subkey; @@ -118,7 +118,7 @@ class DHTRecord implements DHTDeleteable { /// returned if one was returned. Future get( {int subkey = -1, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, DHTRecordRefreshMode refreshMode = DHTRecordRefreshMode.cached, Output? outSeqNum}) async { subkey = subkeyOrDefault(subkey); @@ -146,7 +146,7 @@ class DHTRecord implements DHTDeleteable { return null; } // If we're returning a value, decrypt it - final out = (crypto ?? _crypto).decrypt(valueData.data, subkey); + final out = (crypto ?? _crypto).decrypt(valueData.data); if (outSeqNum != null) { outSeqNum.save(valueData.seq); } @@ -163,7 +163,7 @@ class DHTRecord implements DHTDeleteable { /// returned if one was returned. Future getJson(T Function(dynamic) fromJson, {int subkey = -1, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, DHTRecordRefreshMode refreshMode = DHTRecordRefreshMode.cached, Output? outSeqNum}) async { final data = await get( @@ -189,7 +189,7 @@ class DHTRecord implements DHTDeleteable { Future getProtobuf( T Function(List i) fromBuffer, {int subkey = -1, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, DHTRecordRefreshMode refreshMode = DHTRecordRefreshMode.cached, Output? outSeqNum}) async { final data = await get( @@ -208,13 +208,12 @@ class DHTRecord implements DHTDeleteable { /// If the value was succesfully written, null is returned Future tryWriteBytes(Uint8List newValue, {int subkey = -1, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, KeyPair? writer, Output? outSeqNum}) async { subkey = subkeyOrDefault(subkey); final lastSeq = await _localSubkeySeq(subkey); - final encryptedNewValue = - await (crypto ?? _crypto).encrypt(newValue, subkey); + final encryptedNewValue = await (crypto ?? _crypto).encrypt(newValue); // Set the new data if possible var newValueData = await _routingContext @@ -246,7 +245,7 @@ class DHTRecord implements DHTDeleteable { // Decrypt value to return it final decryptedNewValue = - await (crypto ?? _crypto).decrypt(newValueData.data, subkey); + await (crypto ?? _crypto).decrypt(newValueData.data); if (isUpdated) { DHTRecordPool.instance .processLocalValueChange(key, decryptedNewValue, subkey); @@ -259,13 +258,12 @@ class DHTRecord implements DHTDeleteable { /// will be made to write the subkey until this succeeds Future eventualWriteBytes(Uint8List newValue, {int subkey = -1, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, KeyPair? writer, Output? outSeqNum}) async { subkey = subkeyOrDefault(subkey); final lastSeq = await _localSubkeySeq(subkey); - final encryptedNewValue = - await (crypto ?? _crypto).encrypt(newValue, subkey); + final encryptedNewValue = await (crypto ?? _crypto).encrypt(newValue); ValueData? newValueData; do { @@ -309,7 +307,7 @@ class DHTRecord implements DHTDeleteable { Future eventualUpdateBytes( Future Function(Uint8List? oldValue) update, {int subkey = -1, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, KeyPair? writer, Output? outSeqNum}) async { subkey = subkeyOrDefault(subkey); @@ -334,7 +332,7 @@ class DHTRecord implements DHTDeleteable { /// Like 'tryWriteBytes' but with JSON marshal/unmarshal of the value Future tryWriteJson(T Function(dynamic) fromJson, T newValue, {int subkey = -1, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, KeyPair? writer, Output? outSeqNum}) => tryWriteBytes(jsonEncodeBytes(newValue), @@ -353,7 +351,7 @@ class DHTRecord implements DHTDeleteable { Future tryWriteProtobuf( T Function(List) fromBuffer, T newValue, {int subkey = -1, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, KeyPair? writer, Output? outSeqNum}) => tryWriteBytes(newValue.writeToBuffer(), @@ -371,7 +369,7 @@ class DHTRecord implements DHTDeleteable { /// Like 'eventualWriteBytes' but with JSON marshal/unmarshal of the value Future eventualWriteJson(T newValue, {int subkey = -1, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, KeyPair? writer, Output? outSeqNum}) => eventualWriteBytes(jsonEncodeBytes(newValue), @@ -380,7 +378,7 @@ class DHTRecord implements DHTDeleteable { /// Like 'eventualWriteBytes' but with protobuf marshal/unmarshal of the value Future eventualWriteProtobuf(T newValue, {int subkey = -1, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, KeyPair? writer, Output? outSeqNum}) => eventualWriteBytes(newValue.writeToBuffer(), @@ -390,7 +388,7 @@ class DHTRecord implements DHTDeleteable { Future eventualUpdateJson( T Function(dynamic) fromJson, Future Function(T?) update, {int subkey = -1, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, KeyPair? writer, Output? outSeqNum}) => eventualUpdateBytes(jsonUpdate(fromJson, update), @@ -400,7 +398,7 @@ class DHTRecord implements DHTDeleteable { Future eventualUpdateProtobuf( T Function(List) fromBuffer, Future Function(T?) update, {int subkey = -1, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, KeyPair? writer, Output? outSeqNum}) => eventualUpdateBytes(protobufUpdate(fromBuffer, update), @@ -433,7 +431,7 @@ class DHTRecord implements DHTDeleteable { DHTRecord record, Uint8List? data, List subkeys) onUpdate, { bool localChanges = true, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, }) async { // Set up watch requirements _watchController ??= @@ -457,8 +455,7 @@ class DHTRecord implements DHTDeleteable { final changeData = change.data; data = changeData == null ? null - : await (crypto ?? _crypto) - .decrypt(changeData, change.subkeys.first.low); + : await (crypto ?? _crypto).decrypt(changeData); } await onUpdate(this, data, change.subkeys); }); @@ -544,7 +541,7 @@ class DHTRecord implements DHTDeleteable { final VeilidRoutingContext _routingContext; final int _defaultSubkey; final KeyPair? _writer; - final DHTRecordCrypto _crypto; + final VeilidCrypto _crypto; final String debugName; final _mutex = Mutex(); int _openCount; diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_crypto.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_crypto.dart deleted file mode 100644 index 0e69078..0000000 --- a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_crypto.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; -import '../../../../../veilid_support.dart'; - -abstract class DHTRecordCrypto { - Future encrypt(Uint8List data, int subkey); - Future decrypt(Uint8List data, int subkey); -} - -//////////////////////////////////// -/// Private DHT Record: Encrypted for a specific symmetric key -class DHTRecordCryptoPrivate implements DHTRecordCrypto { - DHTRecordCryptoPrivate._( - VeilidCryptoSystem cryptoSystem, SharedSecret secretKey) - : _cryptoSystem = cryptoSystem, - _secretKey = secretKey; - final VeilidCryptoSystem _cryptoSystem; - final SharedSecret _secretKey; - - static Future fromTypedKeyPair( - TypedKeyPair typedKeyPair) async { - final cryptoSystem = - await Veilid.instance.getCryptoSystem(typedKeyPair.kind); - final secretKey = typedKeyPair.secret; - return DHTRecordCryptoPrivate._(cryptoSystem, secretKey); - } - - static Future fromSecret( - CryptoKind kind, SharedSecret secretKey) async { - final cryptoSystem = await Veilid.instance.getCryptoSystem(kind); - return DHTRecordCryptoPrivate._(cryptoSystem, secretKey); - } - - @override - Future encrypt(Uint8List data, int subkey) => - _cryptoSystem.encryptNoAuthWithNonce(data, _secretKey); - - @override - Future decrypt(Uint8List data, int subkey) => - _cryptoSystem.decryptNoAuthWithNonce(data, _secretKey); -} - -//////////////////////////////////// -/// Public DHT Record: No encryption -class DHTRecordCryptoPublic implements DHTRecordCrypto { - const DHTRecordCryptoPublic(); - - @override - Future encrypt(Uint8List data, int subkey) async => data; - - @override - Future decrypt(Uint8List data, int subkey) async => data; -} 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 a8e86a1..440698a 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 @@ -526,7 +526,7 @@ class DHTRecordPool with TableDBBackedJson { TypedKey? parent, DHTSchema schema = const DHTSchema.dflt(oCnt: 1), int defaultSubkey = 0, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, KeyPair? writer, }) async => _mutex.protect(() async { @@ -547,7 +547,7 @@ class DHTRecordPool with TableDBBackedJson { writer: writer ?? openedRecordInfo.shared.recordDescriptor.ownerKeyPair(), crypto: crypto ?? - await DHTRecordCryptoPrivate.fromTypedKeyPair(openedRecordInfo + await VeilidCryptoPrivate.fromTypedKeyPair(openedRecordInfo .shared.recordDescriptor .ownerTypedKeyPair()!)); @@ -562,7 +562,7 @@ class DHTRecordPool with TableDBBackedJson { VeilidRoutingContext? routingContext, TypedKey? parent, int defaultSubkey = 0, - DHTRecordCrypto? crypto}) async => + VeilidCrypto? crypto}) async => _mutex.protect(() async { final dhtctx = routingContext ?? _routingContext; @@ -578,7 +578,7 @@ class DHTRecordPool with TableDBBackedJson { defaultSubkey: defaultSubkey, sharedDHTRecordData: openedRecordInfo.shared, writer: null, - crypto: crypto ?? const DHTRecordCryptoPublic()); + crypto: crypto ?? const VeilidCryptoPublic()); openedRecordInfo.records.add(rec); @@ -593,7 +593,7 @@ class DHTRecordPool with TableDBBackedJson { VeilidRoutingContext? routingContext, TypedKey? parent, int defaultSubkey = 0, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, }) async => _mutex.protect(() async { final dhtctx = routingContext ?? _routingContext; @@ -612,7 +612,7 @@ class DHTRecordPool with TableDBBackedJson { writer: writer, sharedDHTRecordData: openedRecordInfo.shared, crypto: crypto ?? - await DHTRecordCryptoPrivate.fromTypedKeyPair( + await VeilidCryptoPrivate.fromTypedKeyPair( TypedKeyPair.fromKeyPair(recordKey.kind, writer))); openedRecordInfo.records.add(rec); @@ -632,7 +632,7 @@ class DHTRecordPool with TableDBBackedJson { required TypedKey parent, VeilidRoutingContext? routingContext, int defaultSubkey = 0, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, }) => openRecordWrite( ownedDHTRecordPointer.recordKey, 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 daf3061..0732255 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 @@ -33,7 +33,7 @@ class DHTShortArray implements DHTDeleteable { int stride = maxElements, VeilidRoutingContext? routingContext, TypedKey? parent, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, KeyPair? writer}) async { assert(stride <= maxElements, 'stride too long'); final pool = DHTRecordPool.instance; @@ -79,7 +79,7 @@ class DHTShortArray implements DHTDeleteable { {required String debugName, VeilidRoutingContext? routingContext, TypedKey? parent, - DHTRecordCrypto? crypto}) async { + VeilidCrypto? crypto}) async { final dhtRecord = await DHTRecordPool.instance.openRecordRead(headRecordKey, debugName: debugName, parent: parent, @@ -101,7 +101,7 @@ class DHTShortArray implements DHTDeleteable { required String debugName, VeilidRoutingContext? routingContext, TypedKey? parent, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, }) async { final dhtRecord = await DHTRecordPool.instance.openRecordWrite( headRecordKey, writer, @@ -124,7 +124,7 @@ class DHTShortArray implements DHTDeleteable { required String debugName, required TypedKey parent, VeilidRoutingContext? routingContext, - DHTRecordCrypto? crypto, + VeilidCrypto? crypto, }) => openWrite( ownedShortArrayRecordPointer.recordKey, @@ -186,7 +186,8 @@ class DHTShortArray implements DHTDeleteable { OwnedDHTRecordPointer get recordPointer => _head.recordPointer; /// Runs a closure allowing read-only access to the shortarray - Future operate(Future Function(DHTRandomRead) closure) async { + Future operate( + Future Function(DHTShortArrayReadOperations) closure) async { if (!isOpen) { throw StateError('short array is not open"'); } @@ -203,7 +204,7 @@ class DHTShortArray implements DHTDeleteable { /// Throws DHTOperateException if the write could not be performed /// at this time Future operateWrite( - Future Function(DHTRandomReadWrite) closure) async { + Future Function(DHTShortArrayWriteOperations) closure) async { if (!isOpen) { throw StateError('short array is not open"'); } @@ -221,7 +222,7 @@ class DHTShortArray implements DHTDeleteable { /// succeeded, returning false will trigger another eventual consistency /// attempt. Future operateWriteEventual( - Future Function(DHTRandomReadWrite) closure, + Future Function(DHTShortArrayWriteOperations) closure, {Duration? timeout}) async { if (!isOpen) { throw StateError('short array is not open"'); 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 e0b2504..f4b806e 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 @@ -91,19 +91,20 @@ class DHTShortArrayCubit extends Cubit> await super.close(); } - Future operate(Future Function(DHTRandomRead) closure) async { + Future operate( + Future Function(DHTShortArrayReadOperations) closure) async { await _initWait(); return _shortArray.operate(closure); } Future operateWrite( - Future Function(DHTRandomReadWrite) closure) async { + Future Function(DHTShortArrayWriteOperations) closure) async { await _initWait(); return _shortArray.operateWrite(closure); } Future operateWriteEventual( - Future Function(DHTRandomReadWrite) closure, + Future Function(DHTShortArrayWriteOperations) closure, {Duration? timeout}) async { await _initWait(); return _shortArray.operateWriteEventual(closure, timeout: timeout); 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 index 6485c02..5da8cf8 100644 --- 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 @@ -3,7 +3,9 @@ part of 'dht_short_array.dart'; //////////////////////////////////////////////////////////////////////////// // Reader-only implementation -class _DHTShortArrayRead implements DHTRandomRead { +abstract class DHTShortArrayReadOperations implements DHTRandomRead {} + +class _DHTShortArrayRead implements DHTShortArrayReadOperations { _DHTShortArrayRead._(_DHTShortArrayHead head) : _head = head; @override 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 index df93b59..c336e47 100644 --- 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 @@ -3,8 +3,16 @@ part of 'dht_short_array.dart'; //////////////////////////////////////////////////////////////////////////// // Writer implementation +abstract class DHTShortArrayWriteOperations + implements + DHTRandomRead, + DHTRandomWrite, + DHTInsertRemove, + DHTAdd, + DHTClear {} + class _DHTShortArrayWrite extends _DHTShortArrayRead - implements DHTRandomReadWrite { + implements DHTShortArrayWriteOperations { _DHTShortArrayWrite._(super.head) : super._(); @override diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_append.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_append.dart new file mode 100644 index 0000000..a1f47ee --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_append.dart @@ -0,0 +1,41 @@ +import 'dart:typed_data'; + +import 'package:protobuf/protobuf.dart'; + +import '../../../veilid_support.dart'; + +//////////////////////////////////////////////////////////////////////////// +// Add +abstract class DHTAdd { + /// Try to add an item to the DHT container. + /// 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. + /// Throws a StateError if the container exceeds its maximum size. + Future tryAddItem(Uint8List value); + + /// Try to add a list of items to the DHT container. + /// Return true if the elements were successfully added, and false if the + /// state changed before the element could be added or a newer value was found + /// on the network. + /// Throws a StateError if the container exceeds its maximum size. + Future tryAddItems(List values); +} + +extension DHTAddExt on DHTAdd { + /// Convenience function: + /// Like tryAddItem but also encodes the input value as JSON and parses the + /// returned element as JSON + Future tryAppendItemJson( + T newValue, + ) => + tryAddItem(jsonEncodeBytes(newValue)); + + /// Convenience function: + /// Like tryAddItem but also encodes the input value as a protobuf object + /// and parses the returned element as a protobuf object + Future tryAddItemProtobuf( + T newValue, + ) => + tryAddItem(newValue.writeToBuffer()); +} diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_append_truncate.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_append_truncate.dart deleted file mode 100644 index d98037c..0000000 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_append_truncate.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'dart:typed_data'; - -import 'package:protobuf/protobuf.dart'; - -import '../../../veilid_support.dart'; - -//////////////////////////////////////////////////////////////////////////// -// Append/truncate interface -abstract class DHTAppendTruncate { - /// Try to add an item to the end of the DHT data structure. - /// 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 limits. - Future tryAppendItem(Uint8List value); - - /// Try to add a list of items to the end of the DHT data structure. - /// Return true if the elements were 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 limits. - Future tryAppendItems(List values); - - /// Try to remove a number of items from the head of the DHT data structure. - /// Throws StateError if count < 0 - Future truncate(int count); - - /// Remove all items in the DHT data structure. - Future clear(); -} - -abstract class DHTAppendTruncateRandomRead - implements DHTAppendTruncate, DHTRandomRead {} - -extension DHTAppendTruncateExt on DHTAppendTruncate { - /// Convenience function: - /// Like tryAppendItem but also encodes the input value as JSON and parses the - /// returned element as JSON - Future tryAppendItemJson( - T newValue, - ) => - tryAppendItem(jsonEncodeBytes(newValue)); - - /// Convenience function: - /// Like tryAppendItem but also encodes the input value as a protobuf object - /// and parses the returned element as a protobuf object - Future tryAppendItemProtobuf( - T newValue, - ) => - tryAppendItem(newValue.writeToBuffer()); -} diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_clear.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_clear.dart new file mode 100644 index 0000000..f7ac9dd --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_clear.dart @@ -0,0 +1,7 @@ +//////////////////////////////////////////////////////////////////////////// +// Clear interface +// ignore: one_member_abstracts +abstract class DHTClear { + /// Remove all items in the DHT container. + Future clear(); +} diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_insert_remove.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_insert_remove.dart new file mode 100644 index 0000000..1f98a22 --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_insert_remove.dart @@ -0,0 +1,60 @@ +import 'dart:typed_data'; + +import 'package:protobuf/protobuf.dart'; + +import '../../../veilid_support.dart'; + +//////////////////////////////////////////////////////////////////////////// +// Insert/Remove interface +abstract class DHTInsertRemove { + /// Try to insert an item as position 'pos' of the DHT container. + /// 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. + /// Throws an IndexError if the position removed exceeds the length of + /// the container. + /// Throws a StateError if the container exceeds its maximum size. + Future tryInsertItem(int pos, Uint8List value); + + /// Try to insert items at position 'pos' of the DHT container. + /// Return true if the elements were successfully inserted, and false if the + /// state changed before the elements could be inserted or a newer value was + /// found on the network. + /// Throws an IndexError if the position removed exceeds the length of + /// the container. + /// Throws a StateError if the container exceeds its maximum size. + Future tryInsertItems(int pos, List values); + + /// Swap items at position 'aPos' and 'bPos' in the DHTArray. + /// Throws an IndexError if either of the positions swapped exceeds the length + /// of the container + Future swapItem(int aPos, int bPos); + + /// Remove an item at position 'pos' in the DHT container. + /// If the remove was successful this returns: + /// * outValue will return the prior contents of the element + /// Throws an IndexError if the position removed exceeds the length of + /// the container. + Future removeItem(int pos, {Output? output}); +} + +extension DHTInsertRemoveExt on DHTInsertRemove { + /// Convenience function: + /// Like removeItem but also parses the returned element as JSON + Future removeItemJson(T Function(dynamic) fromJson, int pos, + {Output? output}) async { + final outValueBytes = output == null ? null : Output(); + await removeItem(pos, output: outValueBytes); + output.mapSave(outValueBytes, (b) => jsonDecodeBytes(fromJson, b)); + } + + /// Convenience function: + /// Like removeItem but also parses the returned element as JSON + Future removeItemProtobuf( + T Function(List) fromBuffer, int pos, + {Output? output}) async { + final outValueBytes = output == null ? null : Output(); + await removeItem(pos, output: outValueBytes); + output.mapSave(outValueBytes, fromBuffer); + } +} diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart index d52676e..39d49e6 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart @@ -7,22 +7,21 @@ import '../../../veilid_support.dart'; //////////////////////////////////////////////////////////////////////////// // Reader interface abstract class DHTRandomRead { - /// Returns the number of elements in the DHTArray - /// This number will be >= 0 and <= DHTShortArray.maxElements (256) + /// Returns the number of elements in the DHT container int get length; - /// Return the item at position 'pos' in the DHTArray. If 'forceRefresh' + /// Return the item at position 'pos' in the DHT container. If 'forceRefresh' /// is specified, the network will always be checked for newer values /// rather than returning the existing locally stored copy of the elements. - /// * 'pos' must be >= 0 and < 'length' + /// Throws an IndexError if the 'pos' is not within the length + /// of the container. Future getItem(int pos, {bool forceRefresh = false}); /// Return a list of a range of items in the DHTArray. If 'forceRefresh' /// is specified, the network will always be checked for newer values /// rather than returning the existing locally stored copy of the elements. - /// * 'start' must be >= 0 - /// * 'len' must be >= 0 and <= DHTShortArray.maxElements (256) and defaults - /// to the maximum length + /// Throws an IndexError if either 'start' or '(start+length)' is not within + /// the length of the container. Future?> getItemRange(int start, {int? length, bool forceRefresh = false}); diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart index 17a450e..0d8f3ac 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart @@ -6,8 +6,9 @@ import '../../../veilid_support.dart'; //////////////////////////////////////////////////////////////////////////// // Writer interface +// ignore: one_member_abstracts abstract class DHTRandomWrite { - /// Try to set an item at position 'pos' of the DHTArray. + /// Try to set an item at position 'pos' of the DHT container. /// If the set was successful this returns: /// * A boolean true /// * outValue will return the prior contents of the element, @@ -18,55 +19,10 @@ abstract class DHTRandomWrite { /// * outValue will return the newer value of the element, /// or null if the head record changed. /// - /// This may throw an exception if the position exceeds the built-in limit of - /// 'maxElements = 256' entries. + /// Throws an IndexError if the position is not within the length + /// of the container. Future tryWriteItem(int pos, Uint8List newValue, {Output? output}); - - /// Try to add an item to the end of the DHTArray. 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 add a list of items to the end of the DHTArray. Return true if the - /// elements were successfully added, and false if the state changed before - /// the elements 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 tryAddItems(List values); - - /// Try to insert an item as position 'pos' of the DHTArray. - /// 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 insert items at position 'pos' of the DHTArray. - /// Return true if the elements were successfully inserted, and false if the - /// state changed before the elements 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 tryInsertItems(int pos, List values); - - /// Swap items at position 'aPos' and 'bPos' in the DHTArray. - /// Throws IndexError if either of the positions swapped exceed - /// the length of the list - Future swapItem(int aPos, int bPos); - - /// Remove an item at position 'pos' in the DHTArray. - /// If the remove was successful this returns: - /// * outValue will return the prior contents of the element - /// Throws IndexError if the position removed exceeds the length of - /// the list. - Future removeItem(int pos, {Output? output}); - - /// Remove all items in the DHTShortArray. - Future clear(); } extension DHTRandomWriteExt on DHTRandomWrite { @@ -95,25 +51,4 @@ extension DHTRandomWriteExt on DHTRandomWrite { output.mapSave(outValueBytes, fromBuffer); return out; } - - /// Convenience function: - /// Like removeItem but also parses the returned element as JSON - Future removeItemJson(T Function(dynamic) fromJson, int pos, - {Output? output}) async { - final outValueBytes = output == null ? null : Output(); - await removeItem(pos, output: outValueBytes); - output.mapSave(outValueBytes, (b) => jsonDecodeBytes(fromJson, b)); - } - - /// Convenience function: - /// Like removeItem but also parses the returned element as JSON - Future removeItemProtobuf( - T Function(List) fromBuffer, int pos, - {Output? output}) async { - final outValueBytes = output == null ? null : Output(); - await removeItem(pos, output: outValueBytes); - output.mapSave(outValueBytes, fromBuffer); - } } - -abstract class DHTRandomReadWrite implements DHTRandomRead, DHTRandomWrite {} diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_truncate.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_truncate.dart new file mode 100644 index 0000000..cbda00f --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_truncate.dart @@ -0,0 +1,8 @@ +//////////////////////////////////////////////////////////////////////////// +// Truncate interface +// ignore: one_member_abstracts +abstract class DHTTruncate { + /// Remove items from the DHT container to shrink its size to 'newLength' + /// Throws StateError if newLength < 0 + Future truncate(int newLength); +} diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart b/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart index 16f9970..dd95cac 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart @@ -1,4 +1,8 @@ +export 'dht_append.dart'; +export 'dht_clear.dart'; export 'dht_closeable.dart'; +export 'dht_insert_remove.dart'; export 'dht_random_read.dart'; export 'dht_random_write.dart'; +export 'dht_truncate.dart'; export 'exceptions.dart'; diff --git a/packages/veilid_support/lib/src/identity.dart b/packages/veilid_support/lib/src/identity.dart index 5721461..400d68b 100644 --- a/packages/veilid_support/lib/src/identity.dart +++ b/packages/veilid_support/lib/src/identity.dart @@ -130,7 +130,7 @@ extension IdentityMasterExtension on IdentityMaster { // Read the identity key to get the account keys final pool = DHTRecordPool.instance; - final identityRecordCrypto = await DHTRecordCryptoPrivate.fromSecret( + final identityRecordCrypto = await VeilidCryptoPrivate.fromSecret( identityRecordKey.kind, identitySecret); late final List accountRecordInfo; @@ -234,7 +234,7 @@ class IdentityMasterWithSecrets { return (await pool.createRecord( debugName: 'IdentityMasterWithSecrets::create::IdentityMasterRecord', - crypto: const DHTRecordCryptoPublic())) + crypto: const VeilidCryptoPublic())) .deleteScope((masterRec) async { veilidLoggy.debug('Creating identity record'); // Identity record is private diff --git a/packages/veilid_support/lib/src/table_db_array.dart b/packages/veilid_support/lib/src/table_db_array.dart new file mode 100644 index 0000000..504fb16 --- /dev/null +++ b/packages/veilid_support/lib/src/table_db_array.dart @@ -0,0 +1,517 @@ +import 'dart:async'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:async_tools/async_tools.dart'; +import 'package:charcode/charcode.dart'; + +import '../veilid_support.dart'; + +class TableDBArray { + TableDBArray({ + required String table, + required VeilidCrypto crypto, + }) : _table = table, + _crypto = crypto { + _initWait.add(_init); + } + + Future _init() async { + // Load the array details + await _mutex.protect(() async { + _tableDB = await Veilid.instance.openTableDB(_table, 1); + }); + } + + Future close({bool delete = false}) async { + // Ensure the init finished + await _initWait(); + + await _mutex.acquire(); + + await _changeStream.close(); + _tableDB.close(); + + if (delete) { + await Veilid.instance.deleteTableDB(_table); + } + } + + Future> listen(void Function() onChanged) async => + _changeStream.stream.listen((_) => onChanged()); + + //////////////////////////////////////////////////////////// + // Public interface + + int get length => _length; + + Future add(Uint8List value) async { + await _initWait(); + return _writeTransaction((t) async => _addInner(t, value)); + } + + Future addAll(List values) async { + await _initWait(); + return _writeTransaction((t) async => _addAllInner(t, values)); + } + + Future insert(int pos, Uint8List value) async { + await _initWait(); + return _writeTransaction((t) async => _insertInner(t, pos, value)); + } + + Future insertAll(int pos, List values) async { + await _initWait(); + return _writeTransaction((t) async => _insertAllInner(t, pos, values)); + } + + Future get(int pos) async { + await _initWait(); + return _mutex.protect(() async => _getInner(pos)); + } + + Future> getAll(int start, int length) async { + await _initWait(); + return _mutex.protect(() async => _getAllInner(start, length)); + } + + Future remove(int pos, {Output? out}) async { + await _initWait(); + return _writeTransaction((t) async => _removeInner(t, pos, out: out)); + } + + Future removeRange(int start, int length, + {Output>? out}) async { + await _initWait(); + return _writeTransaction( + (t) async => _removeRangeInner(t, start, length, out: out)); + } + + Future clear() async { + await _initWait(); + return _writeTransaction((t) async { + final keys = await _tableDB.getKeys(0); + for (final key in keys) { + await t.delete(0, key); + } + _length = 0; + _nextFree = 0; + _dirtyChunks.clear(); + _chunkCache.clear(); + }); + } + + //////////////////////////////////////////////////////////// + // Inner interface + + Future _addInner(VeilidTableDBTransaction t, Uint8List value) async { + // Allocate an entry to store the value + final entry = await _allocateEntry(); + await _storeEntry(t, entry, value); + + // Put the entry in the index + final pos = _length; + _length++; + await _setIndexEntry(pos, entry); + } + + Future _addAllInner( + VeilidTableDBTransaction t, List values) async { + var pos = _length; + _length += values.length; + for (final value in values) { + // Allocate an entry to store the value + final entry = await _allocateEntry(); + await _storeEntry(t, entry, value); + + // Put the entry in the index + await _setIndexEntry(pos, entry); + pos++; + } + } + + Future _insertInner( + VeilidTableDBTransaction t, int pos, Uint8List value) async { + if (pos == _length) { + return _addInner(t, value); + } + if (pos < 0 || pos >= _length) { + throw IndexError.withLength(pos, _length); + } + // Allocate an entry to store the value + final entry = await _allocateEntry(); + await _storeEntry(t, entry, value); + + // Put the entry in the index + await _insertIndexEntry(pos); + await _setIndexEntry(pos, entry); + } + + Future _insertAllInner( + VeilidTableDBTransaction t, int pos, List values) async { + if (pos == _length) { + return _addAllInner(t, values); + } + if (pos < 0 || pos >= _length) { + throw IndexError.withLength(pos, _length); + } + await _insertIndexEntries(pos, values.length); + for (final value in values) { + // Allocate an entry to store the value + final entry = await _allocateEntry(); + await _storeEntry(t, entry, value); + + // Put the entry in the index + await _setIndexEntry(pos, entry); + pos++; + } + } + + Future _getInner(int pos) async { + if (pos < 0 || pos >= _length) { + throw IndexError.withLength(pos, _length); + } + final entry = await _getIndexEntry(pos); + return (await _loadEntry(entry))!; + } + + Future> _getAllInner(int start, int length) async { + if (length < 0) { + throw StateError('length should not be negative'); + } + if (start < 0 || start >= _length) { + throw IndexError.withLength(start, _length); + } + if ((start + length) > _length) { + throw IndexError.withLength(start + length, _length); + } + + final out = []; + for (var pos = start; pos < (start + length); pos++) { + final entry = await _getIndexEntry(pos); + final value = (await _loadEntry(entry))!; + out.add(value); + } + return out; + } + + Future _removeInner(VeilidTableDBTransaction t, int pos, + {Output? out}) async { + if (pos < 0 || pos >= _length) { + throw IndexError.withLength(pos, _length); + } + + final entry = await _getIndexEntry(pos); + if (out != null) { + final value = (await _loadEntry(entry))!; + out.save(value); + } + + await _freeEntry(t, entry); + await _removeIndexEntry(pos); + } + + Future _removeRangeInner( + VeilidTableDBTransaction t, int start, int length, + {Output>? out}) async { + if (length < 0) { + throw StateError('length should not be negative'); + } + if (start < 0 || start >= _length) { + throw IndexError.withLength(start, _length); + } + if ((start + length) > _length) { + throw IndexError.withLength(start + length, _length); + } + + final outList = []; + for (var pos = start; pos < (start + length); pos++) { + final entry = await _getIndexEntry(pos); + if (out != null) { + final value = (await _loadEntry(entry))!; + outList.add(value); + } + await _freeEntry(t, entry); + } + if (out != null) { + out.save(outList); + } + + await _removeIndexEntries(start, length); + } + + //////////////////////////////////////////////////////////// + // Private implementation + + static final Uint8List _headKey = Uint8List.fromList([$_, $H, $E, $A, $D]); + static Uint8List _entryKey(int k) => + (ByteData(4)..setUint32(0, k)).buffer.asUint8List(); + static Uint8List _chunkKey(int n) => + (ByteData(2)..setUint16(0, n)).buffer.asUint8List(); + + Future _writeTransaction( + Future Function(VeilidTableDBTransaction) closure) async => + _mutex.protect(() async { + final _oldLength = _length; + final _oldNextFree = _nextFree; + try { + final out = await transactionScope(_tableDB, (t) async { + final out = closure(t); + await _saveHead(t); + await _flushDirtyChunks(t); + return out; + }); + + return out; + } on Exception { + // restore head + _length = _oldLength; + _nextFree = _oldNextFree; + // invalidate caches because they could have been written to + _chunkCache.clear(); + _dirtyChunks.clear(); + // propagate exception + rethrow; + } + }); + + Future _storeEntry( + VeilidTableDBTransaction t, int entry, Uint8List value) async => + t.store(0, _entryKey(entry), await _crypto.encrypt(value)); + + Future _loadEntry(int entry) async { + final encryptedValue = await _tableDB.load(0, _entryKey(entry)); + return (encryptedValue == null) ? null : _crypto.decrypt(encryptedValue); + } + + Future _getIndexEntry(int pos) async { + if (pos < 0 || pos >= _length) { + throw IndexError.withLength(pos, _length); + } + final chunkNumber = pos ~/ _indexStride; + final chunkOffset = pos % _indexStride; + + final chunk = await _loadIndexChunk(chunkNumber); + + return chunk.buffer.asByteData().getUint32(chunkOffset * 4); + } + + Future _setIndexEntry(int pos, int entry) async { + if (pos < 0 || pos >= _length) { + throw IndexError.withLength(pos, _length); + } + + final chunkNumber = pos ~/ _indexStride; + final chunkOffset = pos % _indexStride; + + final chunk = await _loadIndexChunk(chunkNumber); + chunk.buffer.asByteData().setUint32(chunkOffset * 4, entry); + + _dirtyChunks[chunkNumber] = chunk; + } + + Future _insertIndexEntry(int pos) async => _insertIndexEntries(pos, 1); + + Future _insertIndexEntries(int start, int length) async { + if (length == 0) { + return; + } + if (length < 0) { + throw StateError('length should not be negative'); + } + if (start < 0 || start >= _length) { + throw IndexError.withLength(start, _length); + } + final end = start + length - 1; + + // Slide everything over in reverse + final toCopyTotal = _length - start; + var dest = end + toCopyTotal; + var src = _length - 1; + + (int, Uint8List)? lastSrcChunk; + (int, Uint8List)? lastDestChunk; + while (src >= start) { + final srcChunkNumber = src ~/ _indexStride; + final srcIndex = src % _indexStride; + final srcLength = srcIndex + 1; + + final srcChunk = + (lastSrcChunk != null && (lastSrcChunk.$1 == srcChunkNumber)) + ? lastSrcChunk.$2 + : await _loadIndexChunk(srcChunkNumber); + lastSrcChunk = (srcChunkNumber, srcChunk); + + final destChunkNumber = dest ~/ _indexStride; + final destIndex = dest % _indexStride; + final destLength = destIndex + 1; + + final destChunk = + (lastDestChunk != null && (lastDestChunk.$1 == destChunkNumber)) + ? lastDestChunk.$2 + : await _loadIndexChunk(destChunkNumber); + lastDestChunk = (destChunkNumber, destChunk); + + final toCopy = min(srcLength, destLength); + destChunk.setRange((destIndex - (toCopy - 1)) * 4, (destIndex + 1) * 4, + srcChunk, (srcIndex - (toCopy - 1)) * 4); + + dest -= toCopy; + src -= toCopy; + } + + // Then add to length + _length += length; + } + + Future _removeIndexEntry(int pos) async => _removeIndexEntries(pos, 1); + + Future _removeIndexEntries(int start, int length) async { + if (length == 0) { + return; + } + if (length < 0) { + throw StateError('length should not be negative'); + } + if (start < 0 || start >= _length) { + throw IndexError.withLength(start, _length); + } + final end = start + length - 1; + if (end < 0 || end >= _length) { + throw IndexError.withLength(end, _length); + } + + // Slide everything over + var dest = start; + var src = end + 1; + (int, Uint8List)? lastSrcChunk; + (int, Uint8List)? lastDestChunk; + while (src < _length) { + final srcChunkNumber = src ~/ _indexStride; + final srcIndex = src % _indexStride; + final srcLength = _indexStride - srcIndex; + + final srcChunk = + (lastSrcChunk != null && (lastSrcChunk.$1 == srcChunkNumber)) + ? lastSrcChunk.$2 + : await _loadIndexChunk(srcChunkNumber); + lastSrcChunk = (srcChunkNumber, srcChunk); + + final destChunkNumber = dest ~/ _indexStride; + final destIndex = dest % _indexStride; + final destLength = _indexStride - destIndex; + + final destChunk = + (lastDestChunk != null && (lastDestChunk.$1 == destChunkNumber)) + ? lastDestChunk.$2 + : await _loadIndexChunk(destChunkNumber); + lastDestChunk = (destChunkNumber, destChunk); + + final toCopy = min(srcLength, destLength); + destChunk.setRange( + destIndex * 4, (destIndex + toCopy) * 4, srcChunk, srcIndex * 4); + + dest += toCopy; + src += toCopy; + } + + // Then truncate + _length -= length; + } + + Future _loadIndexChunk(int chunkNumber) async { + // Get it from the dirty chunks if we have it + final dirtyChunk = _dirtyChunks[chunkNumber]; + if (dirtyChunk != null) { + return dirtyChunk; + } + + // Get from cache if we have it + for (var i = 0; i < _chunkCache.length; i++) { + if (_chunkCache[i].$1 == chunkNumber) { + // Touch the element + final x = _chunkCache.removeAt(i); + _chunkCache.add(x); + // Return the chunk for this position + return x.$2; + } + } + + // Get chunk from disk + var chunk = await _tableDB.load(0, _chunkKey(chunkNumber)); + chunk ??= Uint8List(_indexStride * 4); + + // Cache the chunk + _chunkCache.add((chunkNumber, chunk)); + if (_chunkCache.length > _chunkCacheLength) { + // Trim the LRU cache + final (_, _) = _chunkCache.removeAt(0); + } + + return chunk; + } + + Future _flushDirtyChunks(VeilidTableDBTransaction t) async { + for (final ec in _dirtyChunks.entries) { + await _tableDB.store(0, _chunkKey(ec.key), ec.value); + } + _dirtyChunks.clear(); + } + + Future _loadHead() async { + assert(_mutex.isLocked, 'should be locked'); + final headBytes = await _tableDB.load(0, _headKey); + if (headBytes == null) { + _length = 0; + _nextFree = 0; + } else { + final b = headBytes.buffer.asByteData(); + _length = b.getUint32(0); + _nextFree = b.getUint32(4); + } + } + + Future _saveHead(VeilidTableDBTransaction t) async { + assert(_mutex.isLocked, 'should be locked'); + final b = ByteData(8) + ..setUint32(0, _length) + ..setUint32(4, _nextFree); + await t.store(0, _headKey, b.buffer.asUint8List()); + } + + Future _allocateEntry() async { + assert(_mutex.isLocked, 'should be locked'); + if (_nextFree == 0) { + return _length; + } + // pop endogenous free list + final free = _nextFree; + final nextFreeBytes = await _tableDB.load(0, _entryKey(free)); + _nextFree = nextFreeBytes!.buffer.asByteData().getUint8(0); + return free; + } + + Future _freeEntry(VeilidTableDBTransaction t, int entry) async { + assert(_mutex.isLocked, 'should be locked'); + // push endogenous free list + final b = ByteData(4)..setUint32(0, _nextFree); + await t.store(0, _entryKey(entry), b.buffer.asUint8List()); + _nextFree = entry; + } + + final String _table; + late final VeilidTableDB _tableDB; + final VeilidCrypto _crypto; + final WaitSet _initWait = WaitSet(); + final Mutex _mutex = Mutex(); + + // Head state + int _length = 0; + int _nextFree = 0; + static const int _indexStride = 16384; + final List<(int, Uint8List)> _chunkCache = []; + final Map _dirtyChunks = {}; + static const int _chunkCacheLength = 3; + + final StreamController _changeStream = StreamController.broadcast(); +} diff --git a/packages/veilid_support/lib/src/veilid_crypto.dart b/packages/veilid_support/lib/src/veilid_crypto.dart new file mode 100644 index 0000000..6965089 --- /dev/null +++ b/packages/veilid_support/lib/src/veilid_crypto.dart @@ -0,0 +1,52 @@ +import 'dart:async'; +import 'dart:typed_data'; +import '../../../veilid_support.dart'; + +abstract class VeilidCrypto { + Future encrypt(Uint8List data); + Future decrypt(Uint8List data); +} + +//////////////////////////////////// +/// Encrypted for a specific symmetric key +class VeilidCryptoPrivate implements VeilidCrypto { + VeilidCryptoPrivate._(VeilidCryptoSystem cryptoSystem, SharedSecret secretKey) + : _cryptoSystem = cryptoSystem, + _secretKey = secretKey; + final VeilidCryptoSystem _cryptoSystem; + final SharedSecret _secretKey; + + static Future fromTypedKeyPair( + TypedKeyPair typedKeyPair) async { + final cryptoSystem = + await Veilid.instance.getCryptoSystem(typedKeyPair.kind); + final secretKey = typedKeyPair.secret; + return VeilidCryptoPrivate._(cryptoSystem, secretKey); + } + + static Future fromSecret( + CryptoKind kind, SharedSecret secretKey) async { + final cryptoSystem = await Veilid.instance.getCryptoSystem(kind); + return VeilidCryptoPrivate._(cryptoSystem, secretKey); + } + + @override + Future encrypt(Uint8List data) => + _cryptoSystem.encryptNoAuthWithNonce(data, _secretKey); + + @override + Future decrypt(Uint8List data) => + _cryptoSystem.decryptNoAuthWithNonce(data, _secretKey); +} + +//////////////////////////////////// +/// No encryption +class VeilidCryptoPublic implements VeilidCrypto { + const VeilidCryptoPublic(); + + @override + Future encrypt(Uint8List data) async => data; + + @override + Future decrypt(Uint8List data) async => data; +} diff --git a/packages/veilid_support/lib/veilid_support.dart b/packages/veilid_support/lib/veilid_support.dart index e741990..1f17da2 100644 --- a/packages/veilid_support/lib/veilid_support.dart +++ b/packages/veilid_support/lib/veilid_support.dart @@ -14,4 +14,6 @@ export 'src/output.dart'; export 'src/persistent_queue.dart'; export 'src/protobuf_tools.dart'; export 'src/table_db.dart'; +export 'src/table_db_array.dart'; +export 'src/veilid_crypto.dart'; export 'src/veilid_log.dart' hide veilidLoggy; diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index d58ee4d..db70f07 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -146,7 +146,7 @@ packages: source: hosted version: "1.3.0" charcode: - dependency: transitive + dependency: "direct main" description: name: charcode sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index 06403ca..e598f8c 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -10,6 +10,7 @@ dependencies: async_tools: ^0.1.1 bloc: ^8.1.4 bloc_advanced_tools: ^0.1.1 + charcode: ^1.3.1 collection: ^1.18.0 equatable: ^2.0.5 fast_immutable_collections: ^10.2.3 From ab65956433912207b347ea53771194422a11ca12 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 26 May 2024 20:41:29 -0400 Subject: [PATCH 112/270] table db array testing --- .../example/integration_test/app_test.dart | 187 +++++++--- .../integration_test/test_table_db_array.dart | 331 +++++++++++------- .../lib/src/table_db_array.dart | 103 ++++-- 3 files changed, 432 insertions(+), 189 deletions(-) diff --git a/packages/veilid_support/example/integration_test/app_test.dart b/packages/veilid_support/example/integration_test/app_test.dart index 1577d7a..b7a807f 100644 --- a/packages/veilid_support/example/integration_test/app_test.dart +++ b/packages/veilid_support/example/integration_test/app_test.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:integration_test/integration_test.dart'; import 'package:test/test.dart'; +import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_test/veilid_test.dart'; import 'fixtures/fixtures.dart'; @@ -37,59 +38,159 @@ void main() { group('TableDB Tests', () { group('TableDBArray Tests', () { - test('create TableDBArray', makeTestTableDBArrayCreateDelete()); - test( - timeout: const Timeout(Duration(seconds: 480)), - 'add/truncate TableDBArray', - makeTestDHTLogAddTruncate(), - ); + // test('create/delete TableDBArray', testTableDBArrayCreateDelete); + + // group('TableDBArray Add/Get Tests', () { + // for (final params in [ + // // + // (99, 3, 15), + // (100, 4, 16), + // (101, 5, 17), + // // + // (511, 3, 127), + // (512, 4, 128), + // (513, 5, 129), + // // + // (4095, 3, 1023), + // (4096, 4, 1024), + // (4097, 5, 1025), + // // + // (65535, 3, 16383), + // (65536, 4, 16384), + // (65537, 5, 16385), + // ]) { + // final count = params.$1; + // final singles = params.$2; + // final batchSize = params.$3; + + // test( + // // timeout: const Timeout(Duration(seconds: 480)), + // 'add/remove TableDBArray count = $count batchSize=$batchSize', + // makeTestTableDBArrayAddGetClear( + // count: count, + // singles: singles, + // batchSize: batchSize, + // crypto: const VeilidCryptoPublic()), + // ); + // } + // }); + + // group('TableDBArray Insert Tests', () { + // for (final params in [ + // // + // (99, 3, 15), + // (100, 4, 16), + // (101, 5, 17), + // // + // (511, 3, 127), + // (512, 4, 128), + // (513, 5, 129), + // // + // (4095, 3, 1023), + // (4096, 4, 1024), + // (4097, 5, 1025), + // // + // (65535, 3, 16383), + // (65536, 4, 16384), + // (65537, 5, 16385), + // ]) { + // final count = params.$1; + // final singles = params.$2; + // final batchSize = params.$3; + + // test( + // // timeout: const Timeout(Duration(seconds: 480)), + // 'insert TableDBArray count=$count singles=$singles batchSize=$batchSize', + // makeTestTableDBArrayInsert( + // count: count, + // singles: singles, + // batchSize: batchSize, + // crypto: const VeilidCryptoPublic()), + // ); + // } + // }); + + group('TableDBArray Remove Tests', () { + for (final params in [ + // + (99, 3, 15), + (100, 4, 16), + (101, 5, 17), + // + (511, 3, 127), + (512, 4, 128), + (513, 5, 129), + // + (4095, 3, 1023), + (4096, 4, 1024), + (4097, 5, 1025), + // + (65535, 3, 16383), + (65536, 4, 16384), + (65537, 5, 16385), + ]) { + final count = params.$1; + final singles = params.$2; + final batchSize = params.$3; + + test( + // timeout: const Timeout(Duration(seconds: 480)), + 'remove TableDBArray count=$count singles=$singles batchSize=$batchSize', + makeTestTableDBArrayRemove( + count: count, + singles: singles, + batchSize: batchSize, + crypto: const VeilidCryptoPublic()), + ); + } + }); }); }); - group('DHT Support Tests', () { - setUpAll(updateProcessorFixture.setUp); - setUpAll(tickerFixture.setUp); - tearDownAll(tickerFixture.tearDown); - tearDownAll(updateProcessorFixture.tearDown); + // group('DHT Support Tests', () { + // setUpAll(updateProcessorFixture.setUp); + // setUpAll(tickerFixture.setUp); + // tearDownAll(tickerFixture.tearDown); + // tearDownAll(updateProcessorFixture.tearDown); - test('create pool', testDHTRecordPoolCreate); + // test('create pool', testDHTRecordPoolCreate); - group('DHTRecordPool Tests', () { - setUpAll(dhtRecordPoolFixture.setUp); - tearDownAll(dhtRecordPoolFixture.tearDown); + // group('DHTRecordPool Tests', () { + // setUpAll(dhtRecordPoolFixture.setUp); + // tearDownAll(dhtRecordPoolFixture.tearDown); - test('create/delete record', testDHTRecordCreateDelete); - test('record scopes', testDHTRecordScopes); - test('create/delete deep record', testDHTRecordDeepCreateDelete); - }); + // test('create/delete record', testDHTRecordCreateDelete); + // test('record scopes', testDHTRecordScopes); + // test('create/delete deep record', testDHTRecordDeepCreateDelete); + // }); - group('DHTShortArray Tests', () { - setUpAll(dhtRecordPoolFixture.setUp); - tearDownAll(dhtRecordPoolFixture.tearDown); + // group('DHTShortArray Tests', () { + // setUpAll(dhtRecordPoolFixture.setUp); + // tearDownAll(dhtRecordPoolFixture.tearDown); - for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { - test('create shortarray stride=$stride', - makeTestDHTShortArrayCreateDelete(stride: stride)); - test('add shortarray stride=$stride', - makeTestDHTShortArrayAdd(stride: stride)); - } - }); + // for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { + // test('create shortarray stride=$stride', + // makeTestDHTShortArrayCreateDelete(stride: stride)); + // test('add shortarray stride=$stride', + // makeTestDHTShortArrayAdd(stride: stride)); + // } + // }); - group('DHTLog Tests', () { - setUpAll(dhtRecordPoolFixture.setUp); - tearDownAll(dhtRecordPoolFixture.tearDown); + // group('DHTLog Tests', () { + // setUpAll(dhtRecordPoolFixture.setUp); + // tearDownAll(dhtRecordPoolFixture.tearDown); - for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { - test('create log stride=$stride', - makeTestDHTLogCreateDelete(stride: stride)); - test( - timeout: const Timeout(Duration(seconds: 480)), - 'add/truncate log stride=$stride', - makeTestDHTLogAddTruncate(stride: stride), - ); - } - }); - }); + // for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { + // test('create log stride=$stride', + // makeTestDHTLogCreateDelete(stride: stride)); + // test( + // timeout: const Timeout(Duration(seconds: 480)), + // 'add/truncate log stride=$stride', + // makeTestDHTLogAddTruncate(stride: stride), + // ); + // } + // }); + // }); }); }); } diff --git a/packages/veilid_support/example/integration_test/test_table_db_array.dart b/packages/veilid_support/example/integration_test/test_table_db_array.dart index e9087f6..81607a6 100644 --- a/packages/veilid_support/example/integration_test/test_table_db_array.dart +++ b/packages/veilid_support/example/integration_test/test_table_db_array.dart @@ -1,134 +1,213 @@ import 'dart:convert'; +import 'dart:math'; +import 'dart:typed_data'; import 'package:test/test.dart'; import 'package:veilid_support/veilid_support.dart'; -Future Function() makeTestTableDBArrayCreateDelete() => () async { - // Close before delete - { - final arr = await TableDBArray( - table: 'test', crypto: const VeilidCryptoPublic()); - - expect(await arr.operate((r) async => r.length), isZero); - expect(arr.isOpen, isTrue); - await arr.close(); - expect(arr.isOpen, isFalse); - await arr.delete(); - // Operate should fail - await expectLater(() async => arr.operate((r) async => r.length), - throwsA(isA())); - } - - // Close after delete - { - final arr = await DHTShortArray.create( - debugName: 'sa_create_delete 2 stride $stride', stride: stride); - await arr.delete(); - // Operate should still succeed because things aren't closed - expect(await arr.operate((r) async => r.length), isZero); - await arr.close(); - // Operate should fail - await expectLater(() async => arr.operate((r) async => r.length), - throwsA(isA())); - } - - // Close after delete multiple - // Okay to request delete multiple times before close - { - final arr = await DHTShortArray.create( - debugName: 'sa_create_delete 3 stride $stride', stride: stride); - await arr.delete(); - await arr.delete(); - // Operate should still succeed because things aren't closed - expect(await arr.operate((r) async => r.length), isZero); - await arr.close(); - await expectLater(() async => arr.close(), throwsA(isA())); - // Operate should fail - await expectLater(() async => arr.operate((r) async => r.length), - throwsA(isA())); - } - }; - -Future Function() makeTestTableDBArrayAdd({required int stride}) => - () async { - final arr = await DHTShortArray.create( - debugName: 'sa_add 1 stride $stride', stride: stride); - - final dataset = Iterable.generate(256) - .map((n) => utf8.encode('elem $n')) - .toList(); - - print('adding singles\n'); - { - final res = await arr.operateWrite((w) async { - for (var n = 4; n < 8; n++) { - print('$n '); - final success = await w.tryAddItem(dataset[n]); - expect(success, isTrue); - } - }); - expect(res, isNull); - } - - print('adding batch\n'); - { - final res = await arr.operateWrite((w) async { - print('${dataset.length ~/ 2}-${dataset.length}'); - final success = await w.tryAddItems( - dataset.sublist(dataset.length ~/ 2, dataset.length)); - expect(success, isTrue); - }); - expect(res, isNull); - } - - print('inserting singles\n'); - { - final res = await arr.operateWrite((w) async { - for (var n = 0; n < 4; n++) { - print('$n '); - final success = await w.tryInsertItem(n, dataset[n]); - expect(success, isTrue); - } - }); - expect(res, isNull); - } - - print('inserting batch\n'); - { - final res = await arr.operateWrite((w) async { - print('8-${dataset.length ~/ 2}'); - final success = await w.tryInsertItems( - 8, dataset.sublist(8, dataset.length ~/ 2)); - expect(success, isTrue); - }); - expect(res, isNull); - } - - //print('get all\n'); - { - final dataset2 = await arr.operate((r) async => r.getItemRange(0)); - expect(dataset2, equals(dataset)); - } - { - final dataset3 = - await arr.operate((r) async => r.getItemRange(64, length: 128)); - expect(dataset3, equals(dataset.sublist(64, 64 + 128))); - } - - //print('clear\n'); - { - await arr.operateWriteEventual((w) async { - await w.clear(); - return true; - }); - } - - //print('get all\n'); - { - final dataset4 = await arr.operate((r) async => r.getItemRange(0)); - expect(dataset4, isEmpty); - } +Future testTableDBArrayCreateDelete() async { + // Close before delete + { + final arr = + TableDBArray(table: 'testArray', crypto: const VeilidCryptoPublic()); + expect(() => arr.length, throwsA(isA())); + expect(arr.isOpen, isTrue); + await arr.initWait(); + expect(arr.isOpen, isTrue); + expect(arr.length, isZero); + await arr.close(); + expect(arr.isOpen, isFalse); + await arr.delete(); + expect(arr.isOpen, isFalse); + } + // Async create with close after delete and then reopen + { + final arr = await TableDBArray.make( + table: 'testArray', crypto: const VeilidCryptoPublic()); + expect(arr.length, isZero); + expect(arr.isOpen, isTrue); + await expectLater(() async { await arr.delete(); - await arr.close(); + }, throwsA(isA())); + expect(arr.isOpen, isTrue); + await arr.close(); + expect(arr.isOpen, isFalse); + + final arr2 = await TableDBArray.make( + table: 'testArray', crypto: const VeilidCryptoPublic()); + expect(arr2.isOpen, isTrue); + expect(arr.isOpen, isFalse); + await arr2.close(); + expect(arr2.isOpen, isFalse); + await arr2.delete(); + } +} + +Uint8List makeData(int n) => utf8.encode('elem $n'); +List makeDataBatch(int n, int batchSize) => + List.generate(batchSize, (x) => makeData(n + x)); + +Future Function() makeTestTableDBArrayAddGetClear( + {required int count, + required int singles, + required int batchSize, + required VeilidCrypto crypto}) => + () async { + final arr = await TableDBArray.make(table: 'testArray', crypto: crypto); + + print('adding'); + { + for (var n = 0; n < count;) { + var toAdd = min(batchSize, count - n); + for (var s = 0; s < min(singles, toAdd); s++) { + await arr.add(makeData(n)); + toAdd--; + n++; + } + + await arr.addAll(makeDataBatch(n, toAdd)); + n += toAdd; + + print(' $n/$count'); + } + } + + print('get singles'); + { + for (var n = 0; n < batchSize; n++) { + expect(await arr.get(n), equals(makeData(n))); + } + } + + print('get batch'); + { + for (var n = batchSize; n < count; n += batchSize) { + final toGet = min(batchSize, count - n); + expect(await arr.getRange(n, toGet), equals(makeDataBatch(n, toGet))); + } + } + + print('clear'); + { + await arr.clear(); + expect(arr.length, isZero); + } + + await arr.close(delete: true); + }; + +Future Function() makeTestTableDBArrayInsert( + {required int count, + required int singles, + required int batchSize, + required VeilidCrypto crypto}) => + () async { + final arr = await TableDBArray.make(table: 'testArray', crypto: crypto); + + final match = []; + + print('inserting'); + { + for (var n = 0; n < count;) { + final start = n; + var toAdd = min(batchSize, count - n); + for (var s = 0; s < min(singles, toAdd); s++) { + final data = makeData(n); + await arr.insert(start, data); + match.insert(start, data); + toAdd--; + n++; + } + + final data = makeDataBatch(n, toAdd); + await arr.insertAll(start, data); + match.insertAll(start, data); + n += toAdd; + + print(' $n/$count'); + } + } + + print('get singles'); + { + for (var n = 0; n < batchSize; n++) { + expect(await arr.get(n), equals(match[n])); + } + } + + print('get batch'); + { + for (var n = batchSize; n < count; n += batchSize) { + final toGet = min(batchSize, count - n); + expect(await arr.getRange(n, toGet), + equals(match.sublist(n, n + toGet))); + } + } + + print('clear'); + { + await arr.clear(); + expect(arr.length, isZero); + } + + await arr.close(delete: true); + }; + + +Future Function() makeTestTableDBArrayRemove( + {required int count, + required int singles, + required int batchSize, + required VeilidCrypto crypto}) => + () async { + final arr = await TableDBArray.make(table: 'testArray', crypto: crypto); + + final match = []; +xxx removal test + print('inserting'); + { + for (var n = 0; n < count;) { + final start = n; + var toAdd = min(batchSize, count - n); + for (var s = 0; s < min(singles, toAdd); s++) { + final data = makeData(n); + await arr.insert(start, data); + match.insert(start, data); + toAdd--; + n++; + } + + final data = makeDataBatch(n, toAdd); + await arr.insertAll(start, data); + match.insertAll(start, data); + n += toAdd; + + print(' $n/$count'); + } + } + + print('get singles'); + { + for (var n = 0; n < batchSize; n++) { + expect(await arr.get(n), equals(match[n])); + } + } + + print('get batch'); + { + for (var n = batchSize; n < count; n += batchSize) { + final toGet = min(batchSize, count - n); + expect(await arr.getRange(n, toGet), + equals(match.sublist(n, n + toGet))); + } + } + + print('clear'); + { + await arr.clear(); + expect(arr.length, isZero); + } + + await arr.close(delete: true); }; diff --git a/packages/veilid_support/lib/src/table_db_array.dart b/packages/veilid_support/lib/src/table_db_array.dart index 504fb16..dbabd3a 100644 --- a/packages/veilid_support/lib/src/table_db_array.dart +++ b/packages/veilid_support/lib/src/table_db_array.dart @@ -16,10 +16,25 @@ class TableDBArray { _initWait.add(_init); } + static Future make({ + required String table, + required VeilidCrypto crypto, + }) async { + final out = TableDBArray(table: table, crypto: crypto); + await out._initWait(); + return out; + } + + Future initWait() async { + await _initWait(); + } + Future _init() async { // Load the array details await _mutex.protect(() async { _tableDB = await Veilid.instance.openTableDB(_table, 1); + await _loadHead(); + _initDone = true; }); } @@ -27,23 +42,45 @@ class TableDBArray { // Ensure the init finished await _initWait(); - await _mutex.acquire(); - - await _changeStream.close(); - _tableDB.close(); - + // Allow multiple attempts to close + if (_open) { + await _mutex.protect(() async { + await _changeStream.close(); + _tableDB.close(); + _open = false; + }); + } if (delete) { await Veilid.instance.deleteTableDB(_table); } } + Future delete() async { + await _initWait(); + if (_open) { + throw StateError('should be closed first'); + } + await Veilid.instance.deleteTableDB(_table); + } + Future> listen(void Function() onChanged) async => _changeStream.stream.listen((_) => onChanged()); //////////////////////////////////////////////////////////// // Public interface - int get length => _length; + int get length { + if (!_open) { + throw StateError('not open'); + } + if (!_initDone) { + throw StateError('not initialized'); + } + + return _length; + } + + bool get isOpen => _open; Future add(Uint8List value) async { await _initWait(); @@ -67,12 +104,22 @@ class TableDBArray { Future get(int pos) async { await _initWait(); - return _mutex.protect(() async => _getInner(pos)); + return _mutex.protect(() async { + if (!_open) { + throw StateError('not open'); + } + return _getInner(pos); + }); } - Future> getAll(int start, int length) async { + Future> getRange(int start, int length) async { await _initWait(); - return _mutex.protect(() async => _getAllInner(start, length)); + return _mutex.protect(() async { + if (!_open) { + throw StateError('not open'); + } + return _getRangeInner(start, length); + }); } Future remove(int pos, {Output? out}) async { @@ -96,6 +143,7 @@ class TableDBArray { } _length = 0; _nextFree = 0; + _maxEntry = 0; _dirtyChunks.clear(); _chunkCache.clear(); }); @@ -175,7 +223,7 @@ class TableDBArray { return (await _loadEntry(entry))!; } - Future> _getAllInner(int start, int length) async { + Future> _getRangeInner(int start, int length) async { if (length < 0) { throw StateError('length should not be negative'); } @@ -252,11 +300,16 @@ class TableDBArray { Future _writeTransaction( Future Function(VeilidTableDBTransaction) closure) async => _mutex.protect(() async { + if (!_open) { + throw StateError('not open'); + } + final _oldLength = _length; final _oldNextFree = _nextFree; + final _oldMaxEntry = _maxEntry; try { final out = await transactionScope(_tableDB, (t) async { - final out = closure(t); + final out = await closure(t); await _saveHead(t); await _flushDirtyChunks(t); return out; @@ -267,6 +320,7 @@ class TableDBArray { // restore head _length = _oldLength; _nextFree = _oldNextFree; + _maxEntry = _oldMaxEntry; // invalidate caches because they could have been written to _chunkCache.clear(); _dirtyChunks.clear(); @@ -322,34 +376,35 @@ class TableDBArray { if (start < 0 || start >= _length) { throw IndexError.withLength(start, _length); } - final end = start + length - 1; // Slide everything over in reverse - final toCopyTotal = _length - start; - var dest = end + toCopyTotal; var src = _length - 1; + var dest = src + length; (int, Uint8List)? lastSrcChunk; (int, Uint8List)? lastDestChunk; while (src >= start) { + final remaining = (src - start) + 1; final srcChunkNumber = src ~/ _indexStride; final srcIndex = src % _indexStride; - final srcLength = srcIndex + 1; + final srcLength = min(remaining, srcIndex + 1); final srcChunk = (lastSrcChunk != null && (lastSrcChunk.$1 == srcChunkNumber)) ? lastSrcChunk.$2 : await _loadIndexChunk(srcChunkNumber); + _dirtyChunks[srcChunkNumber] = srcChunk; lastSrcChunk = (srcChunkNumber, srcChunk); final destChunkNumber = dest ~/ _indexStride; final destIndex = dest % _indexStride; - final destLength = destIndex + 1; + final destLength = min(remaining, destIndex + 1); final destChunk = (lastDestChunk != null && (lastDestChunk.$1 == destChunkNumber)) ? lastDestChunk.$2 : await _loadIndexChunk(destChunkNumber); + _dirtyChunks[destChunkNumber] = destChunk; lastDestChunk = (destChunkNumber, destChunk); final toCopy = min(srcLength, destLength); @@ -395,6 +450,7 @@ class TableDBArray { (lastSrcChunk != null && (lastSrcChunk.$1 == srcChunkNumber)) ? lastSrcChunk.$2 : await _loadIndexChunk(srcChunkNumber); + _dirtyChunks[srcChunkNumber] = srcChunk; lastSrcChunk = (srcChunkNumber, srcChunk); final destChunkNumber = dest ~/ _indexStride; @@ -405,6 +461,7 @@ class TableDBArray { (lastDestChunk != null && (lastDestChunk.$1 == destChunkNumber)) ? lastDestChunk.$2 : await _loadIndexChunk(destChunkNumber); + _dirtyChunks[destChunkNumber] = destChunk; lastDestChunk = (destChunkNumber, destChunk); final toCopy = min(srcLength, destLength); @@ -453,7 +510,7 @@ class TableDBArray { Future _flushDirtyChunks(VeilidTableDBTransaction t) async { for (final ec in _dirtyChunks.entries) { - await _tableDB.store(0, _chunkKey(ec.key), ec.value); + await t.store(0, _chunkKey(ec.key), ec.value); } _dirtyChunks.clear(); } @@ -464,25 +521,28 @@ class TableDBArray { if (headBytes == null) { _length = 0; _nextFree = 0; + _maxEntry = 0; } else { final b = headBytes.buffer.asByteData(); _length = b.getUint32(0); _nextFree = b.getUint32(4); + _maxEntry = b.getUint32(8); } } Future _saveHead(VeilidTableDBTransaction t) async { assert(_mutex.isLocked, 'should be locked'); - final b = ByteData(8) + final b = ByteData(12) ..setUint32(0, _length) - ..setUint32(4, _nextFree); + ..setUint32(4, _nextFree) + ..setUint32(8, _maxEntry); await t.store(0, _headKey, b.buffer.asUint8List()); } Future _allocateEntry() async { assert(_mutex.isLocked, 'should be locked'); if (_nextFree == 0) { - return _length; + return _maxEntry++; } // pop endogenous free list final free = _nextFree; @@ -501,6 +561,8 @@ class TableDBArray { final String _table; late final VeilidTableDB _tableDB; + var _open = true; + var _initDone = false; final VeilidCrypto _crypto; final WaitSet _initWait = WaitSet(); final Mutex _mutex = Mutex(); @@ -508,6 +570,7 @@ class TableDBArray { // Head state int _length = 0; int _nextFree = 0; + int _maxEntry = 0; static const int _indexStride = 16384; final List<(int, Uint8List)> _chunkCache = []; final Map _dirtyChunks = {}; From 17f6dfce466585062c0e94343c2d5358d26b523e Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 27 May 2024 13:17:33 -0400 Subject: [PATCH 113/270] tabledbarray tests pass --- .../example/integration_test/app_test.dart | 218 +++++++++--------- .../integration_test/test_table_db_array.dart | 103 ++++++--- .../lib/src/table_db_array.dart | 45 ++-- 3 files changed, 207 insertions(+), 159 deletions(-) diff --git a/packages/veilid_support/example/integration_test/app_test.dart b/packages/veilid_support/example/integration_test/app_test.dart index b7a807f..6912fd3 100644 --- a/packages/veilid_support/example/integration_test/app_test.dart +++ b/packages/veilid_support/example/integration_test/app_test.dart @@ -40,77 +40,7 @@ void main() { group('TableDBArray Tests', () { // test('create/delete TableDBArray', testTableDBArrayCreateDelete); - // group('TableDBArray Add/Get Tests', () { - // for (final params in [ - // // - // (99, 3, 15), - // (100, 4, 16), - // (101, 5, 17), - // // - // (511, 3, 127), - // (512, 4, 128), - // (513, 5, 129), - // // - // (4095, 3, 1023), - // (4096, 4, 1024), - // (4097, 5, 1025), - // // - // (65535, 3, 16383), - // (65536, 4, 16384), - // (65537, 5, 16385), - // ]) { - // final count = params.$1; - // final singles = params.$2; - // final batchSize = params.$3; - - // test( - // // timeout: const Timeout(Duration(seconds: 480)), - // 'add/remove TableDBArray count = $count batchSize=$batchSize', - // makeTestTableDBArrayAddGetClear( - // count: count, - // singles: singles, - // batchSize: batchSize, - // crypto: const VeilidCryptoPublic()), - // ); - // } - // }); - - // group('TableDBArray Insert Tests', () { - // for (final params in [ - // // - // (99, 3, 15), - // (100, 4, 16), - // (101, 5, 17), - // // - // (511, 3, 127), - // (512, 4, 128), - // (513, 5, 129), - // // - // (4095, 3, 1023), - // (4096, 4, 1024), - // (4097, 5, 1025), - // // - // (65535, 3, 16383), - // (65536, 4, 16384), - // (65537, 5, 16385), - // ]) { - // final count = params.$1; - // final singles = params.$2; - // final batchSize = params.$3; - - // test( - // // timeout: const Timeout(Duration(seconds: 480)), - // 'insert TableDBArray count=$count singles=$singles batchSize=$batchSize', - // makeTestTableDBArrayInsert( - // count: count, - // singles: singles, - // batchSize: batchSize, - // crypto: const VeilidCryptoPublic()), - // ); - // } - // }); - - group('TableDBArray Remove Tests', () { + group('TableDBArray Add/Get Tests', () { for (final params in [ // (99, 3, 15), @@ -134,7 +64,77 @@ void main() { final batchSize = params.$3; test( - // timeout: const Timeout(Duration(seconds: 480)), + timeout: const Timeout(Duration(seconds: 480)), + 'add/remove TableDBArray count = $count batchSize=$batchSize', + makeTestTableDBArrayAddGetClear( + count: count, + singles: singles, + batchSize: batchSize, + crypto: const VeilidCryptoPublic()), + ); + } + }); + + group('TableDBArray Insert Tests', () { + for (final params in [ + // + (99, 3, 15), + (100, 4, 16), + (101, 5, 17), + // + (511, 3, 127), + (512, 4, 128), + (513, 5, 129), + // + (4095, 3, 1023), + (4096, 4, 1024), + (4097, 5, 1025), + // + (65535, 3, 16383), + (65536, 4, 16384), + (65537, 5, 16385), + ]) { + final count = params.$1; + final singles = params.$2; + final batchSize = params.$3; + + test( + timeout: const Timeout(Duration(seconds: 480)), + 'insert TableDBArray count=$count singles=$singles batchSize=$batchSize', + makeTestTableDBArrayInsert( + count: count, + singles: singles, + batchSize: batchSize, + crypto: const VeilidCryptoPublic()), + ); + } + }); + + group('TableDBArray Remove Tests', () { + for (final params in [ + // + (99, 3, 15), + (100, 4, 16), + (101, 5, 17), + // + (511, 3, 127), + (512, 4, 128), + (513, 5, 129), + // + (4095, 3, 1023), + (4096, 4, 1024), + (4097, 5, 1025), + // + (16383, 3, 4095), + (16384, 4, 4096), + (16385, 5, 4097), + ]) { + final count = params.$1; + final singles = params.$2; + final batchSize = params.$3; + + test( + timeout: const Timeout(Duration(seconds: 480)), 'remove TableDBArray count=$count singles=$singles batchSize=$batchSize', makeTestTableDBArrayRemove( count: count, @@ -147,50 +147,50 @@ void main() { }); }); - // group('DHT Support Tests', () { - // setUpAll(updateProcessorFixture.setUp); - // setUpAll(tickerFixture.setUp); - // tearDownAll(tickerFixture.tearDown); - // tearDownAll(updateProcessorFixture.tearDown); + group('DHT Support Tests', () { + setUpAll(updateProcessorFixture.setUp); + setUpAll(tickerFixture.setUp); + tearDownAll(tickerFixture.tearDown); + tearDownAll(updateProcessorFixture.tearDown); - // test('create pool', testDHTRecordPoolCreate); + test('create pool', testDHTRecordPoolCreate); - // group('DHTRecordPool Tests', () { - // setUpAll(dhtRecordPoolFixture.setUp); - // tearDownAll(dhtRecordPoolFixture.tearDown); + group('DHTRecordPool Tests', () { + setUpAll(dhtRecordPoolFixture.setUp); + tearDownAll(dhtRecordPoolFixture.tearDown); - // test('create/delete record', testDHTRecordCreateDelete); - // test('record scopes', testDHTRecordScopes); - // test('create/delete deep record', testDHTRecordDeepCreateDelete); - // }); + test('create/delete record', testDHTRecordCreateDelete); + test('record scopes', testDHTRecordScopes); + test('create/delete deep record', testDHTRecordDeepCreateDelete); + }); - // group('DHTShortArray Tests', () { - // setUpAll(dhtRecordPoolFixture.setUp); - // tearDownAll(dhtRecordPoolFixture.tearDown); + group('DHTShortArray Tests', () { + setUpAll(dhtRecordPoolFixture.setUp); + tearDownAll(dhtRecordPoolFixture.tearDown); - // for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { - // test('create shortarray stride=$stride', - // makeTestDHTShortArrayCreateDelete(stride: stride)); - // test('add shortarray stride=$stride', - // makeTestDHTShortArrayAdd(stride: stride)); - // } - // }); + for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { + test('create shortarray stride=$stride', + makeTestDHTShortArrayCreateDelete(stride: stride)); + test('add shortarray stride=$stride', + makeTestDHTShortArrayAdd(stride: stride)); + } + }); - // group('DHTLog Tests', () { - // setUpAll(dhtRecordPoolFixture.setUp); - // tearDownAll(dhtRecordPoolFixture.tearDown); + group('DHTLog Tests', () { + setUpAll(dhtRecordPoolFixture.setUp); + tearDownAll(dhtRecordPoolFixture.tearDown); - // for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { - // test('create log stride=$stride', - // makeTestDHTLogCreateDelete(stride: stride)); - // test( - // timeout: const Timeout(Duration(seconds: 480)), - // 'add/truncate log stride=$stride', - // makeTestDHTLogAddTruncate(stride: stride), - // ); - // } - // }); - // }); + for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { + test('create log stride=$stride', + makeTestDHTLogCreateDelete(stride: stride)); + test( + timeout: const Timeout(Duration(seconds: 480)), + 'add/truncate log stride=$stride', + makeTestDHTLogAddTruncate(stride: stride), + ); + } + }); + }); }); }); } diff --git a/packages/veilid_support/example/integration_test/test_table_db_array.dart b/packages/veilid_support/example/integration_test/test_table_db_array.dart index 81607a6..e67bc39 100644 --- a/packages/veilid_support/example/integration_test/test_table_db_array.dart +++ b/packages/veilid_support/example/integration_test/test_table_db_array.dart @@ -84,7 +84,8 @@ Future Function() makeTestTableDBArrayAddGetClear( { for (var n = batchSize; n < count; n += batchSize) { final toGet = min(batchSize, count - n); - expect(await arr.getRange(n, toGet), equals(makeDataBatch(n, toGet))); + expect(await arr.getRange(n, n + toGet), + equals(makeDataBatch(n, toGet))); } } @@ -140,7 +141,7 @@ Future Function() makeTestTableDBArrayInsert( { for (var n = batchSize; n < count; n += batchSize) { final toGet = min(batchSize, count - n); - expect(await arr.getRange(n, toGet), + expect(await arr.getRange(n, n + toGet), equals(match.sublist(n, n + toGet))); } } @@ -154,7 +155,6 @@ Future Function() makeTestTableDBArrayInsert( await arr.close(delete: true); }; - Future Function() makeTestTableDBArrayRemove( {required int count, required int singles, @@ -164,42 +164,79 @@ Future Function() makeTestTableDBArrayRemove( final arr = await TableDBArray.make(table: 'testArray', crypto: crypto); final match = []; -xxx removal test - print('inserting'); + { - for (var n = 0; n < count;) { - final start = n; - var toAdd = min(batchSize, count - n); - for (var s = 0; s < min(singles, toAdd); s++) { - final data = makeData(n); - await arr.insert(start, data); - match.insert(start, data); - toAdd--; - n++; + final rems = [ + (0, 0), + (0, 1), + (0, batchSize), + (1, batchSize - 1), + (batchSize, 1), + (batchSize + 1, batchSize), + (batchSize - 1, batchSize + 1) + ]; + for (final rem in rems) { + print('adding '); + { + for (var n = match.length; n < count;) { + final toAdd = min(batchSize, count - n); + final data = makeDataBatch(n, toAdd); + await arr.addAll(data); + match.addAll(data); + n += toAdd; + print(' $n/$count'); + } + expect(arr.length, equals(match.length)); } - final data = makeDataBatch(n, toAdd); - await arr.insertAll(start, data); - match.insertAll(start, data); - n += toAdd; + { + final start = rem.$1; + final length = rem.$2; + print('removing start=$start length=$length'); - print(' $n/$count'); - } - } + final out = Output>(); + await arr.removeRange(start, start + length, out: out); + expect(out.value, equals(match.sublist(start, start + length))); + match.removeRange(start, start + length); + expect(arr.length, equals(match.length)); - print('get singles'); - { - for (var n = 0; n < batchSize; n++) { - expect(await arr.get(n), equals(match[n])); - } - } + print('get batch'); + { + final checkCount = match.length; + for (var n = 0; n < checkCount;) { + final toGet = min(batchSize, checkCount - n); + expect(await arr.getRange(n, n + toGet), + equals(match.sublist(n, n + toGet))); + n += toGet; + print(' $n/$checkCount'); + } + } + } - print('get batch'); - { - for (var n = batchSize; n < count; n += batchSize) { - final toGet = min(batchSize, count - n); - expect(await arr.getRange(n, toGet), - equals(match.sublist(n, n + toGet))); + { + final start = match.length - rem.$1 - rem.$2; + final length = rem.$2; + print('removing from end start=$start length=$length'); + + final out = Output>(); + await arr.removeRange(start, start + length, out: out); + expect(out.value, equals(match.sublist(start, start + length))); + match.removeRange(start, start + length); + expect(arr.length, equals(match.length)); + + print('get batch'); + { + final checkCount = match.length; + for (var n = 0; n < checkCount;) { + final toGet = min(batchSize, checkCount - n); + expect(await arr.getRange(n, n + toGet), + equals(match.sublist(n, n + toGet))); + n += toGet; + print(' $n/$checkCount'); + } + expect(arr.length, equals(match.length)); + } + } } } diff --git a/packages/veilid_support/lib/src/table_db_array.dart b/packages/veilid_support/lib/src/table_db_array.dart index dbabd3a..51b15b8 100644 --- a/packages/veilid_support/lib/src/table_db_array.dart +++ b/packages/veilid_support/lib/src/table_db_array.dart @@ -112,13 +112,13 @@ class TableDBArray { }); } - Future> getRange(int start, int length) async { + Future> getRange(int start, int end) async { await _initWait(); return _mutex.protect(() async { if (!_open) { throw StateError('not open'); } - return _getRangeInner(start, length); + return _getRangeInner(start, end); }); } @@ -127,11 +127,11 @@ class TableDBArray { return _writeTransaction((t) async => _removeInner(t, pos, out: out)); } - Future removeRange(int start, int length, + Future removeRange(int start, int end, {Output>? out}) async { await _initWait(); return _writeTransaction( - (t) async => _removeRangeInner(t, start, length, out: out)); + (t) async => _removeRangeInner(t, start, end, out: out)); } Future clear() async { @@ -223,23 +223,34 @@ class TableDBArray { return (await _loadEntry(entry))!; } - Future> _getRangeInner(int start, int length) async { + Future> _getRangeInner(int start, int end) async { + final length = end - start; if (length < 0) { throw StateError('length should not be negative'); } if (start < 0 || start >= _length) { throw IndexError.withLength(start, _length); } - if ((start + length) > _length) { - throw IndexError.withLength(start + length, _length); + if (end > _length) { + throw IndexError.withLength(end, _length); } final out = []; - for (var pos = start; pos < (start + length); pos++) { - final entry = await _getIndexEntry(pos); - final value = (await _loadEntry(entry))!; - out.add(value); + const batchSize = 16; + + for (var pos = start; pos < end;) { + var batchLen = min(batchSize, end - pos); + final dws = DelayedWaitSet(); + while (batchLen > 0) { + final entry = await _getIndexEntry(pos); + dws.add(() async => (await _loadEntry(entry))!); + pos++; + batchLen--; + } + final batchOut = await dws(); + out.addAll(batchOut); } + return out; } @@ -259,21 +270,21 @@ class TableDBArray { await _removeIndexEntry(pos); } - Future _removeRangeInner( - VeilidTableDBTransaction t, int start, int length, + Future _removeRangeInner(VeilidTableDBTransaction t, int start, int end, {Output>? out}) async { + final length = end - start; if (length < 0) { throw StateError('length should not be negative'); } - if (start < 0 || start >= _length) { + if (start < 0) { throw IndexError.withLength(start, _length); } - if ((start + length) > _length) { - throw IndexError.withLength(start + length, _length); + if (end > _length) { + throw IndexError.withLength(end, _length); } final outList = []; - for (var pos = start; pos < (start + length); pos++) { + for (var pos = start; pos < end; pos++) { final entry = await _getIndexEntry(pos); if (out != null) { final value = (await _loadEntry(entry))!; From 9c5feed732145ed3944d2a640c98bd5bb051dc77 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 27 May 2024 18:04:00 -0400 Subject: [PATCH 114/270] messages wip --- lib/chat/cubits/active_chat_cubit.dart | 4 +- .../cubits/single_contact_messages_cubit.dart | 27 ++++- lib/chat/models/message_state.dart | 20 +++- lib/chat/models/message_state.freezed.dart | 6 +- lib/chat/views/chat_component.dart | 108 ++++++++++++++---- .../active_conversations_bloc_map_cubit.dart | 6 +- ...ve_single_contact_chat_bloc_map_cubit.dart | 9 +- lib/chat_list/cubits/chat_list_cubit.dart | 65 ++++++----- .../chat_single_contact_item_widget.dart | 10 +- .../chat_single_contact_list_widget.dart | 6 +- .../views/contact_invitation_item_widget.dart | 6 +- lib/contacts/cubits/contact_list_cubit.dart | 4 +- lib/contacts/views/contact_item_widget.dart | 11 +- .../home_account_ready_chat.dart | 2 +- .../home_account_ready_main.dart | 6 +- lib/proto/veilidchat.pb.dart | 64 +++++------ lib/proto/veilidchat.pbjson.dart | 72 ++++++------ lib/proto/veilidchat.proto | 19 ++- 18 files changed, 274 insertions(+), 171 deletions(-) diff --git a/lib/chat/cubits/active_chat_cubit.dart b/lib/chat/cubits/active_chat_cubit.dart index e47caec..a1872c2 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? activeChatRemoteConversationRecordKey) { - emit(activeChatRemoteConversationRecordKey); + void setActiveChat(TypedKey? activeChatLocalConversationRecordKey) { + emit(activeChatLocalConversationRecordKey); } } diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index e018e79..20c8d22 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -53,14 +53,12 @@ class SingleContactMessagesCubit extends Cubit { 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, super(const AsyncValue.loading()) { // Async Init _initWait.add(_init); @@ -420,7 +418,14 @@ class SingleContactMessagesCubit extends Cubit { emit(AsyncValue.data(renderedState)); } - void addMessage({required proto.Message message}) { + void addTextMessage({required proto.Message_Text messageText}) { + final message = proto.Message() + ..author = _activeAccountInfo.localAccount.identityMaster + .identityPublicTypedKey() + .toProto() + ..timestamp = Veilid.instance.now().toInt64() + ..text = messageText; + _unreconciledMessagesQueue.addSync(message); _sendingMessagesQueue.addSync(message); @@ -428,6 +433,21 @@ class SingleContactMessagesCubit extends Cubit { _renderState(); } + ///////////////////////////////////////////////////////////////////////// + + static Future cleanupAndDeleteMessages( + {required TypedKey localConversationRecordKey}) async { + final recmsgdbname = + _reconciledMessagesTableDBName(localConversationRecordKey); + await Veilid.instance.deleteTableDB(recmsgdbname); + } + + static String _reconciledMessagesTableDBName( + TypedKey localConversationRecordKey) => + 'msg_$localConversationRecordKey'; + + ///////////////////////////////////////////////////////////////////////// + final WaitSet _initWait = WaitSet(); final ActiveAccountInfo _activeAccountInfo; final TypedKey _remoteIdentityPublicKey; @@ -435,7 +455,6 @@ class SingleContactMessagesCubit extends Cubit { final TypedKey _localMessagesRecordKey; final TypedKey _remoteConversationRecordKey; final TypedKey _remoteMessagesRecordKey; - final OwnedDHTRecordPointer _reconciledChatRecord; late final VeilidCrypto _messagesCrypto; diff --git a/lib/chat/models/message_state.dart b/lib/chat/models/message_state.dart index 8618054..e14c9e8 100644 --- a/lib/chat/models/message_state.dart +++ b/lib/chat/models/message_state.dart @@ -30,10 +30,28 @@ class MessageState with _$MessageState { required proto.Message content, // Received or delivered timestamp required Timestamp timestamp, - // The state of the mssage + // The state of the message required MessageSendState? sendState, }) = _MessageState; factory MessageState.fromJson(dynamic json) => _$MessageStateFromJson(json as Map); } + +extension MessageStateExt on MessageState { + String get uniqueId { + final author = content.author.toVeilid().toString(); + final id = base64UrlNoPadEncode(content.id); + return '$author|$id'; + } + + static (proto.TypedKey, Uint8List) splitUniqueId(String uniqueId) { + final parts = uniqueId.split('|'); + if (parts.length != 2) { + throw Exception('invalid unique id'); + } + final author = TypedKey.fromString(parts[0]).toProto(); + final id = base64UrlNoPadDecode(parts[1]); + return (author, id); + } +} diff --git a/lib/chat/models/message_state.freezed.dart b/lib/chat/models/message_state.freezed.dart index ec8195b..b411f4c 100644 --- a/lib/chat/models/message_state.freezed.dart +++ b/lib/chat/models/message_state.freezed.dart @@ -25,7 +25,7 @@ mixin _$MessageState { proto.Message get content => throw _privateConstructorUsedError; // Received or delivered timestamp Timestamp get timestamp => - throw _privateConstructorUsedError; // The state of the mssage + throw _privateConstructorUsedError; // The state of the message MessageSendState? get sendState => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @@ -147,7 +147,7 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { // Received or delivered timestamp @override final Timestamp timestamp; -// The state of the mssage +// The state of the message @override final MessageSendState? sendState; @@ -211,7 +211,7 @@ abstract class _MessageState implements MessageState { proto.Message get content; @override // Received or delivered timestamp Timestamp get timestamp; - @override // The state of the mssage + @override // The state of the message MessageSendState? get sendState; @override @JsonKey(ignore: true) diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 8ca471e..2ca49a1 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -1,5 +1,8 @@ +import 'dart:typed_data'; + import 'package:async_tools/async_tools.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart' as types; @@ -13,6 +16,10 @@ import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; import '../chat.dart'; +const String metadataKeyExpirationDuration = 'expiration'; +const String metadataKeyViewLimit = 'view_limit'; +const String metadataKeyAttachments = 'attachments'; + class ChatComponent extends StatelessWidget { const ChatComponent._( {required TypedKey localUserIdentityKey, @@ -35,7 +42,7 @@ class ChatComponent extends StatelessWidget { // Builder wrapper function that takes care of state management requirements static Widget builder( - {required TypedKey remoteConversationRecordKey, Key? key}) => + {required TypedKey localConversationRecordKey, Key? key}) => Builder(builder: (context) { // Get all watched dependendies final activeAccountInfo = context.watch(); @@ -51,7 +58,7 @@ class ChatComponent extends StatelessWidget { } final avconversation = context.select?>( - (x) => x.state[remoteConversationRecordKey]); + (x) => x.state[localConversationRecordKey]); if (avconversation == null) { return waitingPage(); } @@ -77,7 +84,7 @@ class ChatComponent extends StatelessWidget { // Get the messages cubit final messages = context.select( - (x) => x.tryOperate(remoteConversationRecordKey, + (x) => x.tryOperate(localConversationRecordKey, closure: (cubit) => (cubit, cubit.state))); // Get the messages to display @@ -97,8 +104,8 @@ class ChatComponent extends StatelessWidget { ///////////////////////////////////////////////////////////////////// - types.Message messageToChatMessage(MessageState message) { - final isLocal = message.author == _localUserIdentityKey; + types.Message? messageToChatMessage(MessageState message) { + final isLocal = message.content.author.toVeilid() == _localUserIdentityKey; types.Status? status; if (message.sendState != null) { @@ -113,31 +120,83 @@ class ChatComponent extends StatelessWidget { } } - final textMessage = types.TextMessage( - author: isLocal ? _localUser : _remoteUser, - createdAt: (message.timestamp.value ~/ BigInt.from(1000)).toInt(), - id: message.timestamp.toString(), - text: message.text, - showStatus: status != null, - status: status); - return textMessage; + switch (message.content.whichKind()) { + case proto.Message_Kind.text: + final contextText = message.content.text; + final textMessage = types.TextMessage( + author: isLocal ? _localUser : _remoteUser, + createdAt: (message.timestamp.value ~/ BigInt.from(1000)).toInt(), + id: message.uniqueId, + text: contextText.text, + showStatus: status != null, + status: status); + return textMessage; + case proto.Message_Kind.secret: + case proto.Message_Kind.delete: + case proto.Message_Kind.erase: + case proto.Message_Kind.settings: + case proto.Message_Kind.permissions: + case proto.Message_Kind.membership: + case proto.Message_Kind.moderation: + case proto.Message_Kind.notSet: + return null; + } } - void _addMessage(proto.Message message) { - if (message.text.isEmpty) { - return; + void _addTextMessage( + {required String text, + String? topic, + Uint8List? replyId, + Timestamp? expiration, + int? viewLimit, + List attachments = const []}) { + final protoMessageText = proto.Message_Text()..text = text; + if (topic != null) { + protoMessageText.topic = topic; } - _messagesCubit.addMessage(message: message); + if (replyId != null) { + protoMessageText.replyId = replyId; + } + protoMessageText + ..expiration = expiration?.toInt64() ?? Int64.ZERO + ..viewLimit = viewLimit ?? 0; + protoMessageText.attachments.addAll(attachments); + + _messagesCubit.addTextMessage(messageText: protoMessageText); } void _handleSendPressed(types.PartialText message) { - final protoMessage = proto.Message() - ..author = _localUserIdentityKey.toProto() - ..timestamp = Veilid.instance.now().toInt64() - ..text = message.text; - //..signature = signature; + final text = message.text; + final replyId = (message.repliedMessage != null) + ? MessageStateExt.splitUniqueId(message.repliedMessage!.id).$2 + : null; + Timestamp? expiration; + int? viewLimit; + List? attachments; + final metadata = message.metadata; + if (metadata != null) { + final expirationValue = + metadata[metadataKeyExpirationDuration] as TimestampDuration?; + if (expirationValue != null) { + expiration = Veilid.instance.now().offset(expirationValue); + } + final viewLimitValue = metadata[metadataKeyViewLimit] as int?; + if (viewLimitValue != null) { + viewLimit = viewLimitValue; + } + final attachmentsValue = + metadata[metadataKeyAttachments] as List?; + if (attachmentsValue != null) { + attachments = attachmentsValue; + } + } - _addMessage(protoMessage); + _addTextMessage( + text: text, + replyId: replyId, + expiration: expiration, + viewLimit: viewLimit, + attachments: attachments ?? []); } // void _handleAttachmentPressed() async { @@ -161,6 +220,9 @@ class ChatComponent extends StatelessWidget { final tsSet = {}; for (final message in messages) { final chatMessage = messageToChatMessage(message); + if (chatMessage == null) { + continue; + } chatMessages.insert(0, chatMessage); if (!tsSet.add(chatMessage.id)) { // ignore: avoid_print 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 b221208..c497941 100644 --- a/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart +++ b/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart @@ -31,7 +31,7 @@ typedef ActiveConversationCubit = TransformerCubit< typedef ActiveConversationsBlocMapState = BlocMapState>; -// Map of remoteConversationRecordKey to ActiveConversationCubit +// Map of localConversationRecordKey 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 @@ -49,7 +49,7 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit _addConversation({required proto.Contact contact}) async => add(() => MapEntry( - contact.remoteConversationRecordKey.toVeilid(), + contact.localConversationRecordKey.toVeilid(), TransformerCubit( ConversationCubit( activeAccountInfo: _activeAccountInfo, @@ -86,7 +86,7 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit c.value.remoteConversationRecordKey.toVeilid() == key); + (c) => c.value.localConversationRecordKey.toVeilid() == key); if (contactIndex == -1) { await addState(key, AsyncValue.error('Contact not found')); return; 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 index d9ecb67..914d357 100644 --- 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 @@ -11,7 +11,7 @@ import '../../proto/proto.dart' as proto; import 'active_conversations_bloc_map_cubit.dart'; import 'chat_list_cubit.dart'; -// Map of remoteConversationRecordKey to MessagesCubit +// Map of localConversationRecordKey to MessagesCubit // Wraps a MessagesCubit to stream the latest messages to the state // Automatically follows the state of a ActiveConversationsBlocMapCubit. class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit add(() => MapEntry( - contact.remoteConversationRecordKey.toVeilid(), + contact.localConversationRecordKey.toVeilid(), SingleContactMessagesCubit( activeAccountInfo: _activeAccountInfo, remoteIdentityPublicKey: contact.identityPublicKey.toVeilid(), @@ -43,7 +43,6 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit c.value.remoteConversationRecordKey.toVeilid() == key); + (c) => c.value.localConversationRecordKey.toVeilid() == key); if (contactIndex == -1) { await addState( key, AsyncValue.error('Contact not found for conversation')); @@ -76,7 +75,7 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit c.value.remoteConversationRecordKey.toVeilid() == key); + (c) => c.value.localConversationRecordKey.toVeilid() == key); if (contactIndex == -1) { await addState(key, AsyncValue.error('Chat not found for conversation')); return; diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index d04b008..ff1d5d0 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; @@ -21,8 +22,7 @@ class ChatListCubit extends DHTShortArrayCubit required ActiveAccountInfo activeAccountInfo, required proto.Account account, required this.activeChatCubit, - }) : _activeAccountInfo = activeAccountInfo, - super( + }) : super( open: () => _open(activeAccountInfo, account), decodeElement: proto.Chat.fromBuffer); @@ -39,16 +39,30 @@ class ChatListCubit extends DHTShortArrayCubit return dhtRecord; } + Future getDefaultChatSettings( + proto.Contact contact) async { + final pronouns = contact.editedProfile.pronouns.isEmpty + ? '' + : ' (${contact.editedProfile.pronouns})'; + return proto.ChatSettings() + ..title = '${contact.editedProfile.name}$pronouns' + ..description = '' + ..defaultExpiration = Int64.ZERO; + } + /// Create a new chat (singleton for single contact chats) Future getOrCreateChatSingleContact({ - required TypedKey remoteConversationRecordKey, + required proto.Contact contact, }) async { + // Make local copy so we don't share the buffer + final localConversationRecordKey = + contact.localConversationRecordKey.toVeilid(); + final remoteConversationRecordKey = + contact.remoteConversationRecordKey.toVeilid(); + // Add Chat to account's list // if this fails, don't keep retrying, user can try again later await operateWrite((writer) async { - final remoteConversationRecordKeyProto = - remoteConversationRecordKey.toProto(); - // See if we have added this chat already for (var i = 0; i < writer.length; i++) { final cbuf = await writer.getItem(i); @@ -56,26 +70,18 @@ class ChatListCubit extends DHTShortArrayCubit throw Exception('Failed to get chat'); } final c = proto.Chat.fromBuffer(cbuf); - if (c.remoteConversationRecordKey == remoteConversationRecordKeyProto) { + if (c.localConversationRecordKey == + contact.localConversationRecordKey) { // 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 DHTLog.create( - debugName: - 'ChatListCubit::getOrCreateChatSingleContact::ReconciledChat', - parent: accountRecordKey)) - .scope((r) async => r.recordPointer); - - // Create conversation type Chat + // Create 1:1 conversation type Chat final chat = proto.Chat() - ..type = proto.ChatType.SINGLE_CONTACT - ..remoteConversationRecordKey = remoteConversationRecordKeyProto - ..reconciledChatRecord = reconciledChatRecord.toProto(); + ..settings = await getDefaultChatSettings(contact) + ..localConversationRecordKey = localConversationRecordKey.toProto() + ..remoteConversationRecordKey = remoteConversationRecordKey.toProto(); // Add chat final added = await writer.tryAddItem(chat.writeToBuffer()); @@ -87,15 +93,16 @@ class ChatListCubit extends DHTShortArrayCubit /// Delete a chat Future deleteChat( - {required TypedKey remoteConversationRecordKey}) async { - final remoteConversationKey = remoteConversationRecordKey.toProto(); + {required TypedKey localConversationRecordKey}) async { + final localConversationRecordKeyProto = + localConversationRecordKey.toProto(); // Remove Chat from account's list // if this fails, don't keep retrying, user can try again later final deletedItem = // Ensure followers get their changes before we return await syncFollowers(() => operateWrite((writer) async { - if (activeChatCubit.state == remoteConversationRecordKey) { + if (activeChatCubit.state == localConversationRecordKey) { activeChatCubit.setActiveChat(null); } for (var i = 0; i < writer.length; i++) { @@ -104,7 +111,8 @@ class ChatListCubit extends DHTShortArrayCubit if (c == null) { throw Exception('Failed to get chat'); } - if (c.remoteConversationRecordKey == remoteConversationKey) { + if (c.localConversationRecordKey == + localConversationRecordKeyProto) { // Found the right chat await writer.removeItem(i); return c; @@ -116,10 +124,10 @@ class ChatListCubit extends DHTShortArrayCubit // chat record now if (deletedItem != null) { try { - await DHTRecordPool.instance.deleteRecord( - deletedItem.reconciledChatRecord.toVeilid().recordKey); + await SingleContactMessagesCubit.cleanupAndDeleteMessages( + localConversationRecordKey: localConversationRecordKey); } on Exception catch (e) { - log.debug('error removing reconciled chat record: $e', e); + log.debug('error removing reconciled chat table: $e', e); } } } @@ -132,10 +140,9 @@ class ChatListCubit extends DHTShortArrayCubit return IMap(); } return IMap.fromIterable(stateValue, - keyMapper: (e) => e.value.remoteConversationRecordKey.toVeilid(), + keyMapper: (e) => e.value.localConversationRecordKey.toVeilid(), valueMapper: (e) => e.value); } final ActiveChatCubit activeChatCubit; - final ActiveAccountInfo _activeAccountInfo; } 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 75501b4..ce9cf0e 100644 --- a/lib/chat_list/views/chat_single_contact_item_widget.dart +++ b/lib/chat_list/views/chat_single_contact_item_widget.dart @@ -24,9 +24,9 @@ class ChatSingleContactItemWidget extends StatelessWidget { BuildContext context, ) { final activeChatCubit = context.watch(); - final remoteConversationRecordKey = - _contact.remoteConversationRecordKey.toVeilid(); - final selected = activeChatCubit.state == remoteConversationRecordKey; + final localConversationRecordKey = + _contact.localConversationRecordKey.toVeilid(); + final selected = activeChatCubit.state == localConversationRecordKey; return SliderTile( key: ObjectKey(_contact), @@ -38,7 +38,7 @@ class ChatSingleContactItemWidget extends StatelessWidget { icon: Icons.chat, onTap: () { singleFuture(activeChatCubit, () async { - activeChatCubit.setActiveChat(remoteConversationRecordKey); + activeChatCubit.setActiveChat(localConversationRecordKey); }); }, endActions: [ @@ -49,7 +49,7 @@ class ChatSingleContactItemWidget extends StatelessWidget { onPressed: (context) async { final chatListCubit = context.read(); await chatListCubit.deleteChat( - remoteConversationRecordKey: remoteConversationRecordKey); + localConversationRecordKey: localConversationRecordKey); }) ], ); 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 785dbcb..9053bc6 100644 --- a/lib/chat_list/views/chat_single_contact_list_widget.dart +++ b/lib/chat_list/views/chat_single_contact_list_widget.dart @@ -20,7 +20,7 @@ class ChatSingleContactListWidget extends StatelessWidget { return contactListV.builder((context, contactList) { final contactMap = IMap.fromIterable(contactList, - keyMapper: (c) => c.value.remoteConversationRecordKey, + keyMapper: (c) => c.value.localConversationRecordKey, valueMapper: (c) => c.value); final chatListV = context.watch().state; @@ -36,7 +36,7 @@ class ChatSingleContactListWidget extends StatelessWidget { initialList: chatList.map((x) => x.value).toList(), itemBuilder: (c) { final contact = - contactMap[c.remoteConversationRecordKey]; + contactMap[c.localConversationRecordKey]; if (contact == null) { return const Text('...'); } @@ -49,7 +49,7 @@ class ChatSingleContactListWidget extends StatelessWidget { final lowerValue = value.toLowerCase(); return chatList.map((x) => x.value).where((c) { final contact = - contactMap[c.remoteConversationRecordKey]; + contactMap[c.localConversationRecordKey]; if (contact == null) { return false; } diff --git a/lib/contact_invitation/views/contact_invitation_item_widget.dart b/lib/contact_invitation/views/contact_invitation_item_widget.dart index fcf021f..c2a93c7 100644 --- a/lib/contact_invitation/views/contact_invitation_item_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_item_widget.dart @@ -27,12 +27,12 @@ class ContactInvitationItemWidget extends StatelessWidget { @override // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { - // final remoteConversationKey = - // contact.remoteConversationRecordKey.toVeilid(); + // final localConversationKey = + // contact.localConversationRecordKey.toVeilid(); const selected = false; // xxx: eventually when we have selectable invitations: - // activeContactCubit.state == remoteConversationRecordKey; + // activeContactCubit.state == localConversationRecordKey; final tileDisabled = disabled || context.watch().isBusy; diff --git a/lib/contacts/cubits/contact_list_cubit.dart b/lib/contacts/cubits/contact_list_cubit.dart index a139b89..2eb8a08 100644 --- a/lib/contacts/cubits/contact_list_cubit.dart +++ b/lib/contacts/cubits/contact_list_cubit.dart @@ -76,8 +76,8 @@ class ContactListCubit extends DHTShortArrayCubit { if (item == null) { throw Exception('Failed to get contact'); } - if (item.remoteConversationRecordKey == - contact.remoteConversationRecordKey) { + if (item.localConversationRecordKey == + contact.localConversationRecordKey) { await writer.removeItem(i); return item; } diff --git a/lib/contacts/views/contact_item_widget.dart b/lib/contacts/views/contact_item_widget.dart index dfe9e6e..3deae23 100644 --- a/lib/contacts/views/contact_item_widget.dart +++ b/lib/contacts/views/contact_item_widget.dart @@ -29,11 +29,11 @@ class ContactItemWidget extends StatelessWidget { Widget build( BuildContext context, ) { - final remoteConversationKey = - contact.remoteConversationRecordKey.toVeilid(); + final localConversationRecordKey = + contact.localConversationRecordKey.toVeilid(); const selected = false; // xxx: eventually when we have selectable contacts: - // activeContactCubit.state == remoteConversationRecordKey; + // activeContactCubit.state == localConversationRecordKey; final tileDisabled = disabled || context.watch().isBusy; @@ -49,8 +49,7 @@ class ContactItemWidget extends StatelessWidget { // Start a chat final chatListCubit = context.read(); - await chatListCubit.getOrCreateChatSingleContact( - remoteConversationRecordKey: remoteConversationKey); + await chatListCubit.getOrCreateChatSingleContact(contact: contact); // Click over to chats if (context.mounted) { await MainPager.of(context) @@ -69,7 +68,7 @@ class ContactItemWidget extends StatelessWidget { // Remove any chats for this contact await chatListCubit.deleteChat( - remoteConversationRecordKey: remoteConversationKey); + localConversationRecordKey: localConversationRecordKey); // Delete the contact itself await contactListCubit.deleteContact(contact: contact); 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 621f9e8..fb0e7b4 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 @@ -34,7 +34,7 @@ class HomeAccountReadyChatState extends State { return const EmptyChatWidget(); } return ChatComponent.builder( - remoteConversationRecordKey: activeChatRemoteConversationKey); + localConversationRecordKey: activeChatRemoteConversationKey); } @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 e6ed99e..59eb00b 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 @@ -66,13 +66,13 @@ class _HomeAccountReadyMainState extends State { Material(color: Colors.transparent, child: buildUserPanel())); Widget buildTabletRightPane(BuildContext context) { - final activeChatRemoteConversationKey = + final activeChatLocalConversationKey = context.watch().state; - if (activeChatRemoteConversationKey == null) { + if (activeChatLocalConversationKey == null) { return const EmptyChatWidget(); } return ChatComponent.builder( - remoteConversationRecordKey: activeChatRemoteConversationKey); + localConversationRecordKey: activeChatLocalConversationKey); } // ignore: prefer_expression_function_bodies diff --git a/lib/proto/veilidchat.pb.dart b/lib/proto/veilidchat.pb.dart index 164c117..3544f00 100644 --- a/lib/proto/veilidchat.pb.dart +++ b/lib/proto/veilidchat.pb.dart @@ -350,9 +350,9 @@ class Message_Text extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.Text', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) ..aOS(1, _omitFieldNames ? '' : 'text') ..aOS(2, _omitFieldNames ? '' : 'topic') - ..aOM<$0.TypedKey>(3, _omitFieldNames ? '' : 'replyId', subBuilder: $0.TypedKey.create) + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'replyId', $pb.PbFieldType.OY) ..a<$fixnum.Int64>(4, _omitFieldNames ? '' : 'expiration', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) - ..a<$fixnum.Int64>(5, _omitFieldNames ? '' : 'viewLimit', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..a<$core.int>(5, _omitFieldNames ? '' : 'viewLimit', $pb.PbFieldType.OU3) ..pc(6, _omitFieldNames ? '' : 'attachments', $pb.PbFieldType.PM, subBuilder: Attachment.create) ..hasRequiredFields = false ; @@ -397,15 +397,13 @@ class Message_Text extends $pb.GeneratedMessage { void clearTopic() => clearField(2); @$pb.TagNumber(3) - $0.TypedKey get replyId => $_getN(2); + $core.List<$core.int> get replyId => $_getN(2); @$pb.TagNumber(3) - set replyId($0.TypedKey v) { setField(3, v); } + set replyId($core.List<$core.int> v) { $_setBytes(2, v); } @$pb.TagNumber(3) $core.bool hasReplyId() => $_has(2); @$pb.TagNumber(3) void clearReplyId() => clearField(3); - @$pb.TagNumber(3) - $0.TypedKey ensureReplyId() => $_ensure(2); @$pb.TagNumber(4) $fixnum.Int64 get expiration => $_getI64(3); @@ -417,9 +415,9 @@ class Message_Text extends $pb.GeneratedMessage { void clearExpiration() => clearField(4); @$pb.TagNumber(5) - $fixnum.Int64 get viewLimit => $_getI64(4); + $core.int get viewLimit => $_getIZ(4); @$pb.TagNumber(5) - set viewLimit($fixnum.Int64 v) { $_setInt64(4, v); } + set viewLimit($core.int v) { $_setUnsignedInt32(4, v); } @$pb.TagNumber(5) $core.bool hasViewLimit() => $_has(4); @$pb.TagNumber(5) @@ -517,13 +515,13 @@ class Message_ControlDelete extends $pb.GeneratedMessage { $core.List<$0.TypedKey> get ids => $_getList(0); } -class Message_ControlClear extends $pb.GeneratedMessage { - factory Message_ControlClear() => create(); - Message_ControlClear._() : super(); - factory Message_ControlClear.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory Message_ControlClear.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); +class Message_ControlErase extends $pb.GeneratedMessage { + factory Message_ControlErase() => create(); + Message_ControlErase._() : super(); + factory Message_ControlErase.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Message_ControlErase.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.ControlClear', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.ControlErase', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) ..a<$fixnum.Int64>(1, _omitFieldNames ? '' : 'timestamp', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) ..hasRequiredFields = false ; @@ -532,22 +530,22 @@ class Message_ControlClear extends $pb.GeneratedMessage { 'Using this can add significant overhead to your binary. ' 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' 'Will be removed in next major version') - Message_ControlClear clone() => Message_ControlClear()..mergeFromMessage(this); + Message_ControlErase clone() => Message_ControlErase()..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_ControlClear copyWith(void Function(Message_ControlClear) updates) => super.copyWith((message) => updates(message as Message_ControlClear)) as Message_ControlClear; + Message_ControlErase copyWith(void Function(Message_ControlErase) updates) => super.copyWith((message) => updates(message as Message_ControlErase)) as Message_ControlErase; $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') - static Message_ControlClear create() => Message_ControlClear._(); - Message_ControlClear createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); + static Message_ControlErase create() => Message_ControlErase._(); + Message_ControlErase createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static Message_ControlClear getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static Message_ControlClear? _defaultInstance; + static Message_ControlErase getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Message_ControlErase? _defaultInstance; @$pb.TagNumber(1) $fixnum.Int64 get timestamp => $_getI64(0); @@ -735,7 +733,7 @@ enum Message_Kind { text, secret, delete, - clear_7, + erase, settings, permissions, membership, @@ -753,7 +751,7 @@ class Message extends $pb.GeneratedMessage { 4 : Message_Kind.text, 5 : Message_Kind.secret, 6 : Message_Kind.delete, - 7 : Message_Kind.clear_7, + 7 : Message_Kind.erase, 8 : Message_Kind.settings, 9 : Message_Kind.permissions, 10 : Message_Kind.membership, @@ -762,13 +760,13 @@ class Message extends $pb.GeneratedMessage { }; static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) ..oo(0, [4, 5, 6, 7, 8, 9, 10, 11]) - ..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'id', subBuilder: $0.TypedKey.create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'id', $pb.PbFieldType.OY) ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'author', subBuilder: $0.TypedKey.create) ..a<$fixnum.Int64>(3, _omitFieldNames ? '' : 'timestamp', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) ..aOM(4, _omitFieldNames ? '' : 'text', subBuilder: Message_Text.create) ..aOM(5, _omitFieldNames ? '' : 'secret', subBuilder: Message_Secret.create) ..aOM(6, _omitFieldNames ? '' : 'delete', subBuilder: Message_ControlDelete.create) - ..aOM(7, _omitFieldNames ? '' : 'clear', subBuilder: Message_ControlClear.create) + ..aOM(7, _omitFieldNames ? '' : 'erase', subBuilder: Message_ControlErase.create) ..aOM(8, _omitFieldNames ? '' : 'settings', subBuilder: Message_ControlSettings.create) ..aOM(9, _omitFieldNames ? '' : 'permissions', subBuilder: Message_ControlPermissions.create) ..aOM(10, _omitFieldNames ? '' : 'membership', subBuilder: Message_ControlMembership.create) @@ -802,15 +800,13 @@ class Message extends $pb.GeneratedMessage { void clearKind() => clearField($_whichOneof(0)); @$pb.TagNumber(1) - $0.TypedKey get id => $_getN(0); + $core.List<$core.int> get id => $_getN(0); @$pb.TagNumber(1) - set id($0.TypedKey v) { setField(1, v); } + set id($core.List<$core.int> v) { $_setBytes(0, v); } @$pb.TagNumber(1) $core.bool hasId() => $_has(0); @$pb.TagNumber(1) void clearId() => clearField(1); - @$pb.TagNumber(1) - $0.TypedKey ensureId() => $_ensure(0); @$pb.TagNumber(2) $0.TypedKey get author => $_getN(1); @@ -866,15 +862,15 @@ class Message extends $pb.GeneratedMessage { Message_ControlDelete ensureDelete() => $_ensure(5); @$pb.TagNumber(7) - Message_ControlClear get clear_7 => $_getN(6); + Message_ControlErase get erase => $_getN(6); @$pb.TagNumber(7) - set clear_7(Message_ControlClear v) { setField(7, v); } + set erase(Message_ControlErase v) { setField(7, v); } @$pb.TagNumber(7) - $core.bool hasClear_7() => $_has(6); + $core.bool hasErase() => $_has(6); @$pb.TagNumber(7) - void clearClear_7() => clearField(7); + void clearErase() => clearField(7); @$pb.TagNumber(7) - Message_ControlClear ensureClear_7() => $_ensure(6); + Message_ControlErase ensureErase() => $_ensure(6); @$pb.TagNumber(8) Message_ControlSettings get settings => $_getN(7); diff --git a/lib/proto/veilidchat.pbjson.dart b/lib/proto/veilidchat.pbjson.dart index 2aaecb2..1470054 100644 --- a/lib/proto/veilidchat.pbjson.dart +++ b/lib/proto/veilidchat.pbjson.dart @@ -159,20 +159,20 @@ final $typed_data.Uint8List chatSettingsDescriptor = $convert.base64Decode( const Message$json = { '1': 'Message', '2': [ - {'1': 'id', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'id'}, + {'1': 'id', '3': 1, '4': 1, '5': 12, '10': 'id'}, {'1': 'author', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'author'}, {'1': 'timestamp', '3': 3, '4': 1, '5': 4, '10': 'timestamp'}, {'1': 'text', '3': 4, '4': 1, '5': 11, '6': '.veilidchat.Message.Text', '9': 0, '10': 'text'}, {'1': 'secret', '3': 5, '4': 1, '5': 11, '6': '.veilidchat.Message.Secret', '9': 0, '10': 'secret'}, {'1': 'delete', '3': 6, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlDelete', '9': 0, '10': 'delete'}, - {'1': 'clear', '3': 7, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlClear', '9': 0, '10': 'clear'}, + {'1': 'erase', '3': 7, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlErase', '9': 0, '10': 'erase'}, {'1': 'settings', '3': 8, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlSettings', '9': 0, '10': 'settings'}, {'1': 'permissions', '3': 9, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlPermissions', '9': 0, '10': 'permissions'}, {'1': 'membership', '3': 10, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlMembership', '9': 0, '10': 'membership'}, {'1': 'moderation', '3': 11, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlModeration', '9': 0, '10': 'moderation'}, {'1': 'signature', '3': 12, '4': 1, '5': 11, '6': '.veilid.Signature', '10': 'signature'}, ], - '3': [Message_Text$json, Message_Secret$json, Message_ControlDelete$json, Message_ControlClear$json, Message_ControlSettings$json, Message_ControlPermissions$json, Message_ControlMembership$json, Message_ControlModeration$json], + '3': [Message_Text$json, Message_Secret$json, Message_ControlDelete$json, Message_ControlErase$json, Message_ControlSettings$json, Message_ControlPermissions$json, Message_ControlMembership$json, Message_ControlModeration$json], '8': [ {'1': 'kind'}, ], @@ -183,12 +183,16 @@ const Message_Text$json = { '1': 'Text', '2': [ {'1': 'text', '3': 1, '4': 1, '5': 9, '10': 'text'}, - {'1': 'topic', '3': 2, '4': 1, '5': 9, '10': 'topic'}, - {'1': 'reply_id', '3': 3, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'replyId'}, + {'1': 'topic', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'topic', '17': true}, + {'1': 'reply_id', '3': 3, '4': 1, '5': 12, '9': 1, '10': 'replyId', '17': true}, {'1': 'expiration', '3': 4, '4': 1, '5': 4, '10': 'expiration'}, - {'1': 'view_limit', '3': 5, '4': 1, '5': 4, '10': 'viewLimit'}, + {'1': 'view_limit', '3': 5, '4': 1, '5': 13, '10': 'viewLimit'}, {'1': 'attachments', '3': 6, '4': 3, '5': 11, '6': '.veilidchat.Attachment', '10': 'attachments'}, ], + '8': [ + {'1': '_topic'}, + {'1': '_reply_id'}, + ], }; @$core.Deprecated('Use messageDescriptor instead') @@ -209,8 +213,8 @@ const Message_ControlDelete$json = { }; @$core.Deprecated('Use messageDescriptor instead') -const Message_ControlClear$json = { - '1': 'ControlClear', +const Message_ControlErase$json = { + '1': 'ControlErase', '2': [ {'1': 'timestamp', '3': 1, '4': 1, '5': 4, '10': 'timestamp'}, ], @@ -251,32 +255,32 @@ const Message_ControlModeration$json = { /// Descriptor for `Message`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List messageDescriptor = $convert.base64Decode( - 'CgdNZXNzYWdlEiAKAmlkGAEgASgLMhAudmVpbGlkLlR5cGVkS2V5UgJpZBIoCgZhdXRob3IYAi' - 'ABKAsyEC52ZWlsaWQuVHlwZWRLZXlSBmF1dGhvchIcCgl0aW1lc3RhbXAYAyABKARSCXRpbWVz' - 'dGFtcBIuCgR0ZXh0GAQgASgLMhgudmVpbGlkY2hhdC5NZXNzYWdlLlRleHRIAFIEdGV4dBI0Cg' - 'ZzZWNyZXQYBSABKAsyGi52ZWlsaWRjaGF0Lk1lc3NhZ2UuU2VjcmV0SABSBnNlY3JldBI7CgZk' - 'ZWxldGUYBiABKAsyIS52ZWlsaWRjaGF0Lk1lc3NhZ2UuQ29udHJvbERlbGV0ZUgAUgZkZWxldG' - 'USOAoFY2xlYXIYByABKAsyIC52ZWlsaWRjaGF0Lk1lc3NhZ2UuQ29udHJvbENsZWFySABSBWNs' - 'ZWFyEkEKCHNldHRpbmdzGAggASgLMiMudmVpbGlkY2hhdC5NZXNzYWdlLkNvbnRyb2xTZXR0aW' - '5nc0gAUghzZXR0aW5ncxJKCgtwZXJtaXNzaW9ucxgJIAEoCzImLnZlaWxpZGNoYXQuTWVzc2Fn' - 'ZS5Db250cm9sUGVybWlzc2lvbnNIAFILcGVybWlzc2lvbnMSRwoKbWVtYmVyc2hpcBgKIAEoCz' - 'IlLnZlaWxpZGNoYXQuTWVzc2FnZS5Db250cm9sTWVtYmVyc2hpcEgAUgptZW1iZXJzaGlwEkcK' - 'Cm1vZGVyYXRpb24YCyABKAsyJS52ZWlsaWRjaGF0Lk1lc3NhZ2UuQ29udHJvbE1vZGVyYXRpb2' - '5IAFIKbW9kZXJhdGlvbhIvCglzaWduYXR1cmUYDCABKAsyES52ZWlsaWQuU2lnbmF0dXJlUglz' - 'aWduYXR1cmUa1gEKBFRleHQSEgoEdGV4dBgBIAEoCVIEdGV4dBIUCgV0b3BpYxgCIAEoCVIFdG' - '9waWMSKwoIcmVwbHlfaWQYAyABKAsyEC52ZWlsaWQuVHlwZWRLZXlSB3JlcGx5SWQSHgoKZXhw' - 'aXJhdGlvbhgEIAEoBFIKZXhwaXJhdGlvbhIdCgp2aWV3X2xpbWl0GAUgASgEUgl2aWV3TGltaX' - 'QSOAoLYXR0YWNobWVudHMYBiADKAsyFi52ZWlsaWRjaGF0LkF0dGFjaG1lbnRSC2F0dGFjaG1l' - 'bnRzGkgKBlNlY3JldBIeCgpjaXBoZXJ0ZXh0GAEgASgMUgpjaXBoZXJ0ZXh0Eh4KCmV4cGlyYX' - 'Rpb24YAiABKARSCmV4cGlyYXRpb24aMwoNQ29udHJvbERlbGV0ZRIiCgNpZHMYASADKAsyEC52' - 'ZWlsaWQuVHlwZWRLZXlSA2lkcxosCgxDb250cm9sQ2xlYXISHAoJdGltZXN0YW1wGAEgASgEUg' - 'l0aW1lc3RhbXAaRwoPQ29udHJvbFNldHRpbmdzEjQKCHNldHRpbmdzGAEgASgLMhgudmVpbGlk' - 'Y2hhdC5DaGF0U2V0dGluZ3NSCHNldHRpbmdzGk8KEkNvbnRyb2xQZXJtaXNzaW9ucxI5CgtwZX' - 'JtaXNzaW9ucxgBIAEoCzIXLnZlaWxpZGNoYXQuUGVybWlzc2lvbnNSC3Blcm1pc3Npb25zGksK' - 'EUNvbnRyb2xNZW1iZXJzaGlwEjYKCm1lbWJlcnNoaXAYASABKAsyFi52ZWlsaWRjaGF0Lk1lbW' - 'JlcnNoaXBSCm1lbWJlcnNoaXAafQoRQ29udHJvbE1vZGVyYXRpb24SMwoMYWNjZXB0ZWRfaWRz' - 'GAEgAygLMhAudmVpbGlkLlR5cGVkS2V5UgthY2NlcHRlZElkcxIzCgxyZWplY3RlZF9pZHMYAi' - 'ADKAsyEC52ZWlsaWQuVHlwZWRLZXlSC3JlamVjdGVkSWRzQgYKBGtpbmQ='); + 'CgdNZXNzYWdlEg4KAmlkGAEgASgMUgJpZBIoCgZhdXRob3IYAiABKAsyEC52ZWlsaWQuVHlwZW' + 'RLZXlSBmF1dGhvchIcCgl0aW1lc3RhbXAYAyABKARSCXRpbWVzdGFtcBIuCgR0ZXh0GAQgASgL' + 'MhgudmVpbGlkY2hhdC5NZXNzYWdlLlRleHRIAFIEdGV4dBI0CgZzZWNyZXQYBSABKAsyGi52ZW' + 'lsaWRjaGF0Lk1lc3NhZ2UuU2VjcmV0SABSBnNlY3JldBI7CgZkZWxldGUYBiABKAsyIS52ZWls' + 'aWRjaGF0Lk1lc3NhZ2UuQ29udHJvbERlbGV0ZUgAUgZkZWxldGUSOAoFZXJhc2UYByABKAsyIC' + '52ZWlsaWRjaGF0Lk1lc3NhZ2UuQ29udHJvbEVyYXNlSABSBWVyYXNlEkEKCHNldHRpbmdzGAgg' + 'ASgLMiMudmVpbGlkY2hhdC5NZXNzYWdlLkNvbnRyb2xTZXR0aW5nc0gAUghzZXR0aW5ncxJKCg' + 'twZXJtaXNzaW9ucxgJIAEoCzImLnZlaWxpZGNoYXQuTWVzc2FnZS5Db250cm9sUGVybWlzc2lv' + 'bnNIAFILcGVybWlzc2lvbnMSRwoKbWVtYmVyc2hpcBgKIAEoCzIlLnZlaWxpZGNoYXQuTWVzc2' + 'FnZS5Db250cm9sTWVtYmVyc2hpcEgAUgptZW1iZXJzaGlwEkcKCm1vZGVyYXRpb24YCyABKAsy' + 'JS52ZWlsaWRjaGF0Lk1lc3NhZ2UuQ29udHJvbE1vZGVyYXRpb25IAFIKbW9kZXJhdGlvbhIvCg' + 'lzaWduYXR1cmUYDCABKAsyES52ZWlsaWQuU2lnbmF0dXJlUglzaWduYXR1cmUa5QEKBFRleHQS' + 'EgoEdGV4dBgBIAEoCVIEdGV4dBIZCgV0b3BpYxgCIAEoCUgAUgV0b3BpY4gBARIeCghyZXBseV' + '9pZBgDIAEoDEgBUgdyZXBseUlkiAEBEh4KCmV4cGlyYXRpb24YBCABKARSCmV4cGlyYXRpb24S' + 'HQoKdmlld19saW1pdBgFIAEoDVIJdmlld0xpbWl0EjgKC2F0dGFjaG1lbnRzGAYgAygLMhYudm' + 'VpbGlkY2hhdC5BdHRhY2htZW50UgthdHRhY2htZW50c0IICgZfdG9waWNCCwoJX3JlcGx5X2lk' + 'GkgKBlNlY3JldBIeCgpjaXBoZXJ0ZXh0GAEgASgMUgpjaXBoZXJ0ZXh0Eh4KCmV4cGlyYXRpb2' + '4YAiABKARSCmV4cGlyYXRpb24aMwoNQ29udHJvbERlbGV0ZRIiCgNpZHMYASADKAsyEC52ZWls' + 'aWQuVHlwZWRLZXlSA2lkcxosCgxDb250cm9sRXJhc2USHAoJdGltZXN0YW1wGAEgASgEUgl0aW' + '1lc3RhbXAaRwoPQ29udHJvbFNldHRpbmdzEjQKCHNldHRpbmdzGAEgASgLMhgudmVpbGlkY2hh' + 'dC5DaGF0U2V0dGluZ3NSCHNldHRpbmdzGk8KEkNvbnRyb2xQZXJtaXNzaW9ucxI5CgtwZXJtaX' + 'NzaW9ucxgBIAEoCzIXLnZlaWxpZGNoYXQuUGVybWlzc2lvbnNSC3Blcm1pc3Npb25zGksKEUNv' + 'bnRyb2xNZW1iZXJzaGlwEjYKCm1lbWJlcnNoaXAYASABKAsyFi52ZWlsaWRjaGF0Lk1lbWJlcn' + 'NoaXBSCm1lbWJlcnNoaXAafQoRQ29udHJvbE1vZGVyYXRpb24SMwoMYWNjZXB0ZWRfaWRzGAEg' + 'AygLMhAudmVpbGlkLlR5cGVkS2V5UgthY2NlcHRlZElkcxIzCgxyZWplY3RlZF9pZHMYAiADKA' + 'syEC52ZWlsaWQuVHlwZWRLZXlSC3JlamVjdGVkSWRzQgYKBGtpbmQ='); @$core.Deprecated('Use reconciledMessageDescriptor instead') const ReconciledMessage$json = { diff --git a/lib/proto/veilidchat.proto b/lib/proto/veilidchat.proto index eb6d08a..943f79b 100644 --- a/lib/proto/veilidchat.proto +++ b/lib/proto/veilidchat.proto @@ -123,13 +123,13 @@ message Message { // Text of the message string text = 1; // Topic of the message / Content warning - string topic = 2; + optional string topic = 2; // Message id replied to - veilid.TypedKey reply_id = 3; + optional bytes reply_id = 3; // Message expiration timestamp uint64 expiration = 4; // Message view limit before deletion - uint64 view_limit = 5; + uint32 view_limit = 5; // Attachments on the message repeated Attachment attachments = 6; } @@ -148,9 +148,9 @@ message Message { message ControlDelete { repeated veilid.TypedKey ids = 1; } - // A 'clear' control message + // An 'erase' control message // Deletes a set of messages from before some timestamp - message ControlClear { + message ControlErase { // The latest timestamp to delete messages before // If this is zero then all messages are cleared uint64 timestamp = 1; @@ -181,10 +181,9 @@ message Message { ////////////////////////////////////////////////////////////////////////// - // Hash of previous message from the same author, - // including its previous hash. - // Also serves as a unique key for the message. - veilid.TypedKey id = 1; + // Unique id for this author stream + // Calculated from the hash of the previous message from this author + bytes id = 1; // Author of the message (identity public key) veilid.TypedKey author = 2; // Time the message was sent according to sender @@ -195,7 +194,7 @@ message Message { Text text = 4; Secret secret = 5; ControlDelete delete = 6; - ControlClear clear = 7; + ControlErase erase = 7; ControlSettings settings = 8; ControlPermissions permissions = 9; ControlMembership membership = 10; From e04fd7ee778d627f1b147668d1794d4e317da0c3 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 27 May 2024 20:34:54 -0400 Subject: [PATCH 115/270] table db change tracking --- .../cubits/single_contact_messages_cubit.dart | 1 + lib/chat/models/message_state.dart | 18 +- lib/chat/views/chat_component.dart | 4 +- lib/proto/veilidchat.proto | 2 +- .../lib/src/table_db_array.dart | 46 ++++- .../lib/src/table_db_array_cubit.dart | 185 ++++++++++++++++++ .../veilid_support/lib/veilid_support.dart | 1 + 7 files changed, 237 insertions(+), 20 deletions(-) create mode 100644 packages/veilid_support/lib/src/table_db_array_cubit.dart diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 20c8d22..cf644f9 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -420,6 +420,7 @@ class SingleContactMessagesCubit extends Cubit { void addTextMessage({required proto.Message_Text messageText}) { final message = proto.Message() + ..id = generateNextId() ..author = _activeAccountInfo.localAccount.identityMaster .identityPublicTypedKey() .toProto() diff --git a/lib/chat/models/message_state.dart b/lib/chat/models/message_state.dart index e14c9e8..993ebcb 100644 --- a/lib/chat/models/message_state.dart +++ b/lib/chat/models/message_state.dart @@ -39,19 +39,9 @@ class MessageState with _$MessageState { } extension MessageStateExt on MessageState { - String get uniqueId { - final author = content.author.toVeilid().toString(); - final id = base64UrlNoPadEncode(content.id); - return '$author|$id'; - } - - static (proto.TypedKey, Uint8List) splitUniqueId(String uniqueId) { - final parts = uniqueId.split('|'); - if (parts.length != 2) { - throw Exception('invalid unique id'); - } - final author = TypedKey.fromString(parts[0]).toProto(); - final id = base64UrlNoPadDecode(parts[1]); - return (author, id); + Uint8List get uniqueId { + final author = content.author.toVeilid().decode(); + final id = content.id; + return author..addAll(id); } } diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 2ca49a1..5aa70b7 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -126,7 +126,7 @@ class ChatComponent extends StatelessWidget { final textMessage = types.TextMessage( author: isLocal ? _localUser : _remoteUser, createdAt: (message.timestamp.value ~/ BigInt.from(1000)).toInt(), - id: message.uniqueId, + id: base64UrlNoPadEncode(message.uniqueId), text: contextText.text, showStatus: status != null, status: status); @@ -168,7 +168,7 @@ class ChatComponent extends StatelessWidget { void _handleSendPressed(types.PartialText message) { final text = message.text; final replyId = (message.repliedMessage != null) - ? MessageStateExt.splitUniqueId(message.repliedMessage!.id).$2 + ? base64UrlNoPadDecode(message.repliedMessage!.id) : null; Timestamp? expiration; int? viewLimit; diff --git a/lib/proto/veilidchat.proto b/lib/proto/veilidchat.proto index 943f79b..81c963c 100644 --- a/lib/proto/veilidchat.proto +++ b/lib/proto/veilidchat.proto @@ -124,7 +124,7 @@ message Message { string text = 1; // Topic of the message / Content warning optional string topic = 2; - // Message id replied to + // Message id replied to (author id + message id) optional bytes reply_id = 3; // Message expiration timestamp uint64 expiration = 4; diff --git a/packages/veilid_support/lib/src/table_db_array.dart b/packages/veilid_support/lib/src/table_db_array.dart index 51b15b8..9714770 100644 --- a/packages/veilid_support/lib/src/table_db_array.dart +++ b/packages/veilid_support/lib/src/table_db_array.dart @@ -4,9 +4,24 @@ import 'dart:typed_data'; import 'package:async_tools/async_tools.dart'; import 'package:charcode/charcode.dart'; +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; import '../veilid_support.dart'; +@immutable +class TableDBArrayUpdate extends Equatable { + const TableDBArrayUpdate( + {required this.headDelta, required this.tailDelta, required this.length}) + : assert(length >= 0, 'should never have negative length'); + final int headDelta; + final int tailDelta; + final int length; + + @override + List get props => [headDelta, tailDelta, length]; +} + class TableDBArray { TableDBArray({ required String table, @@ -63,8 +78,9 @@ class TableDBArray { await Veilid.instance.deleteTableDB(_table); } - Future> listen(void Function() onChanged) async => - _changeStream.stream.listen((_) => onChanged()); + Future> listen( + void Function(TableDBArrayUpdate) onChanged) async => + _changeStream.stream.listen(onChanged); //////////////////////////////////////////////////////////// // Public interface @@ -160,6 +176,7 @@ class TableDBArray { // Put the entry in the index final pos = _length; _length++; + _tailDelta++; await _setIndexEntry(pos, entry); } @@ -167,6 +184,7 @@ class TableDBArray { VeilidTableDBTransaction t, List values) async { var pos = _length; _length += values.length; + _tailDelta += values.length; for (final value in values) { // Allocate an entry to store the value final entry = await _allocateEntry(); @@ -318,11 +336,18 @@ class TableDBArray { final _oldLength = _length; final _oldNextFree = _nextFree; final _oldMaxEntry = _maxEntry; + final _oldHeadDelta = _headDelta; + final _oldTailDelta = _tailDelta; try { final out = await transactionScope(_tableDB, (t) async { final out = await closure(t); await _saveHead(t); await _flushDirtyChunks(t); + // Send change + _changeStream.add(TableDBArrayUpdate( + headDelta: _headDelta, tailDelta: _tailDelta, length: _length)); + _headDelta = 0; + _tailDelta = 0; return out; }); @@ -332,6 +357,8 @@ class TableDBArray { _length = _oldLength; _nextFree = _oldNextFree; _maxEntry = _oldMaxEntry; + _headDelta = _oldHeadDelta; + _tailDelta = _oldTailDelta; // invalidate caches because they could have been written to _chunkCache.clear(); _dirtyChunks.clear(); @@ -428,6 +455,10 @@ class TableDBArray { // Then add to length _length += length; + if (start == 0) { + _headDelta += length; + } + _tailDelta += length; } Future _removeIndexEntry(int pos) async => _removeIndexEntries(pos, 1); @@ -485,6 +516,10 @@ class TableDBArray { // Then truncate _length -= length; + if (start == 0) { + _headDelta -= length; + } + _tailDelta -= length; } Future _loadIndexChunk(int chunkNumber) async { @@ -578,6 +613,10 @@ class TableDBArray { final WaitSet _initWait = WaitSet(); final Mutex _mutex = Mutex(); + // Change tracking + int _headDelta = 0; + int _tailDelta = 0; + // Head state int _length = 0; int _nextFree = 0; @@ -587,5 +626,6 @@ class TableDBArray { final Map _dirtyChunks = {}; static const int _chunkCacheLength = 3; - final StreamController _changeStream = StreamController.broadcast(); + final StreamController _changeStream = + StreamController.broadcast(); } diff --git a/packages/veilid_support/lib/src/table_db_array_cubit.dart b/packages/veilid_support/lib/src/table_db_array_cubit.dart new file mode 100644 index 0000000..1cebab5 --- /dev/null +++ b/packages/veilid_support/lib/src/table_db_array_cubit.dart @@ -0,0 +1,185 @@ +import 'dart:async'; + +import 'package:async_tools/async_tools.dart'; +import 'package:bloc/bloc.dart'; +import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; +import 'package:equatable/equatable.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:meta/meta.dart'; + +import '../../../veilid_support.dart'; + +@immutable +class TableDBArrayStateData extends Equatable { + const TableDBArrayStateData( + {required this.elements, + required this.tail, + required this.count, + required this.follow}); + // The view of the elements in the dhtlog + // Span is from [tail-length, tail) + final IList elements; + // One past the end of the last element + final int tail; + // The total number of elements to try to keep in 'elements' + final int count; + // If we should have the tail following the array + final bool follow; + + @override + List get props => [elements, tail, count, follow]; +} + +typedef TableDBArrayState = AsyncValue>; +typedef TableDBArrayBusyState = BlocBusyState>; + +class TableDBArrayCubit extends Cubit> + with BlocBusyWrapper> { + TableDBArrayCubit({ + required Future Function() open, + required T Function(List data) decodeElement, + }) : _decodeElement = decodeElement, + super(const BlocBusyState(AsyncValue.loading())) { + _initWait.add(() async { + // Open table db array + _array = await open(); + _wantsCloseArray = true; + + // Make initial state update + await _refreshNoWait(); + _subscription = await _array.listen(_update); + }); + } + + // Set the tail position of the array for pagination. + // If tail is 0, the end of the array is used. + // If tail is negative, the position is subtracted from the current array + // length. + // If tail is positive, the position is absolute from the head of the array + // If follow is enabled, the tail offset will update when the array changes + Future setWindow( + {int? tail, int? count, bool? follow, bool forceRefresh = false}) async { + await _initWait(); + if (tail != null) { + _tail = tail; + } + if (count != null) { + _count = count; + } + if (follow != null) { + _follow = follow; + } + await _refreshNoWait(forceRefresh: forceRefresh); + } + + Future refresh({bool forceRefresh = false}) async { + await _initWait(); + await _refreshNoWait(forceRefresh: forceRefresh); + } + + Future _refreshNoWait({bool forceRefresh = false}) async => + busy((emit) async => _refreshInner(emit, forceRefresh: forceRefresh)); + + Future _refreshInner( + void Function(AsyncValue>) emit, + {bool forceRefresh = false}) async { + final avElements = await _loadElements(_tail, _count); + final err = avElements.asError; + if (err != null) { + emit(AsyncValue.error(err.error, err.stackTrace)); + return; + } + final loading = avElements.asLoading; + if (loading != null) { + emit(const AsyncValue.loading()); + return; + } + final elements = avElements.asData!.value; + emit(AsyncValue.data(TableDBArrayStateData( + elements: elements, tail: _tail, count: _count, follow: _follow))); + } + + Future>> _loadElements( + int tail, + int count, + ) async { + try { + final length = _array.length; + final end = ((tail - 1) % length) + 1; + final start = (count < end) ? end - count : 0; + final allItems = + (await _array.getRange(start, end)).map(_decodeElement).toIList(); + return AsyncValue.data(allItems); + } on Exception catch (e, st) { + return AsyncValue.error(e, st); + } + } + + void _update(TableDBArrayUpdate upd) { + // Run at most one background update process + // 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. + + // Accumulate head and tail deltas + _headDelta += upd.headDelta; + _tailDelta += upd.tailDelta; + + _sspUpdate.busyUpdate>(busy, (emit) async { + // apply follow + if (_follow) { + if (_tail <= 0) { + // Negative tail is already following tail changes + } else { + // Positive tail is measured from the head, so apply deltas + _tail = (_tail + _tailDelta - _headDelta) % upd.length; + } + } else { + if (_tail <= 0) { + // Negative tail is following tail changes so apply deltas + var posTail = _tail + upd.length; + posTail = (posTail + _tailDelta - _headDelta) % upd.length; + _tail = posTail - upd.length; + } else { + // Positive tail is measured from head so not following tail + } + } + _headDelta = 0; + _tailDelta = 0; + + await _refreshInner(emit); + }); + } + + @override + Future close() async { + await _initWait(); + await _subscription?.cancel(); + _subscription = null; + if (_wantsCloseArray) { + await _array.close(); + } + await super.close(); + } + + Future operate(Future Function(TableDBArray) closure) async { + await _initWait(); + return closure(_array); + } + + final WaitSet _initWait = WaitSet(); + late final TableDBArray _array; + final T Function(List data) _decodeElement; + StreamSubscription? _subscription; + bool _wantsCloseArray = false; + final _sspUpdate = SingleStatelessProcessor(); + + // Accumulated deltas since last update + var _headDelta = 0; + var _tailDelta = 0; + + // Cubit window into the TableDBArray + var _tail = 0; + var _count = DHTShortArray.maxElements; + var _follow = true; +} diff --git a/packages/veilid_support/lib/veilid_support.dart b/packages/veilid_support/lib/veilid_support.dart index 1f17da2..42aa839 100644 --- a/packages/veilid_support/lib/veilid_support.dart +++ b/packages/veilid_support/lib/veilid_support.dart @@ -15,5 +15,6 @@ export 'src/persistent_queue.dart'; export 'src/protobuf_tools.dart'; export 'src/table_db.dart'; export 'src/table_db_array.dart'; +export 'src/table_db_array_cubit.dart'; export 'src/veilid_crypto.dart'; export 'src/veilid_log.dart' hide veilidLoggy; From 8a5af51ec7714d27a5480313a6591c292888ed28 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 27 May 2024 22:58:37 -0400 Subject: [PATCH 116/270] crypto work --- .../models/active_account_info.dart | 4 ++-- .../cubits/single_contact_messages_cubit.dart | 16 ++++++-------- .../cubits/contact_invitation_list_cubit.dart | 6 ++--- .../cubits/contact_request_inbox_cubit.dart | 8 +++---- .../home_account_ready_chat.dart | 6 ++--- .../src/dht_record/dht_record_pool.dart | 16 ++++++++++---- packages/veilid_support/lib/src/identity.dart | 11 +++++----- .../veilid_support/lib/src/veilid_crypto.dart | 22 ++++++++++++++----- 8 files changed, 53 insertions(+), 36 deletions(-) diff --git a/lib/account_manager/models/active_account_info.dart b/lib/account_manager/models/active_account_info.dart index 2997434..e4a5beb 100644 --- a/lib/account_manager/models/active_account_info.dart +++ b/lib/account_manager/models/active_account_info.dart @@ -33,8 +33,8 @@ class ActiveAccountInfo { identitySecret.value, utf8.encode('VeilidChat Conversation')); - final messagesCrypto = - await VeilidCryptoPrivate.fromSecret(identitySecret.kind, sharedSecret); + final messagesCrypto = await VeilidCryptoPrivate.fromSharedSecret( + identitySecret.kind, sharedSecret); return messagesCrypto; } diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index cf644f9..dc0e2d4 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -146,15 +146,13 @@ class SingleContactMessagesCubit extends Cubit { // Open reconciled chat record key Future _initReconciledMessagesCubit() async { - final accountRecordKey = - _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + final tableName = _localConversationRecordKey.toString(); - _reconciledMessagesCubit = DHTLogCubit( - open: () async => DHTLog.openOwned(_reconciledChatRecord, - debugName: - 'SingleContactMessagesCubit::_initReconciledMessagesCubit::' - 'ReconciledMessages', - parent: accountRecordKey), + xxx whats the right encryption for reconciled messages cubit? + + final crypto = VeilidCryptoPrivate.fromTypedKey(kind, secretKey); + _reconciledMessagesCubit = TableDBArrayCubit( + open: () async => TableDBArray.make(table: tableName, crypto: crypto), decodeElement: proto.Message.fromBuffer); _reconciledSubscription = _reconciledMessagesCubit!.stream.listen(_updateReconciledMessagesState); @@ -461,7 +459,7 @@ class SingleContactMessagesCubit extends Cubit { DHTLogCubit? _sentMessagesCubit; DHTLogCubit? _rcvdMessagesCubit; - DHTLogCubit? _reconciledMessagesCubit; + TableDBArrayCubit? _reconciledMessagesCubit; late final PersistentQueue _unreconciledMessagesQueue; late final PersistentQueue _sendingMessagesQueue; diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index b76eaee..da1f6e3 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -129,9 +129,9 @@ class ContactInvitationListCubit await contactRequestInbox.eventualWriteBytes(Uint8List(0), subkey: 1, writer: contactRequestWriter, - crypto: await VeilidCryptoPrivate.fromTypedKeyPair( - TypedKeyPair.fromKeyPair( - contactRequestInbox.key.kind, contactRequestWriter))); + crypto: await DHTRecordPool.privateCryptoFromTypedSecret(TypedKey( + kind: contactRequestInbox.key.kind, + value: contactRequestWriter.secret))); // Create ContactInvitation and SignedContactInvitation final cinv = proto.ContactInvitation() diff --git a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart index a4d0b8a..214d08b 100644 --- a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart +++ b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart @@ -28,16 +28,16 @@ class ContactRequestInboxCubit 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 writerTypedSecret = + TypedKey(kind: recordKey.kind, value: writerSecret); return pool.openRecordRead(recordKey, debugName: 'ContactRequestInboxCubit::_open::' 'ContactRequestInbox', - crypto: await VeilidCryptoPrivate.fromTypedKeyPair(writer), + crypto: + await DHTRecordPool.privateCryptoFromTypedSecret(writerTypedSecret), parent: accountRecordKey, defaultSubkey: 1); } 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 fb0e7b4..087bb34 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 @@ -28,13 +28,13 @@ class HomeAccountReadyChatState extends State { } Widget buildChatComponent(BuildContext context) { - final activeChatRemoteConversationKey = + final activeChatLocalConversationKey = context.watch().state; - if (activeChatRemoteConversationKey == null) { + if (activeChatLocalConversationKey == null) { return const EmptyChatWidget(); } return ChatComponent.builder( - localConversationRecordKey: activeChatRemoteConversationKey); + localConversationRecordKey: activeChatLocalConversationKey); } @override 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 440698a..8b65d41 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 @@ -27,6 +27,9 @@ const int watchRenewalDenominator = 5; // Maximum number of concurrent DHT operations to perform on the network const int maxDHTConcurrency = 8; +// DHT crypto domain +const String cryptoDomainDHT = 'dht'; + typedef DHTRecordPoolLogger = void Function(String message); /// Record pool that managed DHTRecords and allows for tagged deletion @@ -547,9 +550,9 @@ class DHTRecordPool with TableDBBackedJson { writer: writer ?? openedRecordInfo.shared.recordDescriptor.ownerKeyPair(), crypto: crypto ?? - await VeilidCryptoPrivate.fromTypedKeyPair(openedRecordInfo + await privateCryptoFromTypedSecret(openedRecordInfo .shared.recordDescriptor - .ownerTypedKeyPair()!)); + .ownerTypedSecret()!)); openedRecordInfo.records.add(rec); @@ -612,8 +615,8 @@ class DHTRecordPool with TableDBBackedJson { writer: writer, sharedDHTRecordData: openedRecordInfo.shared, crypto: crypto ?? - await VeilidCryptoPrivate.fromTypedKeyPair( - TypedKeyPair.fromKeyPair(recordKey.kind, writer))); + await privateCryptoFromTypedSecret( + TypedKey(kind: recordKey.kind, value: writer.secret))); openedRecordInfo.records.add(rec); @@ -663,6 +666,11 @@ class DHTRecordPool with TableDBBackedJson { } } + /// Generate default VeilidCrypto for a writer + static Future privateCryptoFromTypedSecret( + TypedKey typedSecret) async => + VeilidCryptoPrivate.fromTypedKey(typedSecret, cryptoDomainDHT); + /// Handle the DHT record updates coming from Veilid void processRemoteValueChange(VeilidUpdateValueChange updateValueChange) { if (updateValueChange.subkeys.isNotEmpty) { diff --git a/packages/veilid_support/lib/src/identity.dart b/packages/veilid_support/lib/src/identity.dart index 400d68b..4666487 100644 --- a/packages/veilid_support/lib/src/identity.dart +++ b/packages/veilid_support/lib/src/identity.dart @@ -125,13 +125,14 @@ extension IdentityMasterExtension on IdentityMaster { } Future> readAccountsFromIdentity( - {required SharedSecret identitySecret, - required String accountKey}) async { + {required SecretKey identitySecret, required String accountKey}) async { // Read the identity key to get the account keys final pool = DHTRecordPool.instance; - final identityRecordCrypto = await VeilidCryptoPrivate.fromSecret( - identityRecordKey.kind, identitySecret); + final identityRecordCrypto = + await DHTRecordPool.privateCryptoFromTypedSecret( + TypedKey(kind: identityRecordKey.kind, value: identitySecret), + ); late final List accountRecordInfo; await (await pool.openRecordRead(identityRecordKey, @@ -157,7 +158,7 @@ extension IdentityMasterExtension on IdentityMaster { /// Creates a new Account associated with master identity and store it in the /// identity key. Future addAccountToIdentity({ - required SharedSecret identitySecret, + required SecretKey identitySecret, required String accountKey, required Future Function(TypedKey parent) createAccountCallback, int maxAccounts = 1, diff --git a/packages/veilid_support/lib/src/veilid_crypto.dart b/packages/veilid_support/lib/src/veilid_crypto.dart index 6965089..75087fb 100644 --- a/packages/veilid_support/lib/src/veilid_crypto.dart +++ b/packages/veilid_support/lib/src/veilid_crypto.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:typed_data'; import '../../../veilid_support.dart'; @@ -16,15 +17,24 @@ class VeilidCryptoPrivate implements VeilidCrypto { final VeilidCryptoSystem _cryptoSystem; final SharedSecret _secretKey; - static Future fromTypedKeyPair( - TypedKeyPair typedKeyPair) async { - final cryptoSystem = - await Veilid.instance.getCryptoSystem(typedKeyPair.kind); - final secretKey = typedKeyPair.secret; + static Future fromTypedKey( + TypedKey typedKey, String domain) async { + final cryptoSystem = await Veilid.instance.getCryptoSystem(typedKey.kind); + final keyMaterial = Uint8List(0) + ..addAll(typedKey.value.decode()) + ..addAll(utf8.encode(domain)); + final secretKey = await cryptoSystem.generateHash(keyMaterial); return VeilidCryptoPrivate._(cryptoSystem, secretKey); } - static Future fromSecret( + static Future fromTypedKeyPair( + TypedKeyPair typedKeyPair, String domain) async { + final typedSecret = + TypedKey(kind: typedKeyPair.kind, value: typedKeyPair.secret); + return fromTypedKey(typedSecret, domain); + } + + static Future fromSharedSecret( CryptoKind kind, SharedSecret secretKey) async { final cryptoSystem = await Veilid.instance.getCryptoSystem(kind); return VeilidCryptoPrivate._(cryptoSystem, secretKey); From 37f6ca19f7da911c9e19dc9c34ac284450f0938e Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 28 May 2024 22:01:50 -0400 Subject: [PATCH 117/270] checkpoint --- .../cubits/single_contact_messages_cubit.dart | 108 ++++++++++++++---- lib/chat/views/chat_component.dart | 2 +- lib/chat_list/cubits/chat_list_cubit.dart | 9 +- .../cubits/contact_invitation_list_cubit.dart | 6 +- lib/contacts/cubits/contact_list_cubit.dart | 6 +- lib/proto/veilidchat.pb.dart | 48 +++++++- lib/proto/veilidchat.pbjson.dart | 34 +++--- lib/proto/veilidchat.proto | 11 +- .../integration_test/test_dht_log.dart | 20 ++-- .../test_dht_short_array.dart | 18 +-- .../lib/dht_support/proto/dht.proto | 15 ++- .../lib/dht_support/src/dht_log/dht_log.dart | 2 +- .../src/dht_log/dht_log_cubit.dart | 7 +- .../dht_support/src/dht_log/dht_log_read.dart | 12 +- .../src/dht_log/dht_log_spine.dart | 9 +- .../src/dht_log/dht_log_write.dart | 50 +++++++- .../dht_short_array_cubit.dart | 13 +-- .../dht_short_array/dht_short_array_read.dart | 8 +- .../dht_short_array_write.dart | 15 ++- .../{dht_append.dart => dht_add.dart} | 12 +- .../src/interfaces/dht_insert_remove.dart | 23 ++-- .../src/interfaces/dht_random_read.dart | 28 ++--- .../src/interfaces/dht_random_write.dart | 5 + .../src/interfaces/interfaces.dart | 2 +- .../lib/src/table_db_array.dart | 38 +++++- .../veilid_support/lib/src/veilid_crypto.dart | 22 ++-- 26 files changed, 357 insertions(+), 166 deletions(-) rename packages/veilid_support/lib/dht_support/src/interfaces/{dht_append.dart => dht_add.dart} (80%) diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index dc0e2d4..7ab3401 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -1,4 +1,6 @@ import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; import 'package:async_tools/async_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; @@ -15,7 +17,6 @@ class RenderStateElement { {required this.message, required this.isLocal, this.reconciled = false, - this.reconciledOffline = false, this.sent = false, this.sentOffline = false}); @@ -27,7 +28,7 @@ class RenderStateElement { if (sent && !sentOffline) { return MessageSendState.delivered; } - if (reconciled && !reconciledOffline) { + if (reconciled) { return MessageSendState.sent; } return MessageSendState.sending; @@ -36,7 +37,6 @@ class RenderStateElement { proto.Message message; bool isLocal; bool reconciled; - bool reconciledOffline; bool sent; bool sentOffline; } @@ -96,7 +96,7 @@ class SingleContactMessagesCubit extends Cubit { ); // Make crypto - await _initMessagesCrypto(); + await _initCrypto(); // Reconciled messages key await _initReconciledMessagesCubit(); @@ -109,9 +109,13 @@ class SingleContactMessagesCubit extends Cubit { } // Make crypto - Future _initMessagesCrypto() async { + Future _initCrypto() async { _messagesCrypto = await _activeAccountInfo .makeConversationCrypto(_remoteIdentityPublicKey); + _localMessagesCryptoSystem = + await Veilid.instance.getCryptoSystem(_localMessagesRecordKey.kind); + _identityCryptoSystem = + await _activeAccountInfo.localAccount.identityMaster.identityCrypto; } // Open local messages key @@ -144,13 +148,16 @@ class SingleContactMessagesCubit extends Cubit { _updateRcvdMessagesState(_rcvdMessagesCubit!.state); } + Future _makeLocalMessagesCrypto() async => + VeilidCryptoPrivate.fromTypedKey( + _activeAccountInfo.userLogin.identitySecret, 'tabledb'); + // Open reconciled chat record key Future _initReconciledMessagesCubit() async { final tableName = _localConversationRecordKey.toString(); - xxx whats the right encryption for reconciled messages cubit? + final crypto = await _makeLocalMessagesCrypto(); - final crypto = VeilidCryptoPrivate.fromTypedKey(kind, secretKey); _reconciledMessagesCubit = TableDBArrayCubit( open: () async => TableDBArray.make(table: tableName, crypto: crypto), decodeElement: proto.Message.fromBuffer); @@ -183,8 +190,8 @@ class SingleContactMessagesCubit extends Cubit { if (sentMessages == null) { return; } - // Don't reconcile, the sending machine will have already added - // to the reconciliation queue on that machine + + await _reconcileMessages(sentMessages, _sentMessagesCubit); // Update the view _renderState(); @@ -197,16 +204,18 @@ class SingleContactMessagesCubit extends Cubit { return; } + await _reconcileMessages(rcvdMessages, _rcvdMessagesCubit); + singleFuture(_rcvdMessagesCubit!, () async { // Get the timestamp of our most recent reconciled message final lastReconciledMessageTs = - await _reconciledMessagesCubit!.operate((r) async { - final len = r.length; + await _reconciledMessagesCubit!.operate((arr) async { + final len = arr.length; if (len == 0) { return null; } else { final lastMessage = - await r.getItemProtobuf(proto.Message.fromBuffer, len - 1); + await arr.getProtobuf(proto.Message.fromBuffer, len - 1); if (lastMessage == null) { throw StateError('should have gotten last message'); } @@ -232,11 +241,9 @@ class SingleContactMessagesCubit extends Cubit { }); } - // Called when the reconciled messages list gets a change - // This can happen when multiple clients for the same identity are - // reading and reconciling the same remote chat + // Called when the reconciled messages window gets a change void _updateReconciledMessagesState( - DHTLogBusyState avmessages) { + TableDBArrayBusyState avmessages) { // Update the view _renderState(); } @@ -252,10 +259,62 @@ class SingleContactMessagesCubit extends Cubit { // }); } + Future _hashSignature(proto.Signature signature) async => + (await _localMessagesCryptoSystem + .generateHash(signature.toVeilid().decode())) + .decode(); + + Future _signMessage(proto.Message message) async { + // Generate data to sign + final data = Uint8List.fromList(utf8.encode(message.writeToJson())); + + // Sign with our identity + final signature = await _identityCryptoSystem.sign( + _activeAccountInfo.localAccount.identityMaster.identityPublicKey, + _activeAccountInfo.userLogin.identitySecret.value, + data); + + // Add to the message + message.signature = signature.toProto(); + } + + Future _processMessageToSend( + proto.Message message, proto.Message? previousMessage) async { + // Get the previous message if we don't have one + previousMessage ??= await _sentMessagesCubit!.operate((r) async => + r.length == 0 + ? null + : await r.getProtobuf(proto.Message.fromBuffer, r.length - 1)); + + if (previousMessage == null) { + // If there's no last sent message, + // we start at a hash of the identity public key + message.id = (await _localMessagesCryptoSystem.generateHash( + _activeAccountInfo.localAccount.identityMaster.identityPublicKey + .decode())) + .decode(); + } else { + // If there is a last message, we generate the hash + // of the last message's signature and use it as our next id + message.id = await _hashSignature(previousMessage.signature); + } + + // Now sign it + await _signMessage(message); + } + // Async process to send messages in the background Future _processSendingMessages(IList messages) async { + // Go through and assign ids to all the messages in order + proto.Message? previousMessage; + final processedMessages = messages.toList(); + for (final message in processedMessages) { + await _processMessageToSend(message, previousMessage); + previousMessage = message; + } + await _sentMessagesCubit!.operateAppendEventual((writer) => - writer.tryAddItems(messages.map((m) => m.writeToBuffer()).toList())); + writer.tryAddAll(messages.map((m) => m.writeToBuffer()).toList())); } Future _reconcileMessagesInner( @@ -345,9 +404,8 @@ class SingleContactMessagesCubit extends Cubit { keyMapper: (x) => x.value.timestamp, values: sentMessages.elements, ); - final reconciledMessagesMap = - IMap>.fromValues( - keyMapper: (x) => x.value.timestamp, + final reconciledMessagesMap = IMap.fromValues( + keyMapper: (x) => x.timestamp, values: reconciledMessages.elements, ); final sendingMessagesMap = IMap.fromValues( @@ -416,7 +474,7 @@ class SingleContactMessagesCubit extends Cubit { emit(AsyncValue.data(renderedState)); } - void addTextMessage({required proto.Message_Text messageText}) { + void sendTextMessage({required proto.Message_Text messageText}) { final message = proto.Message() ..id = generateNextId() ..author = _activeAccountInfo.localAccount.identityMaster @@ -425,7 +483,6 @@ class SingleContactMessagesCubit extends Cubit { ..timestamp = Veilid.instance.now().toInt64() ..text = messageText; - _unreconciledMessagesQueue.addSync(message); _sendingMessagesQueue.addSync(message); // Update the view @@ -456,15 +513,18 @@ class SingleContactMessagesCubit extends Cubit { final TypedKey _remoteMessagesRecordKey; late final VeilidCrypto _messagesCrypto; + late final VeilidCryptoSystem _localMessagesCryptoSystem; + late final VeilidCryptoSystem _identityCryptoSystem; DHTLogCubit? _sentMessagesCubit; DHTLogCubit? _rcvdMessagesCubit; TableDBArrayCubit? _reconciledMessagesCubit; - late final PersistentQueue _unreconciledMessagesQueue; + late final PersistentQueue _unreconciledMessagesQueue; xxx can we eliminate this? and make rcvd messages cubit listener work like sent? late final PersistentQueue _sendingMessagesQueue; StreamSubscription>? _sentSubscription; StreamSubscription>? _rcvdSubscription; - StreamSubscription>? _reconciledSubscription; + StreamSubscription>? + _reconciledSubscription; } diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 5aa70b7..06d4312 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -162,7 +162,7 @@ class ChatComponent extends StatelessWidget { ..viewLimit = viewLimit ?? 0; protoMessageText.attachments.addAll(attachments); - _messagesCubit.addTextMessage(messageText: protoMessageText); + _messagesCubit.sendTextMessage(messageText: protoMessageText); } void _handleSendPressed(types.PartialText message) { diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index ff1d5d0..2ab2993 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -65,7 +65,7 @@ class ChatListCubit extends DHTShortArrayCubit await operateWrite((writer) async { // See if we have added this chat already for (var i = 0; i < writer.length; i++) { - final cbuf = await writer.getItem(i); + final cbuf = await writer.get(i); if (cbuf == null) { throw Exception('Failed to get chat'); } @@ -84,7 +84,7 @@ class ChatListCubit extends DHTShortArrayCubit ..remoteConversationRecordKey = remoteConversationRecordKey.toProto(); // Add chat - final added = await writer.tryAddItem(chat.writeToBuffer()); + final added = await writer.tryAdd(chat.writeToBuffer()); if (!added) { throw Exception('Failed to add chat'); } @@ -106,15 +106,14 @@ class ChatListCubit extends DHTShortArrayCubit activeChatCubit.setActiveChat(null); } for (var i = 0; i < writer.length; i++) { - final c = - await writer.getItemProtobuf(proto.Chat.fromBuffer, i); + final c = await writer.getProtobuf(proto.Chat.fromBuffer, i); if (c == null) { throw Exception('Failed to get chat'); } if (c.localConversationRecordKey == localConversationRecordKeyProto) { // Found the right chat - await writer.removeItem(i); + await writer.remove(i); return c; } } diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index da1f6e3..640d416 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -159,7 +159,7 @@ class ContactInvitationListCubit // Add ContactInvitationRecord to account's list // if this fails, don't keep retrying, user can try again later await operateWrite((writer) async { - if (await writer.tryAddItem(cinvrec.writeToBuffer()) == false) { + if (await writer.tryAdd(cinvrec.writeToBuffer()) == false) { throw Exception('Failed to add contact invitation record'); } }); @@ -179,14 +179,14 @@ class ContactInvitationListCubit // Remove ContactInvitationRecord from account's list final deletedItem = await operateWrite((writer) async { for (var i = 0; i < writer.length; i++) { - final item = await writer.getItemProtobuf( + final item = await writer.getProtobuf( proto.ContactInvitationRecord.fromBuffer, i); if (item == null) { throw Exception('Failed to get contact invitation record'); } if (item.contactRequestInbox.recordKey.toVeilid() == contactRequestInboxRecordKey) { - await writer.removeItem(i); + await writer.remove(i); return item; } } diff --git a/lib/contacts/cubits/contact_list_cubit.dart b/lib/contacts/cubits/contact_list_cubit.dart index 2eb8a08..71669fc 100644 --- a/lib/contacts/cubits/contact_list_cubit.dart +++ b/lib/contacts/cubits/contact_list_cubit.dart @@ -56,7 +56,7 @@ class ContactListCubit extends DHTShortArrayCubit { // Add Contact to account's list // if this fails, don't keep retrying, user can try again later await operateWrite((writer) async { - if (!await writer.tryAddItem(contact.writeToBuffer())) { + if (!await writer.tryAdd(contact.writeToBuffer())) { throw Exception('Failed to add contact record'); } }); @@ -72,13 +72,13 @@ class ContactListCubit extends DHTShortArrayCubit { // Remove Contact from account's list final deletedItem = await operateWrite((writer) async { for (var i = 0; i < writer.length; i++) { - final item = await writer.getItemProtobuf(proto.Contact.fromBuffer, i); + final item = await writer.getProtobuf(proto.Contact.fromBuffer, i); if (item == null) { throw Exception('Failed to get contact'); } if (item.localConversationRecordKey == contact.localConversationRecordKey) { - await writer.removeItem(i); + await writer.remove(i); return item; } } diff --git a/lib/proto/veilidchat.pb.dart b/lib/proto/veilidchat.pb.dart index 3544f00..7770d73 100644 --- a/lib/proto/veilidchat.pb.dart +++ b/lib/proto/veilidchat.pb.dart @@ -486,7 +486,7 @@ class Message_ControlDelete extends $pb.GeneratedMessage { factory Message_ControlDelete.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.ControlDelete', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..pc<$0.TypedKey>(1, _omitFieldNames ? '' : 'ids', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create) + ..p<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'ids', $pb.PbFieldType.PY) ..hasRequiredFields = false ; @@ -512,7 +512,7 @@ class Message_ControlDelete extends $pb.GeneratedMessage { static Message_ControlDelete? _defaultInstance; @$pb.TagNumber(1) - $core.List<$0.TypedKey> get ids => $_getList(0); + $core.List<$core.List<$core.int>> get ids => $_getList(0); } class Message_ControlErase extends $pb.GeneratedMessage { @@ -696,8 +696,8 @@ class Message_ControlModeration extends $pb.GeneratedMessage { factory Message_ControlModeration.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.ControlModeration', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..pc<$0.TypedKey>(1, _omitFieldNames ? '' : 'acceptedIds', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create) - ..pc<$0.TypedKey>(2, _omitFieldNames ? '' : 'rejectedIds', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create) + ..p<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'acceptedIds', $pb.PbFieldType.PY) + ..p<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'rejectedIds', $pb.PbFieldType.PY) ..hasRequiredFields = false ; @@ -723,10 +723,46 @@ class Message_ControlModeration extends $pb.GeneratedMessage { static Message_ControlModeration? _defaultInstance; @$pb.TagNumber(1) - $core.List<$0.TypedKey> get acceptedIds => $_getList(0); + $core.List<$core.List<$core.int>> get acceptedIds => $_getList(0); @$pb.TagNumber(2) - $core.List<$0.TypedKey> get rejectedIds => $_getList(1); + $core.List<$core.List<$core.int>> get rejectedIds => $_getList(1); +} + +class Message_ControlReadReceipt extends $pb.GeneratedMessage { + factory Message_ControlReadReceipt() => create(); + Message_ControlReadReceipt._() : super(); + factory Message_ControlReadReceipt.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Message_ControlReadReceipt.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message.ControlReadReceipt', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..p<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'readIds', $pb.PbFieldType.PY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Message_ControlReadReceipt clone() => Message_ControlReadReceipt()..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_ControlReadReceipt copyWith(void Function(Message_ControlReadReceipt) updates) => super.copyWith((message) => updates(message as Message_ControlReadReceipt)) as Message_ControlReadReceipt; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Message_ControlReadReceipt create() => Message_ControlReadReceipt._(); + Message_ControlReadReceipt createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Message_ControlReadReceipt getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Message_ControlReadReceipt? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.List<$core.int>> get readIds => $_getList(0); } enum Message_Kind { diff --git a/lib/proto/veilidchat.pbjson.dart b/lib/proto/veilidchat.pbjson.dart index 1470054..56ebbe6 100644 --- a/lib/proto/veilidchat.pbjson.dart +++ b/lib/proto/veilidchat.pbjson.dart @@ -172,7 +172,7 @@ const Message$json = { {'1': 'moderation', '3': 11, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlModeration', '9': 0, '10': 'moderation'}, {'1': 'signature', '3': 12, '4': 1, '5': 11, '6': '.veilid.Signature', '10': 'signature'}, ], - '3': [Message_Text$json, Message_Secret$json, Message_ControlDelete$json, Message_ControlErase$json, Message_ControlSettings$json, Message_ControlPermissions$json, Message_ControlMembership$json, Message_ControlModeration$json], + '3': [Message_Text$json, Message_Secret$json, Message_ControlDelete$json, Message_ControlErase$json, Message_ControlSettings$json, Message_ControlPermissions$json, Message_ControlMembership$json, Message_ControlModeration$json, Message_ControlReadReceipt$json], '8': [ {'1': 'kind'}, ], @@ -208,7 +208,7 @@ const Message_Secret$json = { const Message_ControlDelete$json = { '1': 'ControlDelete', '2': [ - {'1': 'ids', '3': 1, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'ids'}, + {'1': 'ids', '3': 1, '4': 3, '5': 12, '10': 'ids'}, ], }; @@ -248,8 +248,16 @@ const Message_ControlMembership$json = { const Message_ControlModeration$json = { '1': 'ControlModeration', '2': [ - {'1': 'accepted_ids', '3': 1, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'acceptedIds'}, - {'1': 'rejected_ids', '3': 2, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'rejectedIds'}, + {'1': 'accepted_ids', '3': 1, '4': 3, '5': 12, '10': 'acceptedIds'}, + {'1': 'rejected_ids', '3': 2, '4': 3, '5': 12, '10': 'rejectedIds'}, + ], +}; + +@$core.Deprecated('Use messageDescriptor instead') +const Message_ControlReadReceipt$json = { + '1': 'ControlReadReceipt', + '2': [ + {'1': 'read_ids', '3': 1, '4': 3, '5': 12, '10': 'readIds'}, ], }; @@ -272,15 +280,15 @@ final $typed_data.Uint8List messageDescriptor = $convert.base64Decode( 'HQoKdmlld19saW1pdBgFIAEoDVIJdmlld0xpbWl0EjgKC2F0dGFjaG1lbnRzGAYgAygLMhYudm' 'VpbGlkY2hhdC5BdHRhY2htZW50UgthdHRhY2htZW50c0IICgZfdG9waWNCCwoJX3JlcGx5X2lk' 'GkgKBlNlY3JldBIeCgpjaXBoZXJ0ZXh0GAEgASgMUgpjaXBoZXJ0ZXh0Eh4KCmV4cGlyYXRpb2' - '4YAiABKARSCmV4cGlyYXRpb24aMwoNQ29udHJvbERlbGV0ZRIiCgNpZHMYASADKAsyEC52ZWls' - 'aWQuVHlwZWRLZXlSA2lkcxosCgxDb250cm9sRXJhc2USHAoJdGltZXN0YW1wGAEgASgEUgl0aW' - '1lc3RhbXAaRwoPQ29udHJvbFNldHRpbmdzEjQKCHNldHRpbmdzGAEgASgLMhgudmVpbGlkY2hh' - 'dC5DaGF0U2V0dGluZ3NSCHNldHRpbmdzGk8KEkNvbnRyb2xQZXJtaXNzaW9ucxI5CgtwZXJtaX' - 'NzaW9ucxgBIAEoCzIXLnZlaWxpZGNoYXQuUGVybWlzc2lvbnNSC3Blcm1pc3Npb25zGksKEUNv' - 'bnRyb2xNZW1iZXJzaGlwEjYKCm1lbWJlcnNoaXAYASABKAsyFi52ZWlsaWRjaGF0Lk1lbWJlcn' - 'NoaXBSCm1lbWJlcnNoaXAafQoRQ29udHJvbE1vZGVyYXRpb24SMwoMYWNjZXB0ZWRfaWRzGAEg' - 'AygLMhAudmVpbGlkLlR5cGVkS2V5UgthY2NlcHRlZElkcxIzCgxyZWplY3RlZF9pZHMYAiADKA' - 'syEC52ZWlsaWQuVHlwZWRLZXlSC3JlamVjdGVkSWRzQgYKBGtpbmQ='); + '4YAiABKARSCmV4cGlyYXRpb24aIQoNQ29udHJvbERlbGV0ZRIQCgNpZHMYASADKAxSA2lkcxos' + 'CgxDb250cm9sRXJhc2USHAoJdGltZXN0YW1wGAEgASgEUgl0aW1lc3RhbXAaRwoPQ29udHJvbF' + 'NldHRpbmdzEjQKCHNldHRpbmdzGAEgASgLMhgudmVpbGlkY2hhdC5DaGF0U2V0dGluZ3NSCHNl' + 'dHRpbmdzGk8KEkNvbnRyb2xQZXJtaXNzaW9ucxI5CgtwZXJtaXNzaW9ucxgBIAEoCzIXLnZlaW' + 'xpZGNoYXQuUGVybWlzc2lvbnNSC3Blcm1pc3Npb25zGksKEUNvbnRyb2xNZW1iZXJzaGlwEjYK' + 'Cm1lbWJlcnNoaXAYASABKAsyFi52ZWlsaWRjaGF0Lk1lbWJlcnNoaXBSCm1lbWJlcnNoaXAaWQ' + 'oRQ29udHJvbE1vZGVyYXRpb24SIQoMYWNjZXB0ZWRfaWRzGAEgAygMUgthY2NlcHRlZElkcxIh' + 'CgxyZWplY3RlZF9pZHMYAiADKAxSC3JlamVjdGVkSWRzGi8KEkNvbnRyb2xSZWFkUmVjZWlwdB' + 'IZCghyZWFkX2lkcxgBIAMoDFIHcmVhZElkc0IGCgRraW5k'); @$core.Deprecated('Use reconciledMessageDescriptor instead') const ReconciledMessage$json = { diff --git a/lib/proto/veilidchat.proto b/lib/proto/veilidchat.proto index 81c963c..fa701fb 100644 --- a/lib/proto/veilidchat.proto +++ b/lib/proto/veilidchat.proto @@ -146,7 +146,7 @@ message Message { // A 'delete' control message // Deletes a set of messages by their ids message ControlDelete { - repeated veilid.TypedKey ids = 1; + repeated bytes ids = 1; } // An 'erase' control message // Deletes a set of messages from before some timestamp @@ -175,8 +175,13 @@ message Message { // A 'moderation' control message // Accepts or rejects a set of messages message ControlModeration { - repeated veilid.TypedKey accepted_ids = 1; - repeated veilid.TypedKey rejected_ids = 2; + repeated bytes accepted_ids = 1; + repeated bytes rejected_ids = 2; + } + + // A 'read receipt' control message + message ControlReadReceipt { + repeated bytes read_ids = 1; } ////////////////////////////////////////////////////////////////////////// diff --git a/packages/veilid_support/example/integration_test/test_dht_log.dart b/packages/veilid_support/example/integration_test/test_dht_log.dart index 0c06c87..0ebdd55 100644 --- a/packages/veilid_support/example/integration_test/test_dht_log.dart +++ b/packages/veilid_support/example/integration_test/test_dht_log.dart @@ -64,7 +64,7 @@ Future Function() makeTestDHTLogAddTruncate({required int stride}) => const chunk = 25; for (var n = 0; n < dataset.length; n += chunk) { print('$n-${n + chunk - 1} '); - final success = await w.tryAddItems(dataset.sublist(n, n + chunk)); + final success = await w.tryAddAll(dataset.sublist(n, n + chunk)); expect(success, isTrue); } }); @@ -73,22 +73,22 @@ Future Function() makeTestDHTLogAddTruncate({required int stride}) => print('get all\n'); { - final dataset2 = await dlog.operate((r) async => r.getItemRange(0)); + final dataset2 = await dlog.operate((r) async => r.getRange(0)); expect(dataset2, equals(dataset)); } { final dataset3 = - await dlog.operate((r) async => r.getItemRange(64, length: 128)); + await dlog.operate((r) async => r.getRange(64, length: 128)); expect(dataset3, equals(dataset.sublist(64, 64 + 128))); } { final dataset4 = - await dlog.operate((r) async => r.getItemRange(0, length: 1000)); + await dlog.operate((r) async => r.getRange(0, length: 1000)); expect(dataset4, equals(dataset.sublist(0, 1000))); } { final dataset5 = - await dlog.operate((r) async => r.getItemRange(500, length: 499)); + await dlog.operate((r) async => r.getRange(500, length: 499)); expect(dataset5, equals(dataset.sublist(500, 999))); } print('truncate\n'); @@ -96,8 +96,8 @@ Future Function() makeTestDHTLogAddTruncate({required int stride}) => await dlog.operateAppend((w) async => w.truncate(w.length - 5)); } { - final dataset6 = await dlog - .operate((r) async => r.getItemRange(500 - 5, length: 499)); + final dataset6 = + await dlog.operate((r) async => r.getRange(500 - 5, length: 499)); expect(dataset6, equals(dataset.sublist(500, 999))); } print('truncate 2\n'); @@ -105,8 +105,8 @@ Future Function() makeTestDHTLogAddTruncate({required int stride}) => await dlog.operateAppend((w) async => w.truncate(w.length - 251)); } { - final dataset7 = await dlog - .operate((r) async => r.getItemRange(500 - 256, length: 499)); + final dataset7 = + await dlog.operate((r) async => r.getRange(500 - 256, length: 499)); expect(dataset7, equals(dataset.sublist(500, 999))); } print('clear\n'); @@ -115,7 +115,7 @@ Future Function() makeTestDHTLogAddTruncate({required int stride}) => } print('get all\n'); { - final dataset8 = await dlog.operate((r) async => r.getItemRange(0)); + final dataset8 = await dlog.operate((r) async => r.getRange(0)); expect(dataset8, isEmpty); } print('delete and close\n'); diff --git a/packages/veilid_support/example/integration_test/test_dht_short_array.dart b/packages/veilid_support/example/integration_test/test_dht_short_array.dart index 637afe0..244b3d5 100644 --- a/packages/veilid_support/example/integration_test/test_dht_short_array.dart +++ b/packages/veilid_support/example/integration_test/test_dht_short_array.dart @@ -64,7 +64,7 @@ Future Function() makeTestDHTShortArrayAdd({required int stride}) => final res = await arr.operateWrite((w) async { for (var n = 4; n < 8; n++) { print('$n '); - final success = await w.tryAddItem(dataset[n]); + final success = await w.tryAdd(dataset[n]); expect(success, isTrue); } }); @@ -75,8 +75,8 @@ Future Function() makeTestDHTShortArrayAdd({required int stride}) => { final res = await arr.operateWrite((w) async { print('${dataset.length ~/ 2}-${dataset.length}'); - final success = await w.tryAddItems( - dataset.sublist(dataset.length ~/ 2, dataset.length)); + final success = await w + .tryAddAll(dataset.sublist(dataset.length ~/ 2, dataset.length)); expect(success, isTrue); }); expect(res, isNull); @@ -87,7 +87,7 @@ Future Function() makeTestDHTShortArrayAdd({required int stride}) => final res = await arr.operateWrite((w) async { for (var n = 0; n < 4; n++) { print('$n '); - final success = await w.tryInsertItem(n, dataset[n]); + final success = await w.tryInsert(n, dataset[n]); expect(success, isTrue); } }); @@ -98,8 +98,8 @@ Future Function() makeTestDHTShortArrayAdd({required int stride}) => { final res = await arr.operateWrite((w) async { print('8-${dataset.length ~/ 2}'); - final success = await w.tryInsertItems( - 8, dataset.sublist(8, dataset.length ~/ 2)); + final success = + await w.tryInsertAll(8, dataset.sublist(8, dataset.length ~/ 2)); expect(success, isTrue); }); expect(res, isNull); @@ -107,12 +107,12 @@ Future Function() makeTestDHTShortArrayAdd({required int stride}) => //print('get all\n'); { - final dataset2 = await arr.operate((r) async => r.getItemRange(0)); + final dataset2 = await arr.operate((r) async => r.getRange(0)); expect(dataset2, equals(dataset)); } { final dataset3 = - await arr.operate((r) async => r.getItemRange(64, length: 128)); + await arr.operate((r) async => r.getRange(64, length: 128)); expect(dataset3, equals(dataset.sublist(64, 64 + 128))); } @@ -126,7 +126,7 @@ Future Function() makeTestDHTShortArrayAdd({required int stride}) => //print('get all\n'); { - final dataset4 = await arr.operate((r) async => r.getItemRange(0)); + final dataset4 = await arr.operate((r) async => r.getRange(0)); expect(dataset4, isEmpty); } diff --git a/packages/veilid_support/lib/dht_support/proto/dht.proto b/packages/veilid_support/lib/dht_support/proto/dht.proto index 6796753..c27915c 100644 --- a/packages/veilid_support/lib/dht_support/proto/dht.proto +++ b/packages/veilid_support/lib/dht_support/proto/dht.proto @@ -62,13 +62,24 @@ message DHTShortArray { // calculated through iteration } +// Reference to data on the DHT +message DHTDataReference { + veilid.TypedKey dht_data = 1; + veilid.TypedKey hash = 2; +} + +// Reference to data on the BlockStore +message BlockStoreDataReference { + veilid.TypedKey block = 1; +} + // DataReference // Pointer to data somewhere in Veilid // Abstraction over DHTData and BlockStore message DataReference { oneof kind { - veilid.TypedKey dht_data = 1; - // TypedKey block = 2; + DHTDataReference dht_data = 1; + BlockStoreDataReference block_store_data = 2; } } diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart index cba15f4..acdc6fe 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart @@ -9,7 +9,7 @@ import 'package:meta/meta.dart'; import '../../../veilid_support.dart'; import '../../proto/proto.dart' as proto; -import '../interfaces/dht_append.dart'; +import '../interfaces/dht_add.dart'; part 'dht_log_spine.dart'; part 'dht_log_read.dart'; diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart index 3c054fc..010c76e 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart @@ -92,7 +92,7 @@ class DHTLogCubit extends Cubit> Future _refreshInner(void Function(AsyncValue>) emit, {bool forceRefresh = false}) async { - final avElements = await _loadElements(_tail, _count); + final avElements = await loadElements(_tail, _count); final err = avElements.asError; if (err != null) { emit(AsyncValue.error(err.error, err.stackTrace)); @@ -108,9 +108,10 @@ class DHTLogCubit extends Cubit> elements: elements, tail: _tail, count: _count, follow: _follow))); } - Future>>> _loadElements( + Future>>> loadElements( int tail, int count, {bool forceRefresh = false}) async { + await _initWait(); try { final allItems = await _log.operate((reader) async { final length = reader.length; @@ -118,7 +119,7 @@ class DHTLogCubit extends Cubit> final start = (count < end) ? end - count : 0; final offlinePositions = await reader.getOfflinePositions(); - final allItems = (await reader.getItemRange(start, + final allItems = (await reader.getRange(start, length: end - start, forceRefresh: forceRefresh)) ?.indexed .map((x) => DHTLogElementState( diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart index 0a66a01..7f397ac 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart @@ -12,7 +12,7 @@ class _DHTLogRead implements DHTLogReadOperations { int get length => _spine.length; @override - Future getItem(int pos, {bool forceRefresh = false}) async { + Future get(int pos, {bool forceRefresh = false}) async { if (pos < 0 || pos >= length) { throw IndexError.withLength(pos, length); } @@ -21,8 +21,8 @@ class _DHTLogRead implements DHTLogReadOperations { return null; } - return lookup.scope((sa) => sa.operate( - (read) => read.getItem(lookup.pos, forceRefresh: forceRefresh))); + return lookup.scope((sa) => + sa.operate((read) => read.get(lookup.pos, forceRefresh: forceRefresh))); } (int, int) _clampStartLen(int start, int? len) { @@ -40,14 +40,14 @@ class _DHTLogRead implements DHTLogReadOperations { } @override - Future?> getItemRange(int start, + Future?> getRange(int start, {int? length, bool forceRefresh = false}) async { final out = []; (start, length) = _clampStartLen(start, length); final chunks = Iterable.generate(length).slices(maxDHTConcurrency).map( - (chunk) => chunk - .map((pos) => getItem(pos + start, forceRefresh: forceRefresh))); + (chunk) => + chunk.map((pos) => get(pos + start, forceRefresh: forceRefresh))); for (final chunk in chunks) { final elems = await chunk.wait; diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index 9a8c64e..a47602a 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -3,16 +3,15 @@ part of 'dht_log.dart'; class _DHTLogPosition extends DHTCloseable<_DHTLogPosition, DHTShortArray> { _DHTLogPosition._({ required _DHTLogSpine dhtLogSpine, - required DHTShortArray shortArray, + required this.shortArray, required this.pos, required int segmentNumber, - }) : _segmentShortArray = shortArray, - _dhtLogSpine = dhtLogSpine, + }) : _dhtLogSpine = dhtLogSpine, _segmentNumber = segmentNumber; final int pos; final _DHTLogSpine _dhtLogSpine; - final DHTShortArray _segmentShortArray; + final DHTShortArray shortArray; var _openCount = 1; final int _segmentNumber; final Mutex _mutex = Mutex(); @@ -23,7 +22,7 @@ class _DHTLogPosition extends DHTCloseable<_DHTLogPosition, DHTShortArray> { /// The type of the openable scope @override - FutureOr scoped() => _segmentShortArray; + FutureOr scoped() => shortArray; /// Add a reference to this log @override diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart index 5503051..49acce0 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart @@ -17,7 +17,7 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { } final lookup = await _spine.lookupPosition(pos); if (lookup == null) { - throw StateError("can't write to dht log"); + throw StateError("can't lookup position in write to dht log"); } // Write item to the segment @@ -26,7 +26,47 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { } @override - Future tryAddItem(Uint8List value) async { + Future swap(int aPos, int bPos) async { + if (aPos < 0 || aPos >= _spine.length) { + throw IndexError.withLength(aPos, _spine.length); + } + if (bPos < 0 || bPos >= _spine.length) { + throw IndexError.withLength(bPos, _spine.length); + } + final aLookup = await _spine.lookupPosition(aPos); + if (aLookup == null) { + throw StateError("can't lookup position a in swap of dht log"); + } + final bLookup = await _spine.lookupPosition(bPos); + if (bLookup == null) { + throw StateError("can't lookup position b in swap of dht log"); + } + + // Swap items in the segments + if (aLookup.shortArray == bLookup.shortArray) { + await aLookup.scope((sa) => sa.operateWriteEventual((aWrite) async { + await aWrite.swap(aLookup.pos, bLookup.pos); + return true; + })); + } else { + final bItem = Output(); + await aLookup.scope( + (sa) => bLookup.scope((sb) => sa.operateWriteEventual((aWrite) async { + if (bItem.value == null) { + final aItem = await aWrite.get(aLookup.pos); + if (aItem == null) { + throw StateError("can't get item for position a in swap"); + } + await sb.operateWriteEventual((bWrite) async => + bWrite.tryWriteItem(bLookup.pos, aItem, output: bItem)); + } + return aWrite.tryWriteItem(aLookup.pos, bItem.value!); + }))); + } + } + + @override + Future tryAdd(Uint8List value) async { // Allocate empty index at the end of the list final insertPos = _spine.length; _spine.allocateTail(1); @@ -44,12 +84,12 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { // We should always be appending at the length throw StateError('appending should be at the end'); } - return write.tryAddItem(value); + return write.tryAdd(value); })); } @override - Future tryAddItems(List values) async { + Future tryAddAll(List values) async { // Allocate empty index at the end of the list final insertPos = _spine.length; _spine.allocateTail(values.length); @@ -79,7 +119,7 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { // We should always be appending at the length throw StateError('appending should be at the end'); } - return write.tryAddItems(sublistValues); + return write.tryAddAll(sublistValues); })); if (!ok) { success = false; 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 f4b806e..90fcbad 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 @@ -54,13 +54,12 @@ class DHTShortArrayCubit extends Cubit> try { final newState = await _shortArray.operate((reader) async { final offlinePositions = await reader.getOfflinePositions(); - final allItems = - (await reader.getItemRange(0, forceRefresh: forceRefresh)) - ?.indexed - .map((x) => DHTShortArrayElementState( - value: _decodeElement(x.$2), - isOffline: offlinePositions.contains(x.$1))) - .toIList(); + final allItems = (await reader.getRange(0, forceRefresh: forceRefresh)) + ?.indexed + .map((x) => DHTShortArrayElementState( + value: _decodeElement(x.$2), + isOffline: offlinePositions.contains(x.$1))) + .toIList(); return allItems; }); if (newState != null) { 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 index 5da8cf8..abe7198 100644 --- 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 @@ -12,7 +12,7 @@ class _DHTShortArrayRead implements DHTShortArrayReadOperations { int get length => _head.length; @override - Future getItem(int pos, {bool forceRefresh = false}) async { + Future get(int pos, {bool forceRefresh = false}) async { if (pos < 0 || pos >= length) { throw IndexError.withLength(pos, length); } @@ -49,14 +49,14 @@ class _DHTShortArrayRead implements DHTShortArrayReadOperations { } @override - Future?> getItemRange(int start, + Future?> getRange(int start, {int? length, bool forceRefresh = false}) async { final out = []; (start, length) = _clampStartLen(start, length); final chunks = Iterable.generate(length).slices(maxDHTConcurrency).map( - (chunk) => chunk - .map((pos) => getItem(pos + start, forceRefresh: forceRefresh))); + (chunk) => + chunk.map((pos) => get(pos + start, forceRefresh: forceRefresh))); for (final chunk in chunks) { final elems = await chunk.wait; 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 index c336e47..d002e35 100644 --- 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 @@ -16,15 +16,14 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead _DHTShortArrayWrite._(super.head) : super._(); @override - Future tryAddItem(Uint8List value) => - tryInsertItem(_head.length, value); + Future tryAdd(Uint8List value) => tryInsert(_head.length, value); @override - Future tryAddItems(List values) => - tryInsertItems(_head.length, values); + Future tryAddAll(List values) => + tryInsertAll(_head.length, values); @override - Future tryInsertItem(int pos, Uint8List value) async { + Future tryInsert(int pos, Uint8List value) async { if (pos < 0 || pos > _head.length) { throw IndexError.withLength(pos, _head.length); } @@ -44,7 +43,7 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead } @override - Future tryInsertItems(int pos, List values) async { + Future tryInsertAll(int pos, List values) async { if (pos < 0 || pos > _head.length) { throw IndexError.withLength(pos, _head.length); } @@ -100,7 +99,7 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead } @override - Future swapItem(int aPos, int bPos) async { + Future swap(int aPos, int bPos) async { if (aPos < 0 || aPos >= _head.length) { throw IndexError.withLength(aPos, _head.length); } @@ -112,7 +111,7 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead } @override - Future removeItem(int pos, {Output? output}) async { + Future remove(int pos, {Output? output}) async { if (pos < 0 || pos >= _head.length) { throw IndexError.withLength(pos, _head.length); } diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_append.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_add.dart similarity index 80% rename from packages/veilid_support/lib/dht_support/src/interfaces/dht_append.dart rename to packages/veilid_support/lib/dht_support/src/interfaces/dht_add.dart index a1f47ee..e2b5ad7 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_append.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_add.dart @@ -12,30 +12,30 @@ abstract class DHTAdd { /// changed before the element could be added or a newer value was found on /// the network. /// Throws a StateError if the container exceeds its maximum size. - Future tryAddItem(Uint8List value); + Future tryAdd(Uint8List value); /// Try to add a list of items to the DHT container. /// Return true if the elements were successfully added, and false if the /// state changed before the element could be added or a newer value was found /// on the network. /// Throws a StateError if the container exceeds its maximum size. - Future tryAddItems(List values); + Future tryAddAll(List values); } extension DHTAddExt on DHTAdd { /// Convenience function: /// Like tryAddItem but also encodes the input value as JSON and parses the /// returned element as JSON - Future tryAppendItemJson( + Future tryAddJson( T newValue, ) => - tryAddItem(jsonEncodeBytes(newValue)); + tryAdd(jsonEncodeBytes(newValue)); /// Convenience function: /// Like tryAddItem but also encodes the input value as a protobuf object /// and parses the returned element as a protobuf object - Future tryAddItemProtobuf( + Future tryAddProtobuf( T newValue, ) => - tryAddItem(newValue.writeToBuffer()); + tryAdd(newValue.writeToBuffer()); } diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_insert_remove.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_insert_remove.dart index 1f98a22..fe44368 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_insert_remove.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_insert_remove.dart @@ -14,7 +14,7 @@ abstract class DHTInsertRemove { /// Throws an IndexError if the position removed exceeds the length of /// the container. /// Throws a StateError if the container exceeds its maximum size. - Future tryInsertItem(int pos, Uint8List value); + Future tryInsert(int pos, Uint8List value); /// Try to insert items at position 'pos' of the DHT container. /// Return true if the elements were successfully inserted, and false if the @@ -23,38 +23,33 @@ abstract class DHTInsertRemove { /// Throws an IndexError if the position removed exceeds the length of /// the container. /// Throws a StateError if the container exceeds its maximum size. - Future tryInsertItems(int pos, List values); - - /// Swap items at position 'aPos' and 'bPos' in the DHTArray. - /// Throws an IndexError if either of the positions swapped exceeds the length - /// of the container - Future swapItem(int aPos, int bPos); + Future tryInsertAll(int pos, List values); /// Remove an item at position 'pos' in the DHT container. /// If the remove was successful this returns: /// * outValue will return the prior contents of the element /// Throws an IndexError if the position removed exceeds the length of /// the container. - Future removeItem(int pos, {Output? output}); + Future remove(int pos, {Output? output}); } extension DHTInsertRemoveExt on DHTInsertRemove { /// Convenience function: - /// Like removeItem but also parses the returned element as JSON - Future removeItemJson(T Function(dynamic) fromJson, int pos, + /// Like remove but also parses the returned element as JSON + Future removeJson(T Function(dynamic) fromJson, int pos, {Output? output}) async { final outValueBytes = output == null ? null : Output(); - await removeItem(pos, output: outValueBytes); + await remove(pos, output: outValueBytes); output.mapSave(outValueBytes, (b) => jsonDecodeBytes(fromJson, b)); } /// Convenience function: - /// Like removeItem but also parses the returned element as JSON - Future removeItemProtobuf( + /// Like remove but also parses the returned element as JSON + Future removeProtobuf( T Function(List) fromBuffer, int pos, {Output? output}) async { final outValueBytes = output == null ? null : Output(); - await removeItem(pos, output: outValueBytes); + await remove(pos, output: outValueBytes); output.mapSave(outValueBytes, fromBuffer); } } diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart index 39d49e6..362d688 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart @@ -15,14 +15,14 @@ abstract class DHTRandomRead { /// rather than returning the existing locally stored copy of the elements. /// Throws an IndexError if the 'pos' is not within the length /// of the container. - Future getItem(int pos, {bool forceRefresh = false}); + Future get(int pos, {bool forceRefresh = false}); /// Return a list of a range of items in the DHTArray. If 'forceRefresh' /// is specified, the network will always be checked for newer values /// rather than returning the existing locally stored copy of the elements. /// Throws an IndexError if either 'start' or '(start+length)' is not within /// the length of the container. - Future?> getItemRange(int start, + Future?> getRange(int start, {int? length, bool forceRefresh = false}); /// Get a list of the positions that were written offline and not flushed yet @@ -31,32 +31,32 @@ abstract class DHTRandomRead { extension DHTRandomReadExt on DHTRandomRead { /// Convenience function: - /// Like getItem but also parses the returned element as JSON - Future getItemJson(T Function(dynamic) fromJson, int pos, + /// Like get but also parses the returned element as JSON + Future getJson(T Function(dynamic) fromJson, int pos, {bool forceRefresh = false}) => - getItem(pos, forceRefresh: forceRefresh) + get(pos, forceRefresh: forceRefresh) .then((out) => jsonDecodeOptBytes(fromJson, out)); /// Convenience function: - /// Like getAllItems but also parses the returned elements as JSON - Future?> getItemRangeJson(T Function(dynamic) fromJson, int start, + /// Like getRange but also parses the returned elements as JSON + Future?> getRangeJson(T Function(dynamic) fromJson, int start, {int? length, bool forceRefresh = false}) => - getItemRange(start, length: length, forceRefresh: forceRefresh) + getRange(start, length: length, forceRefresh: forceRefresh) .then((out) => out?.map(fromJson).toList()); /// Convenience function: - /// Like getItem but also parses the returned element as a protobuf object - Future getItemProtobuf( + /// Like get but also parses the returned element as a protobuf object + Future getProtobuf( T Function(List) fromBuffer, int pos, {bool forceRefresh = false}) => - getItem(pos, forceRefresh: forceRefresh) + get(pos, forceRefresh: forceRefresh) .then((out) => (out == null) ? null : fromBuffer(out)); /// Convenience function: - /// Like getAllItems but also parses the returned elements as protobuf objects - Future?> getItemRangeProtobuf( + /// Like getRange but also parses the returned elements as protobuf objects + Future?> getRangeProtobuf( T Function(List) fromBuffer, int start, {int? length, bool forceRefresh = false}) => - getItemRange(start, length: length, forceRefresh: forceRefresh) + getRange(start, length: length, forceRefresh: forceRefresh) .then((out) => out?.map(fromBuffer).toList()); } diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart index 0d8f3ac..5b3f032 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart @@ -23,6 +23,11 @@ abstract class DHTRandomWrite { /// of the container. Future tryWriteItem(int pos, Uint8List newValue, {Output? output}); + + /// Swap items at position 'aPos' and 'bPos' in the DHTArray. + /// Throws an IndexError if either of the positions swapped exceeds the length + /// of the container + Future swap(int aPos, int bPos); } extension DHTRandomWriteExt on DHTRandomWrite { diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart b/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart index dd95cac..57d0979 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart @@ -1,4 +1,4 @@ -export 'dht_append.dart'; +export 'dht_add.dart'; export 'dht_clear.dart'; export 'dht_closeable.dart'; export 'dht_insert_remove.dart'; diff --git a/packages/veilid_support/lib/src/table_db_array.dart b/packages/veilid_support/lib/src/table_db_array.dart index 9714770..4d5b9dd 100644 --- a/packages/veilid_support/lib/src/table_db_array.dart +++ b/packages/veilid_support/lib/src/table_db_array.dart @@ -6,6 +6,7 @@ import 'package:async_tools/async_tools.dart'; import 'package:charcode/charcode.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; +import 'package:protobuf/protobuf.dart'; import '../veilid_support.dart'; @@ -128,13 +129,13 @@ class TableDBArray { }); } - Future> getRange(int start, int end) async { + Future> getRange(int start, [int? end]) async { await _initWait(); return _mutex.protect(() async { if (!_open) { throw StateError('not open'); } - return _getRangeInner(start, end); + return _getRangeInner(start, end ?? _length); }); } @@ -629,3 +630,36 @@ class TableDBArray { final StreamController _changeStream = StreamController.broadcast(); } + +extension TableDBArrayExt on TableDBArray { + /// Convenience function: + /// Like get but also parses the returned element as JSON + Future getJson( + T Function(dynamic) fromJson, + int pos, + ) => + get( + pos, + ).then((out) => jsonDecodeOptBytes(fromJson, out)); + + /// Convenience function: + /// Like getRange but also parses the returned elements as JSON + Future?> getRangeJson(T Function(dynamic) fromJson, int start, + [int? end]) => + getRange(start, end ?? _length).then((out) => out.map(fromJson).toList()); + + /// Convenience function: + /// Like get but also parses the returned element as a protobuf object + Future getProtobuf( + T Function(List) fromBuffer, + int pos, + ) => + get(pos).then(fromBuffer); + + /// Convenience function: + /// Like getRange but also parses the returned elements as protobuf objects + Future?> getRangeProtobuf( + T Function(List) fromBuffer, int start, [int? end]) => + getRange(start, end ?? _length) + .then((out) => out.map(fromBuffer).toList()); +} diff --git a/packages/veilid_support/lib/src/veilid_crypto.dart b/packages/veilid_support/lib/src/veilid_crypto.dart index 75087fb..565459c 100644 --- a/packages/veilid_support/lib/src/veilid_crypto.dart +++ b/packages/veilid_support/lib/src/veilid_crypto.dart @@ -13,16 +13,16 @@ abstract class VeilidCrypto { class VeilidCryptoPrivate implements VeilidCrypto { VeilidCryptoPrivate._(VeilidCryptoSystem cryptoSystem, SharedSecret secretKey) : _cryptoSystem = cryptoSystem, - _secretKey = secretKey; + _secret = secretKey; final VeilidCryptoSystem _cryptoSystem; - final SharedSecret _secretKey; + final SharedSecret _secret; static Future fromTypedKey( - TypedKey typedKey, String domain) async { - final cryptoSystem = await Veilid.instance.getCryptoSystem(typedKey.kind); - final keyMaterial = Uint8List(0) - ..addAll(typedKey.value.decode()) - ..addAll(utf8.encode(domain)); + TypedKey typedSecret, String domain) async { + final cryptoSystem = + await Veilid.instance.getCryptoSystem(typedSecret.kind); + final keyMaterial = Uint8List.fromList( + [...typedSecret.value.decode(), ...utf8.encode(domain)]); final secretKey = await cryptoSystem.generateHash(keyMaterial); return VeilidCryptoPrivate._(cryptoSystem, secretKey); } @@ -35,18 +35,18 @@ class VeilidCryptoPrivate implements VeilidCrypto { } static Future fromSharedSecret( - CryptoKind kind, SharedSecret secretKey) async { + CryptoKind kind, SharedSecret sharedSecret) async { final cryptoSystem = await Veilid.instance.getCryptoSystem(kind); - return VeilidCryptoPrivate._(cryptoSystem, secretKey); + return VeilidCryptoPrivate._(cryptoSystem, sharedSecret); } @override Future encrypt(Uint8List data) => - _cryptoSystem.encryptNoAuthWithNonce(data, _secretKey); + _cryptoSystem.encryptNoAuthWithNonce(data, _secret); @override Future decrypt(Uint8List data) => - _cryptoSystem.decryptNoAuthWithNonce(data, _secretKey); + _cryptoSystem.decryptNoAuthWithNonce(data, _secret); } //////////////////////////////////// From 6d05c9f1258cce185450a2f90ab140fd5ddbf32c Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 29 May 2024 10:47:43 -0400 Subject: [PATCH 118/270] everything but reconcile --- .../cubits/single_contact_messages_cubit.dart | 180 ++++++++---------- lib/chat/models/message_state.dart | 14 +- lib/chat/models/message_state.freezed.dart | 71 ++++--- lib/chat/models/message_state.g.dart | 8 +- lib/chat/views/chat_component.dart | 9 +- lib/proto/extensions.dart | 14 ++ packages/veilid_support/lib/proto/dht.pb.dart | 124 +++++++++++- .../veilid_support/lib/proto/dht.pbjson.dart | 35 +++- 8 files changed, 305 insertions(+), 150 deletions(-) diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 7ab3401..6310af6 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -16,7 +16,7 @@ class RenderStateElement { RenderStateElement( {required this.message, required this.isLocal, - this.reconciled = false, + this.reconciledTimestamp, this.sent = false, this.sentOffline = false}); @@ -28,7 +28,7 @@ class RenderStateElement { if (sent && !sentOffline) { return MessageSendState.delivered; } - if (reconciled) { + if (reconciledTimestamp != null) { return MessageSendState.sent; } return MessageSendState.sending; @@ -36,7 +36,7 @@ class RenderStateElement { proto.Message message; bool isLocal; - bool reconciled; + Timestamp? reconciledTimestamp; bool sent; bool sentOffline; } @@ -68,7 +68,6 @@ class SingleContactMessagesCubit extends Cubit { Future close() async { await _initWait(); - await _unreconciledMessagesQueue.close(); await _sendingMessagesQueue.close(); await _sentSubscription?.cancel(); await _rcvdSubscription?.cancel(); @@ -81,13 +80,6 @@ class SingleContactMessagesCubit extends Cubit { // Initialize everything Future _init() async { - // Late initialization of queues with closures - _unreconciledMessagesQueue = PersistentQueue( - table: 'SingleContactUnreconciledMessages', - key: _remoteConversationRecordKey.toString(), - fromBuffer: proto.Message.fromBuffer, - closure: _processUnreconciledMessages, - ); _sendingMessagesQueue = PersistentQueue( table: 'SingleContactSendingMessages', key: _remoteConversationRecordKey.toString(), @@ -160,13 +152,14 @@ class SingleContactMessagesCubit extends Cubit { _reconciledMessagesCubit = TableDBArrayCubit( open: () async => TableDBArray.make(table: tableName, crypto: crypto), - decodeElement: proto.Message.fromBuffer); + decodeElement: proto.ReconciledMessage.fromBuffer); _reconciledSubscription = _reconciledMessagesCubit!.stream.listen(_updateReconciledMessagesState); _updateReconciledMessagesState(_reconciledMessagesCubit!.state); } //////////////////////////////////////////////////////////////////////////// + // Public interface // Set the tail position of the log for pagination. // If tail is 0, the end of the log is used. @@ -181,7 +174,14 @@ class SingleContactMessagesCubit extends Cubit { tail: tail, count: count, follow: follow, forceRefresh: forceRefresh); } + // Set a user-visible 'text' message with possible attachments + void sendTextMessage({required proto.Message_Text messageText}) { + final message = proto.Message()..text = messageText; + _sendMessage(message: message); + } + //////////////////////////////////////////////////////////////////////////// + // Internal implementation // Called when the sent messages cubit gets a change // This will re-render when messages are sent from another machine @@ -191,10 +191,7 @@ class SingleContactMessagesCubit extends Cubit { return; } - await _reconcileMessages(sentMessages, _sentMessagesCubit); - - // Update the view - _renderState(); + _reconcileMessages(sentMessages, _sentMessagesCubit!); } // Called when the received messages cubit gets a change @@ -204,61 +201,16 @@ class SingleContactMessagesCubit extends Cubit { return; } - await _reconcileMessages(rcvdMessages, _rcvdMessagesCubit); - - singleFuture(_rcvdMessagesCubit!, () async { - // Get the timestamp of our most recent reconciled message - final lastReconciledMessageTs = - await _reconciledMessagesCubit!.operate((arr) async { - final len = arr.length; - if (len == 0) { - return null; - } else { - final lastMessage = - await arr.getProtobuf(proto.Message.fromBuffer, len - 1); - if (lastMessage == null) { - throw StateError('should have gotten last message'); - } - return lastMessage.timestamp; - } - }); - - // Find oldest message we have not yet reconciled - - // // Go through all the ones from the cubit state first since we've already - // // gotten them from the DHT - // for (var rn = rcvdMessages.elements.length; rn >= 0; rn--) { - // // - // } - - // // Add remote messages updates to queue to process asynchronously - // // Ignore offline state because remote messages are always fully delivered - // // This may happen once per client but should be idempotent - // _unreconciledMessagesQueue.addAllSync(rcvdMessages.map((x) => x.value)); - - // Update the view - _renderState(); - }); + _reconcileMessages(rcvdMessages, _rcvdMessagesCubit!); } // Called when the reconciled messages window gets a change void _updateReconciledMessagesState( - TableDBArrayBusyState avmessages) { + TableDBArrayBusyState avmessages) { // Update the view _renderState(); } - // Async process to reconcile messages sent or received in the background - Future _processUnreconciledMessages( - IList messages) async { - // await _reconciledMessagesCubit! - // .operateAppendEventual((reconciledMessagesWriter) async { - // await _reconcileMessagesInner( - // reconciledMessagesWriter: reconciledMessagesWriter, - // messages: messages); - // }); - } - Future _hashSignature(proto.Signature signature) async => (await _localMessagesCryptoSystem .generateHash(signature.toVeilid().decode())) @@ -317,6 +269,43 @@ class SingleContactMessagesCubit extends Cubit { writer.tryAddAll(messages.map((m) => m.writeToBuffer()).toList())); } + void _reconcileMessages(DHTLogStateData inputMessages, + DHTLogCubit inputMessagesCubit) { + singleFuture(_reconciledMessagesCubit!, () async { + // Get the timestamp of our most recent reconciled message + final lastReconciledMessageTs = + await _reconciledMessagesCubit!.operate((arr) async { + final len = arr.length; + if (len == 0) { + return null; + } else { + final lastMessage = + await arr.getProtobuf(proto.Message.fromBuffer, len - 1); + if (lastMessage == null) { + throw StateError('should have gotten last message'); + } + return lastMessage.timestamp; + } + }); + + // Find oldest message we have not yet reconciled + + // // Go through all the ones from the cubit state first since we've already + // // gotten them from the DHT + // for (var rn = rcvdMessages.elements.length; rn >= 0; rn--) { + // // + // } + + // // Add remote messages updates to queue to process asynchronously + // // Ignore offline state because remote messages are always fully delivered + // // This may happen once per client but should be idempotent + // _unreconciledMessagesQueue.addAllSync(rcvdMessages.map((x) => x.value)); + + // Update the view + _renderState(); + }); + } + Future _reconcileMessagesInner( {required DHTLogWriteOperations reconciledMessagesWriter, required IList messages}) async { @@ -380,15 +369,11 @@ class SingleContactMessagesCubit extends Cubit { // Produce a state for this cubit from the input cubits and queues void _renderState() { - // xxx move into a singlefuture - // Get all reconciled messages final reconciledMessages = _reconciledMessagesCubit?.state.state.asData?.value; // Get all sent messages final sentMessages = _sentMessagesCubit?.state.state.asData?.value; - // Get all items in the unreconciled queue - final unreconciledMessages = _unreconciledMessagesQueue.queue; // Get all items in the unsent queue final sendingMessages = _sendingMessagesQueue.queue; @@ -400,31 +385,30 @@ class SingleContactMessagesCubit extends Cubit { // Generate state for each message final sentMessagesMap = - IMap>.fromValues( - keyMapper: (x) => x.value.timestamp, + IMap>.fromValues( + keyMapper: (x) => x.value.uniqueIdString, values: sentMessages.elements, ); - final reconciledMessagesMap = IMap.fromValues( - keyMapper: (x) => x.timestamp, + final reconciledMessagesMap = + IMap.fromValues( + keyMapper: (x) => x.content.uniqueIdString, values: reconciledMessages.elements, ); - final sendingMessagesMap = IMap.fromValues( - keyMapper: (x) => x.timestamp, + final sendingMessagesMap = IMap.fromValues( + keyMapper: (x) => x.uniqueIdString, values: sendingMessages, ); - final unreconciledMessagesMap = IMap.fromValues( - keyMapper: (x) => x.timestamp, - values: unreconciledMessages, - ); - final renderedElements = {}; + final renderedElements = {}; for (final m in reconciledMessagesMap.entries) { renderedElements[m.key] = RenderStateElement( - message: m.value.value, - isLocal: m.value.value.author.toVeilid() != _remoteIdentityPublicKey, - reconciled: true, - reconciledOffline: m.value.isOffline); + message: m.value.content, + isLocal: m.value.content.author.toVeilid() == + _activeAccountInfo.localAccount.identityMaster + .identityPublicTypedKey(), + reconciledTimestamp: Timestamp.fromInt64(m.value.reconciledTime), + ); } for (final m in sentMessagesMap.entries) { renderedElements.putIfAbsent( @@ -436,17 +420,6 @@ class SingleContactMessagesCubit extends Cubit { ..sent = true ..sentOffline = m.value.isOffline; } - for (final m in unreconciledMessagesMap.entries) { - renderedElements - .putIfAbsent( - m.key, - () => RenderStateElement( - message: m.value, - isLocal: - m.value.author.toVeilid() != _remoteIdentityPublicKey, - )) - .reconciled = false; - } for (final m in sendingMessagesMap.entries) { renderedElements .putIfAbsent( @@ -465,24 +438,25 @@ class SingleContactMessagesCubit extends Cubit { final renderedState = messageKeys .map((x) => MessageState( content: x.value.message, - timestamp: Timestamp.fromInt64(x.key), + sentTimestamp: Timestamp.fromInt64(x.value.message.timestamp), + reconciledTimestamp: x.value.reconciledTimestamp, sendState: x.value.sendState)) .toIList(); // Emit the rendered state - emit(AsyncValue.data(renderedState)); } - void sendTextMessage({required proto.Message_Text messageText}) { - final message = proto.Message() - ..id = generateNextId() + void _sendMessage({required proto.Message message}) { + // Add common fields + // id and signature will get set by _processMessageToSend + message ..author = _activeAccountInfo.localAccount.identityMaster .identityPublicTypedKey() .toProto() - ..timestamp = Veilid.instance.now().toInt64() - ..text = messageText; + ..timestamp = Veilid.instance.now().toInt64(); + // Put in the queue _sendingMessagesQueue.addSync(message); // Update the view @@ -490,6 +464,7 @@ class SingleContactMessagesCubit extends Cubit { } ///////////////////////////////////////////////////////////////////////// + // Static utility functions static Future cleanupAndDeleteMessages( {required TypedKey localConversationRecordKey}) async { @@ -518,13 +493,12 @@ class SingleContactMessagesCubit extends Cubit { DHTLogCubit? _sentMessagesCubit; DHTLogCubit? _rcvdMessagesCubit; - TableDBArrayCubit? _reconciledMessagesCubit; + TableDBArrayCubit? _reconciledMessagesCubit; - late final PersistentQueue _unreconciledMessagesQueue; xxx can we eliminate this? and make rcvd messages cubit listener work like sent? late final PersistentQueue _sendingMessagesQueue; StreamSubscription>? _sentSubscription; StreamSubscription>? _rcvdSubscription; - StreamSubscription>? + StreamSubscription>? _reconciledSubscription; } diff --git a/lib/chat/models/message_state.dart b/lib/chat/models/message_state.dart index 993ebcb..f9952fa 100644 --- a/lib/chat/models/message_state.dart +++ b/lib/chat/models/message_state.dart @@ -28,8 +28,10 @@ class MessageState with _$MessageState { // Content of the message @JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) required proto.Message content, - // Received or delivered timestamp - required Timestamp timestamp, + // Sent timestamp + required Timestamp sentTimestamp, + // Reconciled timestamp + required Timestamp? reconciledTimestamp, // The state of the message required MessageSendState? sendState, }) = _MessageState; @@ -37,11 +39,3 @@ class MessageState with _$MessageState { factory MessageState.fromJson(dynamic json) => _$MessageStateFromJson(json as Map); } - -extension MessageStateExt on MessageState { - Uint8List get uniqueId { - final author = content.author.toVeilid().decode(); - final id = content.id; - return author..addAll(id); - } -} diff --git a/lib/chat/models/message_state.freezed.dart b/lib/chat/models/message_state.freezed.dart index b411f4c..a99f937 100644 --- a/lib/chat/models/message_state.freezed.dart +++ b/lib/chat/models/message_state.freezed.dart @@ -23,8 +23,10 @@ mixin _$MessageState { // Content of the message @JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) proto.Message get content => - throw _privateConstructorUsedError; // Received or delivered timestamp - Timestamp get timestamp => + throw _privateConstructorUsedError; // Sent timestamp + Timestamp get sentTimestamp => + throw _privateConstructorUsedError; // Reconciled timestamp + Timestamp? get reconciledTimestamp => throw _privateConstructorUsedError; // The state of the message MessageSendState? get sendState => throw _privateConstructorUsedError; @@ -43,7 +45,8 @@ abstract class $MessageStateCopyWith<$Res> { $Res call( {@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) proto.Message content, - Timestamp timestamp, + Timestamp sentTimestamp, + Timestamp? reconciledTimestamp, MessageSendState? sendState}); } @@ -61,7 +64,8 @@ class _$MessageStateCopyWithImpl<$Res, $Val extends MessageState> @override $Res call({ Object? content = null, - Object? timestamp = null, + Object? sentTimestamp = null, + Object? reconciledTimestamp = freezed, Object? sendState = freezed, }) { return _then(_value.copyWith( @@ -69,10 +73,14 @@ class _$MessageStateCopyWithImpl<$Res, $Val extends MessageState> ? _value.content : content // ignore: cast_nullable_to_non_nullable as proto.Message, - timestamp: null == timestamp - ? _value.timestamp - : timestamp // ignore: cast_nullable_to_non_nullable + sentTimestamp: null == sentTimestamp + ? _value.sentTimestamp + : sentTimestamp // ignore: cast_nullable_to_non_nullable as Timestamp, + reconciledTimestamp: freezed == reconciledTimestamp + ? _value.reconciledTimestamp + : reconciledTimestamp // ignore: cast_nullable_to_non_nullable + as Timestamp?, sendState: freezed == sendState ? _value.sendState : sendState // ignore: cast_nullable_to_non_nullable @@ -92,7 +100,8 @@ abstract class _$$MessageStateImplCopyWith<$Res> $Res call( {@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) proto.Message content, - Timestamp timestamp, + Timestamp sentTimestamp, + Timestamp? reconciledTimestamp, MessageSendState? sendState}); } @@ -108,7 +117,8 @@ class __$$MessageStateImplCopyWithImpl<$Res> @override $Res call({ Object? content = null, - Object? timestamp = null, + Object? sentTimestamp = null, + Object? reconciledTimestamp = freezed, Object? sendState = freezed, }) { return _then(_$MessageStateImpl( @@ -116,10 +126,14 @@ class __$$MessageStateImplCopyWithImpl<$Res> ? _value.content : content // ignore: cast_nullable_to_non_nullable as proto.Message, - timestamp: null == timestamp - ? _value.timestamp - : timestamp // ignore: cast_nullable_to_non_nullable + sentTimestamp: null == sentTimestamp + ? _value.sentTimestamp + : sentTimestamp // ignore: cast_nullable_to_non_nullable as Timestamp, + reconciledTimestamp: freezed == reconciledTimestamp + ? _value.reconciledTimestamp + : reconciledTimestamp // ignore: cast_nullable_to_non_nullable + as Timestamp?, sendState: freezed == sendState ? _value.sendState : sendState // ignore: cast_nullable_to_non_nullable @@ -134,7 +148,8 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { const _$MessageStateImpl( {@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) required this.content, - required this.timestamp, + required this.sentTimestamp, + required this.reconciledTimestamp, required this.sendState}); factory _$MessageStateImpl.fromJson(Map json) => @@ -144,16 +159,19 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { @override @JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) final proto.Message content; -// Received or delivered timestamp +// Sent timestamp @override - final Timestamp timestamp; + final Timestamp sentTimestamp; +// Reconciled timestamp + @override + final Timestamp? reconciledTimestamp; // The state of the message @override final MessageSendState? sendState; @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'MessageState(content: $content, timestamp: $timestamp, sendState: $sendState)'; + return 'MessageState(content: $content, sentTimestamp: $sentTimestamp, reconciledTimestamp: $reconciledTimestamp, sendState: $sendState)'; } @override @@ -162,7 +180,8 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { properties ..add(DiagnosticsProperty('type', 'MessageState')) ..add(DiagnosticsProperty('content', content)) - ..add(DiagnosticsProperty('timestamp', timestamp)) + ..add(DiagnosticsProperty('sentTimestamp', sentTimestamp)) + ..add(DiagnosticsProperty('reconciledTimestamp', reconciledTimestamp)) ..add(DiagnosticsProperty('sendState', sendState)); } @@ -172,15 +191,18 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { (other.runtimeType == runtimeType && other is _$MessageStateImpl && (identical(other.content, content) || other.content == content) && - (identical(other.timestamp, timestamp) || - other.timestamp == timestamp) && + (identical(other.sentTimestamp, sentTimestamp) || + other.sentTimestamp == sentTimestamp) && + (identical(other.reconciledTimestamp, reconciledTimestamp) || + other.reconciledTimestamp == reconciledTimestamp) && (identical(other.sendState, sendState) || other.sendState == sendState)); } @JsonKey(ignore: true) @override - int get hashCode => Object.hash(runtimeType, content, timestamp, sendState); + int get hashCode => Object.hash( + runtimeType, content, sentTimestamp, reconciledTimestamp, sendState); @JsonKey(ignore: true) @override @@ -200,7 +222,8 @@ abstract class _MessageState implements MessageState { const factory _MessageState( {@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) required final proto.Message content, - required final Timestamp timestamp, + required final Timestamp sentTimestamp, + required final Timestamp? reconciledTimestamp, required final MessageSendState? sendState}) = _$MessageStateImpl; factory _MessageState.fromJson(Map json) = @@ -209,8 +232,10 @@ abstract class _MessageState implements MessageState { @override // Content of the message @JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) proto.Message get content; - @override // Received or delivered timestamp - Timestamp get timestamp; + @override // Sent timestamp + Timestamp get sentTimestamp; + @override // Reconciled timestamp + Timestamp? get reconciledTimestamp; @override // The state of the message MessageSendState? get sendState; @override diff --git a/lib/chat/models/message_state.g.dart b/lib/chat/models/message_state.g.dart index 3471720..99899a7 100644 --- a/lib/chat/models/message_state.g.dart +++ b/lib/chat/models/message_state.g.dart @@ -9,7 +9,10 @@ part of 'message_state.dart'; _$MessageStateImpl _$$MessageStateImplFromJson(Map json) => _$MessageStateImpl( content: messageFromJson(json['content'] as Map), - timestamp: Timestamp.fromJson(json['timestamp']), + sentTimestamp: Timestamp.fromJson(json['sent_timestamp']), + reconciledTimestamp: json['reconciled_timestamp'] == null + ? null + : Timestamp.fromJson(json['reconciled_timestamp']), sendState: json['send_state'] == null ? null : MessageSendState.fromJson(json['send_state']), @@ -18,6 +21,7 @@ _$MessageStateImpl _$$MessageStateImplFromJson(Map json) => Map _$$MessageStateImplToJson(_$MessageStateImpl instance) => { 'content': messageToJson(instance.content), - 'timestamp': instance.timestamp.toJson(), + 'sent_timestamp': instance.sentTimestamp.toJson(), + 'reconciled_timestamp': instance.reconciledTimestamp?.toJson(), 'send_state': instance.sendState?.toJson(), }; diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 06d4312..7f549cb 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -104,7 +104,7 @@ class ChatComponent extends StatelessWidget { ///////////////////////////////////////////////////////////////////// - types.Message? messageToChatMessage(MessageState message) { + types.Message? messageStateToChatMessage(MessageState message) { final isLocal = message.content.author.toVeilid() == _localUserIdentityKey; types.Status? status; @@ -125,8 +125,9 @@ class ChatComponent extends StatelessWidget { final contextText = message.content.text; final textMessage = types.TextMessage( author: isLocal ? _localUser : _remoteUser, - createdAt: (message.timestamp.value ~/ BigInt.from(1000)).toInt(), - id: base64UrlNoPadEncode(message.uniqueId), + createdAt: + (message.sentTimestamp.value ~/ BigInt.from(1000)).toInt(), + id: message.content.uniqueIdString, text: contextText.text, showStatus: status != null, status: status); @@ -219,7 +220,7 @@ class ChatComponent extends StatelessWidget { final chatMessages = []; final tsSet = {}; for (final message in messages) { - final chatMessage = messageToChatMessage(message); + final chatMessage = messageStateToChatMessage(message); if (chatMessage == null) { continue; } diff --git a/lib/proto/extensions.dart b/lib/proto/extensions.dart index 64fabf6..e9fd9a2 100644 --- a/lib/proto/extensions.dart +++ b/lib/proto/extensions.dart @@ -1,3 +1,7 @@ +import 'dart:typed_data'; + +import 'package:veilid_support/veilid_support.dart'; + import 'proto.dart' as proto; proto.Message messageFromJson(Map j) => @@ -10,3 +14,13 @@ proto.ReconciledMessage reconciledMessageFromJson(Map j) => Map reconciledMessageToJson(proto.ReconciledMessage m) => m.writeToJsonMap(); + +extension MessageExt on proto.Message { + Uint8List get uniqueIdBytes { + final author = this.author.toVeilid().decode(); + final id = this.id; + return Uint8List.fromList([...author, ...id]); + } + + String get uniqueIdString => base64UrlNoPadEncode(uniqueIdBytes); +} diff --git a/packages/veilid_support/lib/proto/dht.pb.dart b/packages/veilid_support/lib/proto/dht.pb.dart index 4007d3d..814bd22 100644 --- a/packages/veilid_support/lib/proto/dht.pb.dart +++ b/packages/veilid_support/lib/proto/dht.pb.dart @@ -195,8 +195,109 @@ class DHTShortArray extends $pb.GeneratedMessage { $core.List<$core.int> get seqs => $_getList(2); } +class DHTDataReference extends $pb.GeneratedMessage { + factory DHTDataReference() => create(); + DHTDataReference._() : super(); + factory DHTDataReference.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory DHTDataReference.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DHTDataReference', package: const $pb.PackageName(_omitMessageNames ? '' : 'dht'), createEmptyInstance: create) + ..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'dhtData', subBuilder: $0.TypedKey.create) + ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'hash', subBuilder: $0.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') + DHTDataReference clone() => DHTDataReference()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + DHTDataReference copyWith(void Function(DHTDataReference) updates) => super.copyWith((message) => updates(message as DHTDataReference)) as DHTDataReference; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DHTDataReference create() => DHTDataReference._(); + DHTDataReference createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DHTDataReference getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static DHTDataReference? _defaultInstance; + + @$pb.TagNumber(1) + $0.TypedKey get dhtData => $_getN(0); + @$pb.TagNumber(1) + set dhtData($0.TypedKey v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasDhtData() => $_has(0); + @$pb.TagNumber(1) + void clearDhtData() => clearField(1); + @$pb.TagNumber(1) + $0.TypedKey ensureDhtData() => $_ensure(0); + + @$pb.TagNumber(2) + $0.TypedKey get hash => $_getN(1); + @$pb.TagNumber(2) + set hash($0.TypedKey v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasHash() => $_has(1); + @$pb.TagNumber(2) + void clearHash() => clearField(2); + @$pb.TagNumber(2) + $0.TypedKey ensureHash() => $_ensure(1); +} + +class BlockStoreDataReference extends $pb.GeneratedMessage { + factory BlockStoreDataReference() => create(); + BlockStoreDataReference._() : super(); + factory BlockStoreDataReference.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory BlockStoreDataReference.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'BlockStoreDataReference', package: const $pb.PackageName(_omitMessageNames ? '' : 'dht'), createEmptyInstance: create) + ..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'block', subBuilder: $0.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') + BlockStoreDataReference clone() => BlockStoreDataReference()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + BlockStoreDataReference copyWith(void Function(BlockStoreDataReference) updates) => super.copyWith((message) => updates(message as BlockStoreDataReference)) as BlockStoreDataReference; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static BlockStoreDataReference create() => BlockStoreDataReference._(); + BlockStoreDataReference createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static BlockStoreDataReference getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static BlockStoreDataReference? _defaultInstance; + + @$pb.TagNumber(1) + $0.TypedKey get block => $_getN(0); + @$pb.TagNumber(1) + set block($0.TypedKey v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasBlock() => $_has(0); + @$pb.TagNumber(1) + void clearBlock() => clearField(1); + @$pb.TagNumber(1) + $0.TypedKey ensureBlock() => $_ensure(0); +} + enum DataReference_Kind { dhtData, + blockStoreData, notSet } @@ -208,11 +309,13 @@ class DataReference extends $pb.GeneratedMessage { static const $core.Map<$core.int, DataReference_Kind> _DataReference_KindByTag = { 1 : DataReference_Kind.dhtData, + 2 : DataReference_Kind.blockStoreData, 0 : DataReference_Kind.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DataReference', package: const $pb.PackageName(_omitMessageNames ? '' : 'dht'), createEmptyInstance: create) - ..oo(0, [1]) - ..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'dhtData', subBuilder: $0.TypedKey.create) + ..oo(0, [1, 2]) + ..aOM(1, _omitFieldNames ? '' : 'dhtData', subBuilder: DHTDataReference.create) + ..aOM(2, _omitFieldNames ? '' : 'blockStoreData', subBuilder: BlockStoreDataReference.create) ..hasRequiredFields = false ; @@ -241,15 +344,26 @@ class DataReference extends $pb.GeneratedMessage { void clearKind() => clearField($_whichOneof(0)); @$pb.TagNumber(1) - $0.TypedKey get dhtData => $_getN(0); + DHTDataReference get dhtData => $_getN(0); @$pb.TagNumber(1) - set dhtData($0.TypedKey v) { setField(1, v); } + set dhtData(DHTDataReference v) { setField(1, v); } @$pb.TagNumber(1) $core.bool hasDhtData() => $_has(0); @$pb.TagNumber(1) void clearDhtData() => clearField(1); @$pb.TagNumber(1) - $0.TypedKey ensureDhtData() => $_ensure(0); + DHTDataReference ensureDhtData() => $_ensure(0); + + @$pb.TagNumber(2) + BlockStoreDataReference get blockStoreData => $_getN(1); + @$pb.TagNumber(2) + set blockStoreData(BlockStoreDataReference v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasBlockStoreData() => $_has(1); + @$pb.TagNumber(2) + void clearBlockStoreData() => clearField(2); + @$pb.TagNumber(2) + BlockStoreDataReference ensureBlockStoreData() => $_ensure(1); } class OwnedDHTRecordPointer extends $pb.GeneratedMessage { diff --git a/packages/veilid_support/lib/proto/dht.pbjson.dart b/packages/veilid_support/lib/proto/dht.pbjson.dart index 6c99cb7..b8575b9 100644 --- a/packages/veilid_support/lib/proto/dht.pbjson.dart +++ b/packages/veilid_support/lib/proto/dht.pbjson.dart @@ -60,11 +60,39 @@ final $typed_data.Uint8List dHTShortArrayDescriptor = $convert.base64Decode( 'Cg1ESFRTaG9ydEFycmF5EiQKBGtleXMYASADKAsyEC52ZWlsaWQuVHlwZWRLZXlSBGtleXMSFA' 'oFaW5kZXgYAiABKAxSBWluZGV4EhIKBHNlcXMYAyADKA1SBHNlcXM='); +@$core.Deprecated('Use dHTDataReferenceDescriptor instead') +const DHTDataReference$json = { + '1': 'DHTDataReference', + '2': [ + {'1': 'dht_data', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'dhtData'}, + {'1': 'hash', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'hash'}, + ], +}; + +/// Descriptor for `DHTDataReference`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List dHTDataReferenceDescriptor = $convert.base64Decode( + 'ChBESFREYXRhUmVmZXJlbmNlEisKCGRodF9kYXRhGAEgASgLMhAudmVpbGlkLlR5cGVkS2V5Ug' + 'dkaHREYXRhEiQKBGhhc2gYAiABKAsyEC52ZWlsaWQuVHlwZWRLZXlSBGhhc2g='); + +@$core.Deprecated('Use blockStoreDataReferenceDescriptor instead') +const BlockStoreDataReference$json = { + '1': 'BlockStoreDataReference', + '2': [ + {'1': 'block', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'block'}, + ], +}; + +/// Descriptor for `BlockStoreDataReference`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List blockStoreDataReferenceDescriptor = $convert.base64Decode( + 'ChdCbG9ja1N0b3JlRGF0YVJlZmVyZW5jZRImCgVibG9jaxgBIAEoCzIQLnZlaWxpZC5UeXBlZE' + 'tleVIFYmxvY2s='); + @$core.Deprecated('Use dataReferenceDescriptor instead') const DataReference$json = { '1': 'DataReference', '2': [ - {'1': 'dht_data', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '9': 0, '10': 'dhtData'}, + {'1': 'dht_data', '3': 1, '4': 1, '5': 11, '6': '.dht.DHTDataReference', '9': 0, '10': 'dhtData'}, + {'1': 'block_store_data', '3': 2, '4': 1, '5': 11, '6': '.dht.BlockStoreDataReference', '9': 0, '10': 'blockStoreData'}, ], '8': [ {'1': 'kind'}, @@ -73,8 +101,9 @@ const DataReference$json = { /// Descriptor for `DataReference`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List dataReferenceDescriptor = $convert.base64Decode( - 'Cg1EYXRhUmVmZXJlbmNlEi0KCGRodF9kYXRhGAEgASgLMhAudmVpbGlkLlR5cGVkS2V5SABSB2' - 'RodERhdGFCBgoEa2luZA=='); + 'Cg1EYXRhUmVmZXJlbmNlEjIKCGRodF9kYXRhGAEgASgLMhUuZGh0LkRIVERhdGFSZWZlcmVuY2' + 'VIAFIHZGh0RGF0YRJIChBibG9ja19zdG9yZV9kYXRhGAIgASgLMhwuZGh0LkJsb2NrU3RvcmVE' + 'YXRhUmVmZXJlbmNlSABSDmJsb2NrU3RvcmVEYXRhQgYKBGtpbmQ='); @$core.Deprecated('Use ownedDHTRecordPointerDescriptor instead') const OwnedDHTRecordPointer$json = { From c9525bde77b115c5a6646d0c22515e4adabf2af6 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 29 May 2024 16:09:09 -0400 Subject: [PATCH 119/270] more reconciliation --- .../cubits/single_contact_messages_cubit.dart | 139 +++++++++++++++--- 1 file changed, 115 insertions(+), 24 deletions(-) diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 6310af6..ff21d43 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -1,17 +1,29 @@ import 'dart:async'; +import 'dart:collection'; import 'dart:convert'; 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:fixnum/fixnum.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; import '../models/models.dart'; +@immutable +class MessagePosition extends Equatable { + const MessagePosition(this.message, this.pos); + final proto.Message message; + final int pos; + @override + List get props => [message, pos]; +} + class RenderStateElement { RenderStateElement( {required this.message, @@ -191,7 +203,10 @@ class SingleContactMessagesCubit extends Cubit { return; } - _reconcileMessages(sentMessages, _sentMessagesCubit!); + _reconcileMessages( + _activeAccountInfo.localAccount.identityMaster.identityPublicTypedKey(), + sentMessages, + _sentMessagesCubit!); } // Called when the received messages cubit gets a change @@ -201,7 +216,8 @@ class SingleContactMessagesCubit extends Cubit { return; } - _reconcileMessages(rcvdMessages, _rcvdMessagesCubit!); + _reconcileMessages( + _remoteIdentityPublicKey, rcvdMessages, _rcvdMessagesCubit!); } // Called when the reconciled messages window gets a change @@ -269,37 +285,108 @@ class SingleContactMessagesCubit extends Cubit { writer.tryAddAll(messages.map((m) => m.writeToBuffer()).toList())); } - void _reconcileMessages(DHTLogStateData inputMessages, + void _reconcileMessages( + TypedKey author, + DHTLogStateData inputMessages, DHTLogCubit inputMessagesCubit) { singleFuture(_reconciledMessagesCubit!, () async { - // Get the timestamp of our most recent reconciled message - final lastReconciledMessageTs = + // Get the position of our most recent + // reconciled message from this author + // XXX: For a group chat, this should find when the author + // was added to the membership so we don't just go back in time forever + final lastReconciledMessage = await _reconciledMessagesCubit!.operate((arr) async { - final len = arr.length; - if (len == 0) { - return null; - } else { - final lastMessage = - await arr.getProtobuf(proto.Message.fromBuffer, len - 1); - if (lastMessage == null) { + var pos = arr.length - 1; + while (pos >= 0) { + final message = await arr.getProtobuf(proto.Message.fromBuffer, pos); + if (message == null) { throw StateError('should have gotten last message'); } - return lastMessage.timestamp; + if (message.author.toVeilid() == author) { + return MessagePosition(message, pos); + } + pos--; } + return null; }); // Find oldest message we have not yet reconciled + final toReconcile = ListQueue(); - // // Go through all the ones from the cubit state first since we've already - // // gotten them from the DHT - // for (var rn = rcvdMessages.elements.length; rn >= 0; rn--) { - // // - // } + // Go through batches of the input dhtlog starting with + // the current cubit state which is at the tail of the log + // Find the last reconciled message for this author + var currentInputPos = inputMessages.tail; + var currentInputElements = inputMessages.elements; + final inputBatchCount = inputMessages.count; + outer: + while (true) { + for (var rn = currentInputElements.length; + rn >= 0 && currentInputPos >= 0; + rn--, currentInputPos--) { + final elem = currentInputElements[rn]; - // // Add remote messages updates to queue to process asynchronously - // // Ignore offline state because remote messages are always fully delivered - // // This may happen once per client but should be idempotent - // _unreconciledMessagesQueue.addAllSync(rcvdMessages.map((x) => x.value)); + // If we've found an input element that is older than our last + // reconciled message for this author, then we stop + if (lastReconciledMessage != null) { + if (elem.value.timestamp < + lastReconciledMessage.message.timestamp) { + break outer; + } + } + + // Drop the 'offline' elements because we don't reconcile + // anything until it has been confirmed to be committed to the DHT + if (elem.isOffline) { + continue; + } + + // Add to head of reconciliation queue + toReconcile.addFirst(elem.value); + if (toReconcile.length > _maxReconcileChunk) { + toReconcile.removeLast(); + } + } + if (currentInputPos < 0) { + break; + } + + // Get another input batch futher back + final nextInputBatch = await inputMessagesCubit.loadElements( + currentInputPos, inputBatchCount); + final asErr = nextInputBatch.asError; + if (asErr != null) { + emit(AsyncValue.error(asErr.error, asErr.stackTrace)); + return; + } + final asLoading = nextInputBatch.asLoading; + if (asLoading != null) { + // xxx: no need to block the cubit here for this + // xxx: might want to switch to a 'busy' state though + // xxx: to let the messages view show a spinner at the bottom + // xxx: while we reconcile... + // emit(const AsyncValue.loading()); + return; + } + currentInputElements = nextInputBatch.asData!.value; + } + + // Now iterate from our current input position in batches + // and reconcile the messages in the forward direction + var insertPosition = + (lastReconciledMessage != null) ? lastReconciledMessage.pos : 0; + var lastInsertTime = (lastReconciledMessage != null) + ? lastReconciledMessage.message.timestamp + : Int64.ZERO; + + // Insert this batch + xxx expand upon 'res' and iterate batches and update insert position/time + final res = await _reconciledMessagesCubit!.operate((arr) async => + _reconcileMessagesInner( + reconciledArray: arr, + toReconcile: toReconcile, + insertPosition: insertPosition, + lastInsertTime: lastInsertTime)); // Update the view _renderState(); @@ -307,8 +394,10 @@ class SingleContactMessagesCubit extends Cubit { } Future _reconcileMessagesInner( - {required DHTLogWriteOperations reconciledMessagesWriter, - required IList messages}) async { + {required TableDBArray reconciledArray, + required Iterable toReconcile, + required int insertPosition, + required Int64 lastInsertTime}) async { // // Ensure remoteMessages is sorted by timestamp // final newMessages = messages // .sort((a, b) => a.timestamp.compareTo(b.timestamp)) @@ -501,4 +590,6 @@ class SingleContactMessagesCubit extends Cubit { StreamSubscription>? _rcvdSubscription; StreamSubscription>? _reconciledSubscription; + + static const int _maxReconcileChunk = 65536; } From 490051a650ddf08e8f2a3d783572660e013b3a50 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 30 May 2024 23:25:47 -0400 Subject: [PATCH 120/270] reconciliation work --- .../reconciliation/author_input_queue.dart | 145 ++++++++++++ .../reconciliation/author_input_source.dart | 10 + .../message_reconciliation.dart | 206 +++++++++++++++++ .../reconciliation/output_position.dart | 13 ++ .../cubits/reconciliation/reconciliation.dart | 1 + .../cubits/single_contact_messages_cubit.dart | 213 ++---------------- lib/proto/extensions.dart | 3 + .../lib/dht_support/src/dht_log/dht_log.dart | 3 +- .../src/dht_log/dht_log_cubit.dart | 4 +- .../lib/src/table_db_array.dart | 42 ++++ pubspec.lock | 9 + pubspec.yaml | 4 + 12 files changed, 457 insertions(+), 196 deletions(-) create mode 100644 lib/chat/cubits/reconciliation/author_input_queue.dart create mode 100644 lib/chat/cubits/reconciliation/author_input_source.dart create mode 100644 lib/chat/cubits/reconciliation/message_reconciliation.dart create mode 100644 lib/chat/cubits/reconciliation/output_position.dart create mode 100644 lib/chat/cubits/reconciliation/reconciliation.dart diff --git a/lib/chat/cubits/reconciliation/author_input_queue.dart b/lib/chat/cubits/reconciliation/author_input_queue.dart new file mode 100644 index 0000000..b441e75 --- /dev/null +++ b/lib/chat/cubits/reconciliation/author_input_queue.dart @@ -0,0 +1,145 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:math'; + +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../../proto/proto.dart' as proto; + +import 'author_input_source.dart'; +import 'output_position.dart'; + +class AuthorInputQueue { + AuthorInputQueue({ + required this.author, + required this.inputSource, + required this.lastOutputPosition, + required this.onError, + }): + assert(inputSource.messages.count>0, 'no input source window length'), + assert(inputSource.messages.elements.isNotEmpty, 'no input source elements'), + assert(inputSource.messages.tail >= inputSource.messages.elements.length, 'tail is before initial messages end'), + assert(inputSource.messages.tail > 0, 'tail is not greater than zero'), + currentPosition = inputSource.messages.tail, + currentWindow = inputSource.messages.elements, + windowLength = inputSource.messages.count, + windowFirst = inputSource.messages.tail - inputSource.messages.elements.length, + windowLast = inputSource.messages.tail - 1; + + //////////////////////////////////////////////////////////////////////////// + + bool get isEmpty => toReconcile.isEmpty; + + proto.Message? get current => toReconcile.firstOrNull; + + bool consume() { + toReconcile.removeFirst(); + return toReconcile.isNotEmpty; + } + + Future prepareInputQueue() async { + // Go through batches of the input dhtlog starting with + // the current cubit state which is at the tail of the log + // Find the last reconciled message for this author + + outer: + while (true) { + for (var rn = currentWindow.length; + rn >= 0 && currentPosition >= 0; + rn--, currentPosition--) { + final elem = currentWindow[rn]; + + // If we've found an input element that is older than our last + // reconciled message for this author, then we stop + if (lastOutputPosition != null) { + if (elem.value.timestamp < lastOutputPosition!.message.timestamp) { + break outer; + } + } + + // Drop the 'offline' elements because we don't reconcile + // anything until it has been confirmed to be committed to the DHT + if (elem.isOffline) { + continue; + } + + // Add to head of reconciliation queue + toReconcile.addFirst(elem.value); + if (toReconcile.length > _maxQueueChunk) { + toReconcile.removeLast(); + } + } + if (currentPosition < 0) { + break; + } + + xxx update window here and make this and other methods work + } + return true; + } + + // Slide the window toward the current position and load the batch around it + Future updateWindow() async { + + // Check if we are still in the window + if (currentPosition>=windowFirst && currentPosition <= windowLast) { + return true; + } + + // Get the length of the cubit + final inputLength = await inputSource.cubit.operate((r) async => r.length); + + // If not, slide the window + if (currentPosition toReconcile = ListQueue(); + final AuthorInputSource inputSource; + final OutputPosition? lastOutputPosition; + final void Function(Object, StackTrace?) onError; + + // The current position in the input log that we are looking at + int currentPosition; + // The current input window elements + IList> currentWindow; + // The first position of the sliding input window + int windowFirst; + // The last position of the sliding input window + int windowLast; + // Desired maximum window length + int windowLength; + + static const int _maxQueueChunk = 256; +} diff --git a/lib/chat/cubits/reconciliation/author_input_source.dart b/lib/chat/cubits/reconciliation/author_input_source.dart new file mode 100644 index 0000000..75f020e --- /dev/null +++ b/lib/chat/cubits/reconciliation/author_input_source.dart @@ -0,0 +1,10 @@ +import 'package:veilid_support/veilid_support.dart'; + +import '../../../proto/proto.dart' as proto; + +class AuthorInputSource { + AuthorInputSource({required this.messages, required this.cubit}); + + final DHTLogStateData messages; + final DHTLogCubit cubit; +} diff --git a/lib/chat/cubits/reconciliation/message_reconciliation.dart b/lib/chat/cubits/reconciliation/message_reconciliation.dart new file mode 100644 index 0000000..51cfe2b --- /dev/null +++ b/lib/chat/cubits/reconciliation/message_reconciliation.dart @@ -0,0 +1,206 @@ +import 'dart:async'; + +import 'package:async_tools/async_tools.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:sorted_list/sorted_list.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../../proto/proto.dart' as proto; +import 'author_input_queue.dart'; +import 'author_input_source.dart'; +import 'output_position.dart'; + +class MessageReconciliation { + MessageReconciliation( + {required TableDBArrayCubit output, + required void Function(Object, StackTrace?) onError}) + : _outputCubit = output, + _onError = onError; + + //////////////////////////////////////////////////////////////////////////// + + void reconcileMessages( + TypedKey author, + DHTLogStateData inputMessages, + DHTLogCubit inputMessagesCubit) { + if (inputMessages.elements.isEmpty) { + return; + } + + _inputSources[author] = + AuthorInputSource(messages: inputMessages, cubit: inputMessagesCubit); + + singleFuture(this, onError: _onError, () async { + // Take entire list of input sources we have currently and process them + final inputSources = _inputSources; + _inputSources = {}; + + final inputFuts = >[]; + for (final kv in inputSources.entries) { + final author = kv.key; + final inputSource = kv.value; + inputFuts + .add(_enqueueAuthorInput(author: author, inputSource: inputSource)); + } + final inputQueues = await inputFuts.wait; + + // Make this safe to cast by removing inputs that were rejected or empty + inputQueues.removeNulls(); + + // Process all input queues together + await _outputCubit + .operate((reconciledArray) async => _reconcileInputQueues( + reconciledArray: reconciledArray, + inputQueues: inputQueues.cast(), + )); + }); + } + + //////////////////////////////////////////////////////////////////////////// + + // Set up a single author's message reconciliation + Future _enqueueAuthorInput( + {required TypedKey author, + required AuthorInputSource inputSource}) async { + // Get the position of our most recent reconciled message from this author + final lastReconciledMessage = + await _findNewestReconciledMessage(author: author); + + // Find oldest message we have not yet reconciled + final inputQueue = await _buildAuthorInputQueue( + author: author, + inputSource: inputSource, + lastOutputPosition: lastReconciledMessage); + return inputQueue; + } + + // Get the position of our most recent + // reconciled message from this author + // XXX: For a group chat, this should find when the author + // was added to the membership so we don't just go back in time forever + Future _findNewestReconciledMessage( + {required TypedKey author}) async => + _outputCubit.operate((arr) async { + var pos = arr.length - 1; + while (pos >= 0) { + final message = await arr.getProtobuf(proto.Message.fromBuffer, pos); + if (message == null) { + throw StateError('should have gotten last message'); + } + if (message.author.toVeilid() == author) { + return OutputPosition(message, pos); + } + pos--; + } + return null; + }); + + // Find oldest message we have not yet reconciled and build a queue forward + // from that position + Future _buildAuthorInputQueue( + {required TypedKey author, + required AuthorInputSource inputSource, + required OutputPosition? lastOutputPosition}) async { + // Make an author input queue + final authorInputQueue = AuthorInputQueue( + author: author, + inputSource: inputSource, + lastOutputPosition: lastOutputPosition, + onError: _onError); + + if (!await authorInputQueue.prepareInputQueue()) { + return null; + } + + return authorInputQueue; + } + + // Process a list of author input queues and insert their messages + // into the output array, performing validation steps along the way + Future _reconcileInputQueues({ + required TableDBArray reconciledArray, + required List inputQueues, + }) async { + // Ensure queues all have something to do + inputQueues.removeWhere((q) => q.isEmpty); + if (inputQueues.isEmpty) { + return; + } + + // Sort queues from earliest to latest and then by author + // to ensure a deterministic insert order + inputQueues.sort((a, b) { + final acmp = a.lastOutputPosition?.pos ?? -1; + final bcmp = b.lastOutputPosition?.pos ?? -1; + if (acmp == bcmp) { + return a.author.toString().compareTo(b.author.toString()); + } + return acmp.compareTo(bcmp); + }); + + // Start at the earliest position we know about in all the queues + final firstOutputPos = inputQueues.first.lastOutputPosition?.pos; + // Get the timestamp for this output position + var currentOutputMessage = firstOutputPos == null + ? null + : await reconciledArray.getProtobuf( + proto.Message.fromBuffer, firstOutputPos); + + var currentOutputPos = firstOutputPos ?? 0; + + final toInsert = + SortedList(proto.MessageExt.compareTimestamp); + + while (inputQueues.isNotEmpty) { + // Get up to '_maxReconcileChunk' of the items from the queues + // that we can insert at this location + + bool added; + do { + added = false; + var someQueueEmpty = false; + for (final inputQueue in inputQueues) { + final inputCurrent = inputQueue.current!; + if (currentOutputMessage == null || + inputCurrent.timestamp <= currentOutputMessage.timestamp) { + toInsert.add(inputCurrent); + added = true; + + // Advance this queue + if (!inputQueue.consume()) { + // Queue is empty now, run a queue purge + someQueueEmpty = true; + } + } + } + // Remove empty queues now that we're done iterating + if (someQueueEmpty) { + inputQueues.removeWhere((q) => q.isEmpty); + } + + if (toInsert.length >= _maxReconcileChunk) { + break; + } + } while (added); + + // Perform insertions in bulk + if (toInsert.isNotEmpty) { + await reconciledArray.insertAllProtobuf(currentOutputPos, toInsert); + toInsert.clear(); + } else { + // If there's nothing to insert at this position move to the next one + currentOutputPos++; + currentOutputMessage = await reconciledArray.getProtobuf( + proto.Message.fromBuffer, currentOutputPos); + } + } + } + + //////////////////////////////////////////////////////////////////////////// + + Map _inputSources = {}; + final TableDBArrayCubit _outputCubit; + final void Function(Object, StackTrace?) _onError; + + static const int _maxReconcileChunk = 65536; +} diff --git a/lib/chat/cubits/reconciliation/output_position.dart b/lib/chat/cubits/reconciliation/output_position.dart new file mode 100644 index 0000000..258259e --- /dev/null +++ b/lib/chat/cubits/reconciliation/output_position.dart @@ -0,0 +1,13 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +import '../../../proto/proto.dart' as proto; + +@immutable +class OutputPosition extends Equatable { + const OutputPosition(this.message, this.pos); + final proto.Message message; + final int pos; + @override + List get props => [message, pos]; +} diff --git a/lib/chat/cubits/reconciliation/reconciliation.dart b/lib/chat/cubits/reconciliation/reconciliation.dart new file mode 100644 index 0000000..2dc0b93 --- /dev/null +++ b/lib/chat/cubits/reconciliation/reconciliation.dart @@ -0,0 +1 @@ +export 'message_reconciliation.dart'; diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index ff21d43..6c4fd8f 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -1,28 +1,16 @@ import 'dart:async'; -import 'dart:collection'; import 'dart:convert'; 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:fixnum/fixnum.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; import '../models/models.dart'; - -@immutable -class MessagePosition extends Equatable { - const MessagePosition(this.message, this.pos); - final proto.Message message; - final int pos; - @override - List get props => [message, pos]; -} +import 'message_reconciliation.dart'; class RenderStateElement { RenderStateElement( @@ -165,6 +153,13 @@ class SingleContactMessagesCubit extends Cubit { _reconciledMessagesCubit = TableDBArrayCubit( open: () async => TableDBArray.make(table: tableName, crypto: crypto), decodeElement: proto.ReconciledMessage.fromBuffer); + + _reconciliation = MessageReconciliation( + output: _reconciledMessagesCubit!, + onError: (e, st) { + emit(AsyncValue.error(e, st)); + }); + _reconciledSubscription = _reconciledMessagesCubit!.stream.listen(_updateReconciledMessagesState); _updateReconciledMessagesState(_reconciledMessagesCubit!.state); @@ -203,7 +198,7 @@ class SingleContactMessagesCubit extends Cubit { return; } - _reconcileMessages( + _reconciliation.reconcileMessages( _activeAccountInfo.localAccount.identityMaster.identityPublicTypedKey(), sentMessages, _sentMessagesCubit!); @@ -216,7 +211,7 @@ class SingleContactMessagesCubit extends Cubit { return; } - _reconcileMessages( + _reconciliation.reconcileMessages( _remoteIdentityPublicKey, rcvdMessages, _rcvdMessagesCubit!); } @@ -246,6 +241,12 @@ class SingleContactMessagesCubit extends Cubit { message.signature = signature.toProto(); } + Future _generateInitialId( + {required PublicKey identityPublicKey}) async => + (await _localMessagesCryptoSystem + .generateHash(identityPublicKey.decode())) + .decode(); + Future _processMessageToSend( proto.Message message, proto.Message? previousMessage) async { // Get the previous message if we don't have one @@ -257,10 +258,9 @@ class SingleContactMessagesCubit extends Cubit { if (previousMessage == null) { // If there's no last sent message, // we start at a hash of the identity public key - message.id = (await _localMessagesCryptoSystem.generateHash( - _activeAccountInfo.localAccount.identityMaster.identityPublicKey - .decode())) - .decode(); + message.id = await _generateInitialId( + identityPublicKey: + _activeAccountInfo.localAccount.identityMaster.identityPublicKey); } else { // If there is a last message, we generate the hash // of the last message's signature and use it as our next id @@ -285,177 +285,6 @@ class SingleContactMessagesCubit extends Cubit { writer.tryAddAll(messages.map((m) => m.writeToBuffer()).toList())); } - void _reconcileMessages( - TypedKey author, - DHTLogStateData inputMessages, - DHTLogCubit inputMessagesCubit) { - singleFuture(_reconciledMessagesCubit!, () async { - // Get the position of our most recent - // reconciled message from this author - // XXX: For a group chat, this should find when the author - // was added to the membership so we don't just go back in time forever - final lastReconciledMessage = - await _reconciledMessagesCubit!.operate((arr) async { - var pos = arr.length - 1; - while (pos >= 0) { - final message = await arr.getProtobuf(proto.Message.fromBuffer, pos); - if (message == null) { - throw StateError('should have gotten last message'); - } - if (message.author.toVeilid() == author) { - return MessagePosition(message, pos); - } - pos--; - } - return null; - }); - - // Find oldest message we have not yet reconciled - final toReconcile = ListQueue(); - - // Go through batches of the input dhtlog starting with - // the current cubit state which is at the tail of the log - // Find the last reconciled message for this author - var currentInputPos = inputMessages.tail; - var currentInputElements = inputMessages.elements; - final inputBatchCount = inputMessages.count; - outer: - while (true) { - for (var rn = currentInputElements.length; - rn >= 0 && currentInputPos >= 0; - rn--, currentInputPos--) { - final elem = currentInputElements[rn]; - - // If we've found an input element that is older than our last - // reconciled message for this author, then we stop - if (lastReconciledMessage != null) { - if (elem.value.timestamp < - lastReconciledMessage.message.timestamp) { - break outer; - } - } - - // Drop the 'offline' elements because we don't reconcile - // anything until it has been confirmed to be committed to the DHT - if (elem.isOffline) { - continue; - } - - // Add to head of reconciliation queue - toReconcile.addFirst(elem.value); - if (toReconcile.length > _maxReconcileChunk) { - toReconcile.removeLast(); - } - } - if (currentInputPos < 0) { - break; - } - - // Get another input batch futher back - final nextInputBatch = await inputMessagesCubit.loadElements( - currentInputPos, inputBatchCount); - final asErr = nextInputBatch.asError; - if (asErr != null) { - emit(AsyncValue.error(asErr.error, asErr.stackTrace)); - return; - } - final asLoading = nextInputBatch.asLoading; - if (asLoading != null) { - // xxx: no need to block the cubit here for this - // xxx: might want to switch to a 'busy' state though - // xxx: to let the messages view show a spinner at the bottom - // xxx: while we reconcile... - // emit(const AsyncValue.loading()); - return; - } - currentInputElements = nextInputBatch.asData!.value; - } - - // Now iterate from our current input position in batches - // and reconcile the messages in the forward direction - var insertPosition = - (lastReconciledMessage != null) ? lastReconciledMessage.pos : 0; - var lastInsertTime = (lastReconciledMessage != null) - ? lastReconciledMessage.message.timestamp - : Int64.ZERO; - - // Insert this batch - xxx expand upon 'res' and iterate batches and update insert position/time - final res = await _reconciledMessagesCubit!.operate((arr) async => - _reconcileMessagesInner( - reconciledArray: arr, - toReconcile: toReconcile, - insertPosition: insertPosition, - lastInsertTime: lastInsertTime)); - - // Update the view - _renderState(); - }); - } - - Future _reconcileMessagesInner( - {required TableDBArray reconciledArray, - required Iterable toReconcile, - required int insertPosition, - required Int64 lastInsertTime}) 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 = await reconciledMessagesWriter - // .getItemRangeProtobuf(proto.Message.fromBuffer, 0); - // if (existingMessages == null) { - // throw Exception( - // 'Could not load existing reconciled messages at this time'); - // } - - // 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 reconciledMessagesWriter.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 reconciledMessagesWriter.tryAddItem(newMessage.writeToBuffer()); - // // Insert into local copy as well for this operation - // existingMessages.add(newMessage); - - // nPos++; - // } - } - // Produce a state for this cubit from the input cubits and queues void _renderState() { // Get all reconciled messages @@ -584,12 +413,12 @@ class SingleContactMessagesCubit extends Cubit { DHTLogCubit? _rcvdMessagesCubit; TableDBArrayCubit? _reconciledMessagesCubit; + late final MessageReconciliation _reconciliation; + late final PersistentQueue _sendingMessagesQueue; StreamSubscription>? _sentSubscription; StreamSubscription>? _rcvdSubscription; StreamSubscription>? _reconciledSubscription; - - static const int _maxReconcileChunk = 65536; } diff --git a/lib/proto/extensions.dart b/lib/proto/extensions.dart index e9fd9a2..1da55f9 100644 --- a/lib/proto/extensions.dart +++ b/lib/proto/extensions.dart @@ -23,4 +23,7 @@ extension MessageExt on proto.Message { } String get uniqueIdString => base64UrlNoPadEncode(uniqueIdBytes); + + static int compareTimestamp(proto.Message a, proto.Message b) => + a.timestamp.compareTo(b.timestamp); } diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart index acdc6fe..985b11f 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart @@ -209,8 +209,7 @@ class DHTLog implements DHTDeleteable { OwnedDHTRecordPointer get recordPointer => _spine.recordPointer; /// Runs a closure allowing read-only access to the log - Future operate( - Future Function(DHTLogReadOperations) closure) async { + Future operate(Future Function(DHTLogReadOperations) closure) async { if (!isOpen) { throw StateError('log is not open"'); } diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart index 010c76e..2f97b3f 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart @@ -108,6 +108,7 @@ class DHTLogCubit extends Cubit> elements: elements, tail: _tail, count: _count, follow: _follow))); } + // Tail is one past the last element to load Future>>> loadElements( int tail, int count, {bool forceRefresh = false}) async { @@ -184,8 +185,7 @@ class DHTLogCubit extends Cubit> await super.close(); } - Future operate( - Future Function(DHTLogReadOperations) closure) async { + Future operate(Future Function(DHTLogReadOperations) closure) async { await _initWait(); return _log.operate(closure); } diff --git a/packages/veilid_support/lib/src/table_db_array.dart b/packages/veilid_support/lib/src/table_db_array.dart index 4d5b9dd..8bcd146 100644 --- a/packages/veilid_support/lib/src/table_db_array.dart +++ b/packages/veilid_support/lib/src/table_db_array.dart @@ -662,4 +662,46 @@ extension TableDBArrayExt on TableDBArray { T Function(List) fromBuffer, int start, [int? end]) => getRange(start, end ?? _length) .then((out) => out.map(fromBuffer).toList()); + + /// Convenience function: + /// Like add but for a JSON value + Future addJson(T value) async => add(jsonEncodeBytes(value)); + + /// Convenience function: + /// Like add but for a Protobuf value + Future addProtobuf(T value) => + add(value.writeToBuffer()); + + /// Convenience function: + /// Like addAll but for a JSON value + Future addAllJson(List values) async => + addAll(values.map(jsonEncodeBytes).toList()); + + /// Convenience function: + /// Like addAll but for a Protobuf value + Future addAllProtobuf( + List values) async => + addAll(values.map((x) => x.writeToBuffer()).toList()); + + /// Convenience function: + /// Like insert but for a JSON value + Future insertJson(int pos, T value) async => + insert(pos, jsonEncodeBytes(value)); + + /// Convenience function: + /// Like insert but for a Protobuf value + Future insertProtobuf( + int pos, T value) async => + insert(pos, value.writeToBuffer()); + + /// Convenience function: + /// Like insertAll but for a JSON value + Future insertAllJson(int pos, List values) async => + insertAll(pos, values.map(jsonEncodeBytes).toList()); + + /// Convenience function: + /// Like insertAll but for a Protobuf value + Future insertAllProtobuf( + int pos, List values) async => + insertAll(pos, values.map((x) => x.writeToBuffer()).toList()); } diff --git a/pubspec.lock b/pubspec.lock index c6e754b..8a70f22 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1219,6 +1219,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + sorted_list: + dependency: "direct main" + description: + path: "." + ref: main + resolved-ref: "090eb9be48ab85ff064a0a1d8175b4a72d79b139" + url: "https://gitlab.com/veilid/dart-sorted-list-improved.git" + source: git + version: "1.0.0" source_gen: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1cc893f..133d482 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -69,6 +69,10 @@ dependencies: share_plus: ^9.0.0 shared_preferences: ^2.2.3 signal_strength_indicator: ^0.4.1 + sorted_list: + git: + url: https://gitlab.com/veilid/dart-sorted-list-improved.git + ref: main split_view: ^3.2.1 stack_trace: ^1.11.1 stream_transform: ^2.1.0 From fd63a0d5e01dc48bffec66fb67670afd84a40aee Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 31 May 2024 18:27:50 -0400 Subject: [PATCH 121/270] message integrity --- .../reconciliation/author_input_queue.dart | 268 ++++++++++-------- .../reconciliation/author_input_source.dart | 76 ++++- .../reconciliation/message_integrity.dart | 74 +++++ .../message_reconciliation.dart | 51 ++-- .../cubits/reconciliation/reconciliation.dart | 1 + .../cubits/single_contact_messages_cubit.dart | 71 ++--- lib/chat/views/chat_component.dart | 2 +- lib/proto/extensions.dart | 6 +- .../src/dht_log/dht_log_cubit.dart | 45 ++- .../lib/src/online_element_state.dart | 12 + .../veilid_support/lib/veilid_support.dart | 1 + 11 files changed, 370 insertions(+), 237 deletions(-) create mode 100644 lib/chat/cubits/reconciliation/message_integrity.dart create mode 100644 packages/veilid_support/lib/src/online_element_state.dart diff --git a/lib/chat/cubits/reconciliation/author_input_queue.dart b/lib/chat/cubits/reconciliation/author_input_queue.dart index b441e75..b9fd7d7 100644 --- a/lib/chat/cubits/reconciliation/author_input_queue.dart +++ b/lib/chat/cubits/reconciliation/author_input_queue.dart @@ -1,145 +1,191 @@ import 'dart:async'; -import 'dart:collection'; -import 'dart:math'; - -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../../proto/proto.dart' as proto; import 'author_input_source.dart'; +import 'message_integrity.dart'; import 'output_position.dart'; class AuthorInputQueue { - AuthorInputQueue({ - required this.author, - required this.inputSource, - required this.lastOutputPosition, - required this.onError, - }): - assert(inputSource.messages.count>0, 'no input source window length'), - assert(inputSource.messages.elements.isNotEmpty, 'no input source elements'), - assert(inputSource.messages.tail >= inputSource.messages.elements.length, 'tail is before initial messages end'), - assert(inputSource.messages.tail > 0, 'tail is not greater than zero'), - currentPosition = inputSource.messages.tail, - currentWindow = inputSource.messages.elements, - windowLength = inputSource.messages.count, - windowFirst = inputSource.messages.tail - inputSource.messages.elements.length, - windowLast = inputSource.messages.tail - 1; + AuthorInputQueue._({ + required TypedKey author, + required AuthorInputSource inputSource, + required OutputPosition? outputPosition, + required void Function(Object, StackTrace?) onError, + required MessageIntegrity messageIntegrity, + }) : _author = author, + _onError = onError, + _inputSource = inputSource, + _outputPosition = outputPosition, + _lastMessage = outputPosition?.message, + _messageIntegrity = messageIntegrity, + _currentPosition = inputSource.currentWindow.last; - //////////////////////////////////////////////////////////////////////////// - - bool get isEmpty => toReconcile.isEmpty; - - proto.Message? get current => toReconcile.firstOrNull; - - bool consume() { - toReconcile.removeFirst(); - return toReconcile.isNotEmpty; + static Future create({ + required TypedKey author, + required AuthorInputSource inputSource, + required OutputPosition? outputPosition, + required void Function(Object, StackTrace?) onError, + }) async { + final queue = AuthorInputQueue._( + author: author, + inputSource: inputSource, + outputPosition: outputPosition, + onError: onError, + messageIntegrity: await MessageIntegrity.create(author: author)); + if (!await queue._findStartOfWork()) { + return null; + } + return queue; } - Future prepareInputQueue() async { - // Go through batches of the input dhtlog starting with - // the current cubit state which is at the tail of the log - // Find the last reconciled message for this author + //////////////////////////////////////////////////////////////////////////// + // Public interface - outer: + // Check if there are no messages in this queue to reconcile + bool get isEmpty => _currentMessage == null; + + // Get the current message that needs reconciliation + proto.Message? get current => _currentMessage; + + // Get the earliest output position to start inserting + OutputPosition? get outputPosition => _outputPosition; + + // Get the author of this queue + TypedKey get author => _author; + + // Remove a reconciled message and move to the next message + // Returns true if there is more work to do + Future consume() async { while (true) { - for (var rn = currentWindow.length; - rn >= 0 && currentPosition >= 0; - rn--, currentPosition--) { - final elem = currentWindow[rn]; + _lastMessage = _currentMessage; - // If we've found an input element that is older than our last - // reconciled message for this author, then we stop - if (lastOutputPosition != null) { - if (elem.value.timestamp < lastOutputPosition!.message.timestamp) { - break outer; - } - } + _currentPosition++; - // Drop the 'offline' elements because we don't reconcile - // anything until it has been confirmed to be committed to the DHT - if (elem.isOffline) { + // Get more window if we need to + if (!await _updateWindow()) { + // Window is not available so this queue can't work right now + return false; + } + final nextMessage = _inputSource.currentWindow + .elements[_currentPosition - _inputSource.currentWindow.first]; + + // Drop the 'offline' elements because we don't reconcile + // anything until it has been confirmed to be committed to the DHT + if (nextMessage.isOffline) { + continue; + } + + if (_lastMessage != null) { + // Ensure the timestamp is not moving backward + if (nextMessage.value.timestamp < _lastMessage!.timestamp) { continue; } - - // Add to head of reconciliation queue - toReconcile.addFirst(elem.value); - if (toReconcile.length > _maxQueueChunk) { - toReconcile.removeLast(); - } - } - if (currentPosition < 0) { - break; } - xxx update window here and make this and other methods work + // Verify the id chain for the message + final matchId = await _messageIntegrity.generateMessageId(_lastMessage); + if (matchId.compare(nextMessage.value.idBytes) != 0) { + continue; + } + + // Verify the signature for the message + if (!await _messageIntegrity.verifyMessage(nextMessage.value)) { + continue; + } + + _currentMessage = nextMessage.value; + break; } return true; } + //////////////////////////////////////////////////////////////////////////// + // Internal implementation + + // Walk backward from the tail of the input queue to find the first + // message newer than our last reconcicled message from this author + // Returns false if no work is needed + Future _findStartOfWork() async { + // Iterate windows over the inputSource + outer: + while (true) { + // Iterate through current window backward + for (var i = _inputSource.currentWindow.elements.length; + i >= 0 && _currentPosition >= 0; + i--, _currentPosition--) { + final elem = _inputSource.currentWindow.elements[i]; + + // If we've found an input element that is older than our last + // reconciled message for this author, then we stop + if (_lastMessage != null) { + if (elem.value.timestamp < _lastMessage!.timestamp) { + break outer; + } + } + } + // If we're at the beginning of the inputSource then we stop + if (_currentPosition < 0) { + break; + } + + // Get more window if we need to + if (!await _updateWindow()) { + // Window is not available or things are empty so this + // queue can't work right now + return false; + } + } + + // The current position should be equal to the first message to process + // and the current window to process should not be empty + return _inputSource.currentWindow.elements.isNotEmpty; + } + // Slide the window toward the current position and load the batch around it - Future updateWindow() async { - - // Check if we are still in the window - if (currentPosition>=windowFirst && currentPosition <= windowLast) { - return true; - } - - // Get the length of the cubit - final inputLength = await inputSource.cubit.operate((r) async => r.length); - - // If not, slide the window - if (currentPosition _updateWindow() async { + // Check if we are still in the window + if (_currentPosition >= _inputSource.currentWindow.first && + _currentPosition <= _inputSource.currentWindow.last) { return true; + } + + // Get another input batch futher back + final avOk = + await _inputSource.updateWindow(_currentPosition, _maxWindowLength); + + final asErr = avOk.asError; + if (asErr != null) { + _onError(asErr.error, asErr.stackTrace); + return false; + } + final asLoading = avOk.asLoading; + if (asLoading != null) { + // xxx: no need to block the cubit here for this + // xxx: might want to switch to a 'busy' state though + // xxx: to let the messages view show a spinner at the bottom + // xxx: while we reconcile... + // emit(const AsyncValue.loading()); + return false; + } + return avOk.asData!.value; } //////////////////////////////////////////////////////////////////////////// - final TypedKey author; - final ListQueue toReconcile = ListQueue(); - final AuthorInputSource inputSource; - final OutputPosition? lastOutputPosition; - final void Function(Object, StackTrace?) onError; + final TypedKey _author; + final AuthorInputSource _inputSource; + final OutputPosition? _outputPosition; + final void Function(Object, StackTrace?) _onError; + final MessageIntegrity _messageIntegrity; + // The last message we've consumed + proto.Message? _lastMessage; // The current position in the input log that we are looking at - int currentPosition; - // The current input window elements - IList> currentWindow; - // The first position of the sliding input window - int windowFirst; - // The last position of the sliding input window - int windowLast; + int _currentPosition; + // The current message we're looking at + proto.Message? _currentMessage; // Desired maximum window length - int windowLength; - - static const int _maxQueueChunk = 256; + static const int _maxWindowLength = 256; } diff --git a/lib/chat/cubits/reconciliation/author_input_source.dart b/lib/chat/cubits/reconciliation/author_input_source.dart index 75f020e..1f67264 100644 --- a/lib/chat/cubits/reconciliation/author_input_source.dart +++ b/lib/chat/cubits/reconciliation/author_input_source.dart @@ -1,10 +1,76 @@ +import 'dart:math'; + +import 'package:async_tools/async_tools.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:meta/meta.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../../proto/proto.dart' as proto; -class AuthorInputSource { - AuthorInputSource({required this.messages, required this.cubit}); - - final DHTLogStateData messages; - final DHTLogCubit cubit; +@immutable +class InputWindow { + const InputWindow( + {required this.elements, required this.first, required this.last}); + final IList> elements; + final int first; + final int last; +} + +class AuthorInputSource { + AuthorInputSource.fromCubit( + {required DHTLogStateData cubitState, + required this.cubit}) { + _currentWindow = InputWindow( + elements: cubitState.elements, + first: cubitState.tail - cubitState.elements.length, + last: cubitState.tail - 1); + } + + //////////////////////////////////////////////////////////////////////////// + + InputWindow get currentWindow => _currentWindow; + + Future> updateWindow( + int currentPosition, int windowLength) async => + cubit.operate((reader) async { + // See if we're beyond the input source + if (currentPosition < 0 || currentPosition >= reader.length) { + return const AsyncValue.data(false); + } + + // Slide the window if we need to + var first = _currentWindow.first; + var last = _currentWindow.last; + if (currentPosition < first) { + // Slide it backward, current position is now last + first = max((currentPosition - windowLength) + 1, 0); + last = currentPosition; + } else if (currentPosition > last) { + // Slide it forward, current position is now first + first = currentPosition; + last = min((currentPosition + windowLength) - 1, reader.length - 1); + } else { + return const AsyncValue.data(true); + } + + // Get another input batch futher back + final nextWindow = await cubit.loadElementsFromReader( + reader, last + 1, (last + 1) - first); + final asErr = nextWindow.asError; + if (asErr != null) { + return AsyncValue.error(asErr.error, asErr.stackTrace); + } + final asLoading = nextWindow.asLoading; + if (asLoading != null) { + return const AsyncValue.loading(); + } + _currentWindow = InputWindow( + elements: nextWindow.asData!.value, first: first, last: last); + return const AsyncValue.data(true); + }); + + //////////////////////////////////////////////////////////////////////////// + final DHTLogCubit cubit; + + late InputWindow _currentWindow; } diff --git a/lib/chat/cubits/reconciliation/message_integrity.dart b/lib/chat/cubits/reconciliation/message_integrity.dart new file mode 100644 index 0000000..2fd1956 --- /dev/null +++ b/lib/chat/cubits/reconciliation/message_integrity.dart @@ -0,0 +1,74 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:protobuf/protobuf.dart'; +import 'package:veilid_support/veilid_support.dart'; +import '../../../proto/proto.dart' as proto; + +class MessageIntegrity { + MessageIntegrity._({ + required TypedKey author, + required VeilidCryptoSystem crypto, + }) : _author = author, + _crypto = crypto; + static Future create({required TypedKey author}) async { + final crypto = await Veilid.instance.getCryptoSystem(author.kind); + return MessageIntegrity._(author: author, crypto: crypto); + } + + //////////////////////////////////////////////////////////////////////////// + // Public interface + + Future generateMessageId(proto.Message? previous) async { + if (previous == null) { + // If there's no last sent message, + // we start at a hash of the identity public key + return _generateInitialId(); + } else { + // If there is a last message, we generate the hash + // of the last message's signature and use it as our next id + return _hashSignature(previous.signature); + } + } + + Future signMessage( + proto.Message message, + SecretKey authorSecret, + ) async { + // Ensure this message is not already signed + assert(!message.hasSignature(), 'should not sign message twice'); + // Generate data to sign + final data = Uint8List.fromList(utf8.encode(message.writeToJson())); + + // Sign with our identity + final signature = await _crypto.sign(_author.value, authorSecret, data); + + // Add to the message + message.signature = signature.toProto(); + } + + Future verifyMessage(proto.Message message) async { + // Ensure the message is signed + assert(message.hasSignature(), 'should not verify unsigned message'); + final signature = message.signature.toVeilid(); + + // Generate data to sign + final messageNoSig = message.deepCopy()..clearSignature(); + final data = Uint8List.fromList(utf8.encode(messageNoSig.writeToJson())); + + // Verify signature + return _crypto.verify(_author.value, data, signature); + } + + //////////////////////////////////////////////////////////////////////////// + // Private implementation + + Future _generateInitialId() async => + (await _crypto.generateHash(_author.decode())).decode(); + + Future _hashSignature(proto.Signature signature) async => + (await _crypto.generateHash(signature.toVeilid().decode())).decode(); + //////////////////////////////////////////////////////////////////////////// + final TypedKey _author; + final VeilidCryptoSystem _crypto; +} diff --git a/lib/chat/cubits/reconciliation/message_reconciliation.dart b/lib/chat/cubits/reconciliation/message_reconciliation.dart index 51cfe2b..1687f4d 100644 --- a/lib/chat/cubits/reconciliation/message_reconciliation.dart +++ b/lib/chat/cubits/reconciliation/message_reconciliation.dart @@ -21,14 +21,14 @@ class MessageReconciliation { void reconcileMessages( TypedKey author, - DHTLogStateData inputMessages, + DHTLogStateData inputMessagesCubitState, DHTLogCubit inputMessagesCubit) { - if (inputMessages.elements.isEmpty) { + if (inputMessagesCubitState.elements.isEmpty) { return; } - _inputSources[author] = - AuthorInputSource(messages: inputMessages, cubit: inputMessagesCubit); + _inputSources[author] = AuthorInputSource.fromCubit( + cubitState: inputMessagesCubitState, cubit: inputMessagesCubit); singleFuture(this, onError: _onError, () async { // Take entire list of input sources we have currently and process them @@ -63,14 +63,15 @@ class MessageReconciliation { {required TypedKey author, required AuthorInputSource inputSource}) async { // Get the position of our most recent reconciled message from this author - final lastReconciledMessage = - await _findNewestReconciledMessage(author: author); + final outputPosition = await _findLastOutputPosition(author: author); // Find oldest message we have not yet reconciled - final inputQueue = await _buildAuthorInputQueue( - author: author, - inputSource: inputSource, - lastOutputPosition: lastReconciledMessage); + final inputQueue = await AuthorInputQueue.create( + author: author, + inputSource: inputSource, + outputPosition: outputPosition, + onError: _onError, + ); return inputQueue; } @@ -78,7 +79,7 @@ class MessageReconciliation { // reconciled message from this author // XXX: For a group chat, this should find when the author // was added to the membership so we don't just go back in time forever - Future _findNewestReconciledMessage( + Future _findLastOutputPosition( {required TypedKey author}) async => _outputCubit.operate((arr) async { var pos = arr.length - 1; @@ -95,26 +96,6 @@ class MessageReconciliation { return null; }); - // Find oldest message we have not yet reconciled and build a queue forward - // from that position - Future _buildAuthorInputQueue( - {required TypedKey author, - required AuthorInputSource inputSource, - required OutputPosition? lastOutputPosition}) async { - // Make an author input queue - final authorInputQueue = AuthorInputQueue( - author: author, - inputSource: inputSource, - lastOutputPosition: lastOutputPosition, - onError: _onError); - - if (!await authorInputQueue.prepareInputQueue()) { - return null; - } - - return authorInputQueue; - } - // Process a list of author input queues and insert their messages // into the output array, performing validation steps along the way Future _reconcileInputQueues({ @@ -130,8 +111,8 @@ class MessageReconciliation { // Sort queues from earliest to latest and then by author // to ensure a deterministic insert order inputQueues.sort((a, b) { - final acmp = a.lastOutputPosition?.pos ?? -1; - final bcmp = b.lastOutputPosition?.pos ?? -1; + final acmp = a.outputPosition?.pos ?? -1; + final bcmp = b.outputPosition?.pos ?? -1; if (acmp == bcmp) { return a.author.toString().compareTo(b.author.toString()); } @@ -139,7 +120,7 @@ class MessageReconciliation { }); // Start at the earliest position we know about in all the queues - final firstOutputPos = inputQueues.first.lastOutputPosition?.pos; + final firstOutputPos = inputQueues.first.outputPosition?.pos; // Get the timestamp for this output position var currentOutputMessage = firstOutputPos == null ? null @@ -167,7 +148,7 @@ class MessageReconciliation { added = true; // Advance this queue - if (!inputQueue.consume()) { + if (!await inputQueue.consume()) { // Queue is empty now, run a queue purge someQueueEmpty = true; } diff --git a/lib/chat/cubits/reconciliation/reconciliation.dart b/lib/chat/cubits/reconciliation/reconciliation.dart index 2dc0b93..a8187cf 100644 --- a/lib/chat/cubits/reconciliation/reconciliation.dart +++ b/lib/chat/cubits/reconciliation/reconciliation.dart @@ -1 +1,2 @@ +export 'message_integrity.dart'; export 'message_reconciliation.dart'; diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 6c4fd8f..c444134 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -1,6 +1,4 @@ import 'dart:async'; -import 'dart:convert'; -import 'dart:typed_data'; import 'package:async_tools/async_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; @@ -10,7 +8,7 @@ import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../proto/proto.dart' as proto; import '../models/models.dart'; -import 'message_reconciliation.dart'; +import 'reconciliation/reconciliation.dart'; class RenderStateElement { RenderStateElement( @@ -102,12 +100,11 @@ class SingleContactMessagesCubit extends Cubit { // Make crypto Future _initCrypto() async { - _messagesCrypto = await _activeAccountInfo + _conversationCrypto = await _activeAccountInfo .makeConversationCrypto(_remoteIdentityPublicKey); - _localMessagesCryptoSystem = - await Veilid.instance.getCryptoSystem(_localMessagesRecordKey.kind); - _identityCryptoSystem = - await _activeAccountInfo.localAccount.identityMaster.identityCrypto; + _senderMessageIntegrity = await MessageIntegrity.create( + author: _activeAccountInfo.localAccount.identityMaster + .identityPublicTypedKey()); } // Open local messages key @@ -119,7 +116,7 @@ class SingleContactMessagesCubit extends Cubit { debugName: 'SingleContactMessagesCubit::_initSentMessagesCubit::' 'SentMessages', parent: _localConversationRecordKey, - crypto: _messagesCrypto), + crypto: _conversationCrypto), decodeElement: proto.Message.fromBuffer); _sentSubscription = _sentMessagesCubit!.stream.listen(_updateSentMessagesState); @@ -133,7 +130,7 @@ class SingleContactMessagesCubit extends Cubit { debugName: 'SingleContactMessagesCubit::_initRcvdMessagesCubit::' 'RcvdMessages', parent: _remoteConversationRecordKey, - crypto: _messagesCrypto), + crypto: _conversationCrypto), decodeElement: proto.Message.fromBuffer); _rcvdSubscription = _rcvdMessagesCubit!.stream.listen(_updateRcvdMessagesState); @@ -222,31 +219,6 @@ class SingleContactMessagesCubit extends Cubit { _renderState(); } - Future _hashSignature(proto.Signature signature) async => - (await _localMessagesCryptoSystem - .generateHash(signature.toVeilid().decode())) - .decode(); - - Future _signMessage(proto.Message message) async { - // Generate data to sign - final data = Uint8List.fromList(utf8.encode(message.writeToJson())); - - // Sign with our identity - final signature = await _identityCryptoSystem.sign( - _activeAccountInfo.localAccount.identityMaster.identityPublicKey, - _activeAccountInfo.userLogin.identitySecret.value, - data); - - // Add to the message - message.signature = signature.toProto(); - } - - Future _generateInitialId( - {required PublicKey identityPublicKey}) async => - (await _localMessagesCryptoSystem - .generateHash(identityPublicKey.decode())) - .decode(); - Future _processMessageToSend( proto.Message message, proto.Message? previousMessage) async { // Get the previous message if we don't have one @@ -255,20 +227,12 @@ class SingleContactMessagesCubit extends Cubit { ? null : await r.getProtobuf(proto.Message.fromBuffer, r.length - 1)); - if (previousMessage == null) { - // If there's no last sent message, - // we start at a hash of the identity public key - message.id = await _generateInitialId( - identityPublicKey: - _activeAccountInfo.localAccount.identityMaster.identityPublicKey); - } else { - // If there is a last message, we generate the hash - // of the last message's signature and use it as our next id - message.id = await _hashSignature(previousMessage.signature); - } + message.id = + await _senderMessageIntegrity.generateMessageId(previousMessage); // Now sign it - await _signMessage(message); + await _senderMessageIntegrity.signMessage( + message, _activeAccountInfo.userLogin.identitySecret.value); } // Async process to send messages in the background @@ -303,17 +267,17 @@ class SingleContactMessagesCubit extends Cubit { // Generate state for each message final sentMessagesMap = - IMap>.fromValues( - keyMapper: (x) => x.value.uniqueIdString, + IMap>.fromValues( + keyMapper: (x) => x.value.authorUniqueIdString, values: sentMessages.elements, ); final reconciledMessagesMap = IMap.fromValues( - keyMapper: (x) => x.content.uniqueIdString, + keyMapper: (x) => x.content.authorUniqueIdString, values: reconciledMessages.elements, ); final sendingMessagesMap = IMap.fromValues( - keyMapper: (x) => x.uniqueIdString, + keyMapper: (x) => x.authorUniqueIdString, values: sendingMessages, ); @@ -405,9 +369,8 @@ class SingleContactMessagesCubit extends Cubit { final TypedKey _remoteConversationRecordKey; final TypedKey _remoteMessagesRecordKey; - late final VeilidCrypto _messagesCrypto; - late final VeilidCryptoSystem _localMessagesCryptoSystem; - late final VeilidCryptoSystem _identityCryptoSystem; + late final VeilidCrypto _conversationCrypto; + late final MessageIntegrity _senderMessageIntegrity; DHTLogCubit? _sentMessagesCubit; DHTLogCubit? _rcvdMessagesCubit; diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 7f549cb..1e296e9 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -127,7 +127,7 @@ class ChatComponent extends StatelessWidget { author: isLocal ? _localUser : _remoteUser, createdAt: (message.sentTimestamp.value ~/ BigInt.from(1000)).toInt(), - id: message.content.uniqueIdString, + id: message.content.authorUniqueIdString, text: contextText.text, showStatus: status != null, status: status); diff --git a/lib/proto/extensions.dart b/lib/proto/extensions.dart index 1da55f9..25b8558 100644 --- a/lib/proto/extensions.dart +++ b/lib/proto/extensions.dart @@ -16,13 +16,15 @@ Map reconciledMessageToJson(proto.ReconciledMessage m) => m.writeToJsonMap(); extension MessageExt on proto.Message { - Uint8List get uniqueIdBytes { + Uint8List get idBytes => Uint8List.fromList(id); + + Uint8List get authorUniqueIdBytes { final author = this.author.toVeilid().decode(); final id = this.id; return Uint8List.fromList([...author, ...id]); } - String get uniqueIdString => base64UrlNoPadEncode(uniqueIdBytes); + String get authorUniqueIdString => base64UrlNoPadEncode(authorUniqueIdBytes); static int compareTimestamp(proto.Message a, proto.Message b) => a.timestamp.compareTo(b.timestamp); diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart index 2f97b3f..f70a34c 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart @@ -9,16 +9,6 @@ import 'package:meta/meta.dart'; import '../../../veilid_support.dart'; -@immutable -class DHTLogElementState extends Equatable { - const DHTLogElementState({required this.value, required this.isOffline}); - final T value; - final bool isOffline; - - @override - List get props => [value, isOffline]; -} - @immutable class DHTLogStateData extends Equatable { const DHTLogStateData( @@ -28,7 +18,7 @@ class DHTLogStateData extends Equatable { required this.follow}); // The view of the elements in the dhtlog // Span is from [tail-length, tail) - final IList> elements; + final IList> elements; // One past the end of the last element final int tail; // The total number of elements to try to keep in 'elements' @@ -92,7 +82,8 @@ class DHTLogCubit extends Cubit> Future _refreshInner(void Function(AsyncValue>) emit, {bool forceRefresh = false}) async { - final avElements = await loadElements(_tail, _count); + final avElements = await operate( + (reader) => loadElementsFromReader(reader, _tail, _count)); final err = avElements.asError; if (err != null) { emit(AsyncValue.error(err.error, err.stackTrace)); @@ -109,26 +100,22 @@ class DHTLogCubit extends Cubit> } // Tail is one past the last element to load - Future>>> loadElements( - int tail, int count, + Future>>> loadElementsFromReader( + DHTLogReadOperations reader, int tail, int count, {bool forceRefresh = false}) async { - await _initWait(); try { - final allItems = await _log.operate((reader) async { - final length = reader.length; - final end = ((tail - 1) % length) + 1; - final start = (count < end) ? end - count : 0; + final length = reader.length; + final end = ((tail - 1) % length) + 1; + final start = (count < end) ? end - count : 0; - final offlinePositions = await reader.getOfflinePositions(); - final allItems = (await reader.getRange(start, - length: end - start, forceRefresh: forceRefresh)) - ?.indexed - .map((x) => DHTLogElementState( - value: _decodeElement(x.$2), - isOffline: offlinePositions.contains(x.$1))) - .toIList(); - return allItems; - }); + final offlinePositions = await reader.getOfflinePositions(); + final allItems = (await reader.getRange(start, + length: end - start, forceRefresh: forceRefresh)) + ?.indexed + .map((x) => OnlineElementState( + value: _decodeElement(x.$2), + isOffline: offlinePositions.contains(x.$1))) + .toIList(); if (allItems == null) { return const AsyncValue.loading(); } diff --git a/packages/veilid_support/lib/src/online_element_state.dart b/packages/veilid_support/lib/src/online_element_state.dart new file mode 100644 index 0000000..8cbd38b --- /dev/null +++ b/packages/veilid_support/lib/src/online_element_state.dart @@ -0,0 +1,12 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +@immutable +class OnlineElementState extends Equatable { + const OnlineElementState({required this.value, required this.isOffline}); + final T value; + final bool isOffline; + + @override + List get props => [value, isOffline]; +} diff --git a/packages/veilid_support/lib/veilid_support.dart b/packages/veilid_support/lib/veilid_support.dart index 42aa839..0f19bf3 100644 --- a/packages/veilid_support/lib/veilid_support.dart +++ b/packages/veilid_support/lib/veilid_support.dart @@ -10,6 +10,7 @@ export 'src/config.dart'; export 'src/identity.dart'; export 'src/json_tools.dart'; export 'src/memory_tools.dart'; +export 'src/online_element_state.dart'; export 'src/output.dart'; export 'src/persistent_queue.dart'; export 'src/protobuf_tools.dart'; From f780a60d69ca162add0a42f8fae4042d69afccc8 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 31 May 2024 18:55:44 -0400 Subject: [PATCH 122/270] fixing bugs --- lib/chat/cubits/single_contact_messages_cubit.dart | 3 ++- lib/chat/models/message_state.dart | 3 ++- packages/veilid_support/lib/src/table_db_array_cubit.dart | 3 +++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index c444134..d2abb22 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -143,7 +143,8 @@ class SingleContactMessagesCubit extends Cubit { // Open reconciled chat record key Future _initReconciledMessagesCubit() async { - final tableName = _localConversationRecordKey.toString(); + final tableName = + _localConversationRecordKey.toString().replaceAll(':', '_'); final crypto = await _makeLocalMessagesCrypto(); diff --git a/lib/chat/models/message_state.dart b/lib/chat/models/message_state.dart index f9952fa..8eacc8e 100644 --- a/lib/chat/models/message_state.dart +++ b/lib/chat/models/message_state.dart @@ -4,6 +4,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../proto/proto.dart' as proto; +import '../../proto/proto.dart' show messageFromJson, messageToJson; part 'message_state.freezed.dart'; part 'message_state.g.dart'; @@ -26,7 +27,7 @@ enum MessageSendState { class MessageState with _$MessageState { const factory MessageState({ // Content of the message - @JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + @JsonKey(fromJson: messageFromJson, toJson: messageToJson) required proto.Message content, // Sent timestamp required Timestamp sentTimestamp, diff --git a/packages/veilid_support/lib/src/table_db_array_cubit.dart b/packages/veilid_support/lib/src/table_db_array_cubit.dart index 1cebab5..c4ff507 100644 --- a/packages/veilid_support/lib/src/table_db_array_cubit.dart +++ b/packages/veilid_support/lib/src/table_db_array_cubit.dart @@ -105,6 +105,9 @@ class TableDBArrayCubit extends Cubit> ) async { try { final length = _array.length; + if (length == 0) { + return AsyncValue.data(IList.empty()); + } final end = ((tail - 1) % length) + 1; final start = (count < end) ? end - count : 0; final allItems = From 0e4606f35e2c12d49c720ef399d3dfecf7b50619 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 2 Jun 2024 11:04:19 -0400 Subject: [PATCH 123/270] debugging --- assets/i18n/en.json | 1 + .../reconciliation/author_input_queue.dart | 38 ++- .../reconciliation/author_input_source.dart | 7 +- .../message_reconciliation.dart | 38 ++- .../reconciliation/output_position.dart | 2 +- .../cubits/single_contact_messages_cubit.dart | 134 +++++---- lib/chat/models/message_state.freezed.dart | 14 +- lib/chat/views/chat_component.dart | 24 +- lib/chat/views/no_conversation_widget.dart | 55 ++-- .../home_account_ready_chat.dart | 2 +- .../home_account_ready_main.dart | 2 +- lib/main.dart | 4 +- .../src/dht_log/dht_log_cubit.dart | 74 +++-- .../src/dht_log/dht_log_spine.dart | 105 ++++--- .../src/dht_short_array/dht_short_array.dart | 11 + .../lib/src/table_db_array.dart | 269 ++++++++++++------ ...art => table_db_array_protobuf_cubit.dart} | 40 +-- .../veilid_support/lib/veilid_support.dart | 2 +- packages/veilid_support/pubspec.lock | 14 +- packages/veilid_support/pubspec.yaml | 6 + 20 files changed, 521 insertions(+), 321 deletions(-) rename packages/veilid_support/lib/src/{table_db_array_cubit.dart => table_db_array_protobuf_cubit.dart} (81%) diff --git a/assets/i18n/en.json b/assets/i18n/en.json index f074031..eeaa476 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -67,6 +67,7 @@ "new_chat": "New Chat" }, "chat": { + "start_a_conversation": "Start A Conversation", "say_something": "Say Something" }, "create_invitation_dialog": { diff --git a/lib/chat/cubits/reconciliation/author_input_queue.dart b/lib/chat/cubits/reconciliation/author_input_queue.dart index b9fd7d7..009604d 100644 --- a/lib/chat/cubits/reconciliation/author_input_queue.dart +++ b/lib/chat/cubits/reconciliation/author_input_queue.dart @@ -18,7 +18,7 @@ class AuthorInputQueue { _onError = onError, _inputSource = inputSource, _outputPosition = outputPosition, - _lastMessage = outputPosition?.message, + _lastMessage = outputPosition?.message.content, _messageIntegrity = messageIntegrity, _currentPosition = inputSource.currentWindow.last; @@ -43,8 +43,8 @@ class AuthorInputQueue { //////////////////////////////////////////////////////////////////////////// // Public interface - // Check if there are no messages in this queue to reconcile - bool get isEmpty => _currentMessage == null; + // Check if there are no messages left in this queue to reconcile + bool get isDone => _isDone; // Get the current message that needs reconciliation proto.Message? get current => _currentMessage; @@ -58,6 +58,9 @@ class AuthorInputQueue { // Remove a reconciled message and move to the next message // Returns true if there is more work to do Future consume() async { + if (_isDone) { + return false; + } while (true) { _lastMessage = _currentMessage; @@ -66,6 +69,7 @@ class AuthorInputQueue { // Get more window if we need to if (!await _updateWindow()) { // Window is not available so this queue can't work right now + _isDone = true; return false; } final nextMessage = _inputSource.currentWindow @@ -73,9 +77,9 @@ class AuthorInputQueue { // Drop the 'offline' elements because we don't reconcile // anything until it has been confirmed to be committed to the DHT - if (nextMessage.isOffline) { - continue; - } + // if (nextMessage.isOffline) { + // continue; + // } if (_lastMessage != null) { // Ensure the timestamp is not moving backward @@ -112,7 +116,7 @@ class AuthorInputQueue { outer: while (true) { // Iterate through current window backward - for (var i = _inputSource.currentWindow.elements.length; + for (var i = _inputSource.currentWindow.elements.length - 1; i >= 0 && _currentPosition >= 0; i--, _currentPosition--) { final elem = _inputSource.currentWindow.elements[i]; @@ -134,13 +138,24 @@ class AuthorInputQueue { if (!await _updateWindow()) { // Window is not available or things are empty so this // queue can't work right now + _isDone = true; return false; } } - // The current position should be equal to the first message to process - // and the current window to process should not be empty - return _inputSource.currentWindow.elements.isNotEmpty; + // _currentPosition points to either before the input source starts + // or the position of the previous element. We still need to set the + // _currentMessage to the previous element so consume() can compare + // against it if we can. + if (_currentPosition >= 0) { + _currentMessage = _inputSource.currentWindow + .elements[_currentPosition - _inputSource.currentWindow.first].value; + } + + // After this consume(), the currentPosition and _currentMessage should + // be equal to the first message to process and the current window to + // process should not be empty + return consume(); } // Slide the window toward the current position and load the batch around it @@ -186,6 +201,9 @@ class AuthorInputQueue { int _currentPosition; // The current message we're looking at proto.Message? _currentMessage; + // If we have reached the end + bool _isDone = false; + // Desired maximum window length static const int _maxWindowLength = 256; } diff --git a/lib/chat/cubits/reconciliation/author_input_source.dart b/lib/chat/cubits/reconciliation/author_input_source.dart index 1f67264..32a750e 100644 --- a/lib/chat/cubits/reconciliation/author_input_source.dart +++ b/lib/chat/cubits/reconciliation/author_input_source.dart @@ -21,9 +21,10 @@ class AuthorInputSource { {required DHTLogStateData cubitState, required this.cubit}) { _currentWindow = InputWindow( - elements: cubitState.elements, - first: cubitState.tail - cubitState.elements.length, - last: cubitState.tail - 1); + elements: cubitState.window, + first: (cubitState.windowTail - cubitState.window.length) % + cubitState.length, + last: (cubitState.windowTail - 1) % cubitState.length); } //////////////////////////////////////////////////////////////////////////// diff --git a/lib/chat/cubits/reconciliation/message_reconciliation.dart b/lib/chat/cubits/reconciliation/message_reconciliation.dart index 1687f4d..aa53f49 100644 --- a/lib/chat/cubits/reconciliation/message_reconciliation.dart +++ b/lib/chat/cubits/reconciliation/message_reconciliation.dart @@ -12,7 +12,7 @@ import 'output_position.dart'; class MessageReconciliation { MessageReconciliation( - {required TableDBArrayCubit output, + {required TableDBArrayProtobufCubit output, required void Function(Object, StackTrace?) onError}) : _outputCubit = output, _onError = onError; @@ -23,7 +23,7 @@ class MessageReconciliation { TypedKey author, DHTLogStateData inputMessagesCubitState, DHTLogCubit inputMessagesCubit) { - if (inputMessagesCubitState.elements.isEmpty) { + if (inputMessagesCubitState.window.isEmpty) { return; } @@ -84,11 +84,11 @@ class MessageReconciliation { _outputCubit.operate((arr) async { var pos = arr.length - 1; while (pos >= 0) { - final message = await arr.getProtobuf(proto.Message.fromBuffer, pos); + final message = await arr.get(pos); if (message == null) { throw StateError('should have gotten last message'); } - if (message.author.toVeilid() == author) { + if (message.content.author.toVeilid() == author) { return OutputPosition(message, pos); } pos--; @@ -99,11 +99,11 @@ class MessageReconciliation { // Process a list of author input queues and insert their messages // into the output array, performing validation steps along the way Future _reconcileInputQueues({ - required TableDBArray reconciledArray, + required TableDBArrayProtobuf reconciledArray, required List inputQueues, }) async { // Ensure queues all have something to do - inputQueues.removeWhere((q) => q.isEmpty); + inputQueues.removeWhere((q) => q.isDone); if (inputQueues.isEmpty) { return; } @@ -124,8 +124,7 @@ class MessageReconciliation { // Get the timestamp for this output position var currentOutputMessage = firstOutputPos == null ? null - : await reconciledArray.getProtobuf( - proto.Message.fromBuffer, firstOutputPos); + : await reconciledArray.get(firstOutputPos); var currentOutputPos = firstOutputPos ?? 0; @@ -143,7 +142,7 @@ class MessageReconciliation { for (final inputQueue in inputQueues) { final inputCurrent = inputQueue.current!; if (currentOutputMessage == null || - inputCurrent.timestamp <= currentOutputMessage.timestamp) { + inputCurrent.timestamp < currentOutputMessage.content.timestamp) { toInsert.add(inputCurrent); added = true; @@ -156,7 +155,7 @@ class MessageReconciliation { } // Remove empty queues now that we're done iterating if (someQueueEmpty) { - inputQueues.removeWhere((q) => q.isEmpty); + inputQueues.removeWhere((q) => q.isDone); } if (toInsert.length >= _maxReconcileChunk) { @@ -166,13 +165,24 @@ class MessageReconciliation { // Perform insertions in bulk if (toInsert.isNotEmpty) { - await reconciledArray.insertAllProtobuf(currentOutputPos, toInsert); + final reconciledTime = Veilid.instance.now().toInt64(); + + // Add reconciled timestamps + final reconciledInserts = toInsert + .map((message) => proto.ReconciledMessage() + ..reconciledTime = reconciledTime + ..content = message) + .toList(); + + await reconciledArray.insertAll(currentOutputPos, reconciledInserts); + toInsert.clear(); } else { // If there's nothing to insert at this position move to the next one currentOutputPos++; - currentOutputMessage = await reconciledArray.getProtobuf( - proto.Message.fromBuffer, currentOutputPos); + currentOutputMessage = (currentOutputPos == reconciledArray.length) + ? null + : await reconciledArray.get(currentOutputPos); } } } @@ -180,7 +190,7 @@ class MessageReconciliation { //////////////////////////////////////////////////////////////////////////// Map _inputSources = {}; - final TableDBArrayCubit _outputCubit; + final TableDBArrayProtobufCubit _outputCubit; final void Function(Object, StackTrace?) _onError; static const int _maxReconcileChunk = 65536; diff --git a/lib/chat/cubits/reconciliation/output_position.dart b/lib/chat/cubits/reconciliation/output_position.dart index 258259e..d983c95 100644 --- a/lib/chat/cubits/reconciliation/output_position.dart +++ b/lib/chat/cubits/reconciliation/output_position.dart @@ -6,7 +6,7 @@ import '../../../proto/proto.dart' as proto; @immutable class OutputPosition extends Equatable { const OutputPosition(this.message, this.pos); - final proto.Message message; + final proto.ReconciledMessage message; final int pos; @override List get props => [message, pos]; diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index d2abb22..bc2d8c5 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -22,14 +22,17 @@ class RenderStateElement { if (!isLocal) { return null; } - - if (sent && !sentOffline) { + if (reconciledTimestamp != null) { return MessageSendState.delivered; } - if (reconciledTimestamp != null) { - return MessageSendState.sent; + if (sent) { + if (!sentOffline) { + return MessageSendState.sent; + } else { + return MessageSendState.sending; + } } - return MessageSendState.sending; + return null; } proto.Message message; @@ -66,7 +69,7 @@ class SingleContactMessagesCubit extends Cubit { Future close() async { await _initWait(); - await _sendingMessagesQueue.close(); + await _unsentMessagesQueue.close(); await _sentSubscription?.cancel(); await _rcvdSubscription?.cancel(); await _reconciledSubscription?.cancel(); @@ -78,11 +81,11 @@ class SingleContactMessagesCubit extends Cubit { // Initialize everything Future _init() async { - _sendingMessagesQueue = PersistentQueue( - table: 'SingleContactSendingMessages', + _unsentMessagesQueue = PersistentQueue( + table: 'SingleContactUnsentMessages', key: _remoteConversationRecordKey.toString(), fromBuffer: proto.Message.fromBuffer, - closure: _processSendingMessages, + closure: _processUnsentMessages, ); // Make crypto @@ -144,13 +147,16 @@ class SingleContactMessagesCubit extends Cubit { // Open reconciled chat record key Future _initReconciledMessagesCubit() async { final tableName = - _localConversationRecordKey.toString().replaceAll(':', '_'); + _reconciledMessagesTableDBName(_localConversationRecordKey); final crypto = await _makeLocalMessagesCrypto(); - _reconciledMessagesCubit = TableDBArrayCubit( - open: () async => TableDBArray.make(table: tableName, crypto: crypto), - decodeElement: proto.ReconciledMessage.fromBuffer); + _reconciledMessagesCubit = TableDBArrayProtobufCubit( + open: () async => TableDBArrayProtobuf.make( + table: tableName, + crypto: crypto, + fromBuffer: proto.ReconciledMessage.fromBuffer), + ); _reconciliation = MessageReconciliation( output: _reconciledMessagesCubit!, @@ -200,6 +206,9 @@ class SingleContactMessagesCubit extends Cubit { _activeAccountInfo.localAccount.identityMaster.identityPublicTypedKey(), sentMessages, _sentMessagesCubit!); + + // Update the view + _renderState(); } // Called when the received messages cubit gets a change @@ -211,11 +220,14 @@ class SingleContactMessagesCubit extends Cubit { _reconciliation.reconcileMessages( _remoteIdentityPublicKey, rcvdMessages, _rcvdMessagesCubit!); + + // Update the view + _renderState(); } // Called when the reconciled messages window gets a change void _updateReconciledMessagesState( - TableDBArrayBusyState avmessages) { + TableDBArrayProtobufBusyState avmessages) { // Update the view _renderState(); } @@ -237,7 +249,7 @@ class SingleContactMessagesCubit extends Cubit { } // Async process to send messages in the background - Future _processSendingMessages(IList messages) async { + Future _processUnsentMessages(IList messages) async { // Go through and assign ids to all the messages in order proto.Message? previousMessage; final processedMessages = messages.toList(); @@ -258,7 +270,7 @@ class SingleContactMessagesCubit extends Cubit { // Get all sent messages final sentMessages = _sentMessagesCubit?.state.state.asData?.value; // Get all items in the unsent queue - final sendingMessages = _sendingMessagesQueue.queue; + // final unsentMessages = _unsentMessagesQueue.queue; // If we aren't ready to render a state, say we're loading if (reconciledMessages == null || sentMessages == null) { @@ -267,63 +279,49 @@ class SingleContactMessagesCubit extends Cubit { } // Generate state for each message + // final reconciledMessagesMap = + // IMap.fromValues( + // keyMapper: (x) => x.content.authorUniqueIdString, + // values: reconciledMessages.elements, + // ); final sentMessagesMap = IMap>.fromValues( keyMapper: (x) => x.value.authorUniqueIdString, - values: sentMessages.elements, - ); - final reconciledMessagesMap = - IMap.fromValues( - keyMapper: (x) => x.content.authorUniqueIdString, - values: reconciledMessages.elements, - ); - final sendingMessagesMap = IMap.fromValues( - keyMapper: (x) => x.authorUniqueIdString, - values: sendingMessages, + values: sentMessages.window, ); + // final unsentMessagesMap = IMap.fromValues( + // keyMapper: (x) => x.authorUniqueIdString, + // values: unsentMessages, + // ); - final renderedElements = {}; + final renderedElements = []; - for (final m in reconciledMessagesMap.entries) { - renderedElements[m.key] = RenderStateElement( - message: m.value.content, - isLocal: m.value.content.author.toVeilid() == - _activeAccountInfo.localAccount.identityMaster - .identityPublicTypedKey(), - reconciledTimestamp: Timestamp.fromInt64(m.value.reconciledTime), - ); - } - for (final m in sentMessagesMap.entries) { - renderedElements.putIfAbsent( - m.key, - () => RenderStateElement( - message: m.value.value, - isLocal: true, - )) - ..sent = true - ..sentOffline = m.value.isOffline; - } - for (final m in sendingMessagesMap.entries) { - renderedElements - .putIfAbsent( - m.key, - () => RenderStateElement( - message: m.value, - isLocal: true, - )) - .sent = false; + for (final m in reconciledMessages.elements) { + final isLocal = m.content.author.toVeilid() == + _activeAccountInfo.localAccount.identityMaster + .identityPublicTypedKey(); + final reconciledTimestamp = Timestamp.fromInt64(m.reconciledTime); + final sm = + isLocal ? sentMessagesMap[m.content.authorUniqueIdString] : null; + final sent = isLocal && sm != null; + final sentOffline = isLocal && sm != null && sm.isOffline; + + renderedElements.add(RenderStateElement( + message: m.content, + isLocal: isLocal, + reconciledTimestamp: reconciledTimestamp, + sent: sent, + sentOffline: sentOffline, + )); } // Render the state - final messageKeys = renderedElements.entries - .toIList() - .sort((x, y) => x.key.compareTo(y.key)); - final renderedState = messageKeys + final renderedState = renderedElements .map((x) => MessageState( - content: x.value.message, - sentTimestamp: Timestamp.fromInt64(x.value.message.timestamp), - reconciledTimestamp: x.value.reconciledTimestamp, - sendState: x.value.sendState)) + content: x.message, + sentTimestamp: Timestamp.fromInt64(x.message.timestamp), + reconciledTimestamp: x.reconciledTimestamp, + sendState: x.sendState)) .toIList(); // Emit the rendered state @@ -340,7 +338,7 @@ class SingleContactMessagesCubit extends Cubit { ..timestamp = Veilid.instance.now().toInt64(); // Put in the queue - _sendingMessagesQueue.addSync(message); + _unsentMessagesQueue.addSync(message); // Update the view _renderState(); @@ -358,7 +356,7 @@ class SingleContactMessagesCubit extends Cubit { static String _reconciledMessagesTableDBName( TypedKey localConversationRecordKey) => - 'msg_$localConversationRecordKey'; + 'msg_${localConversationRecordKey.toString().replaceAll(':', '_')}'; ///////////////////////////////////////////////////////////////////////// @@ -375,14 +373,14 @@ class SingleContactMessagesCubit extends Cubit { DHTLogCubit? _sentMessagesCubit; DHTLogCubit? _rcvdMessagesCubit; - TableDBArrayCubit? _reconciledMessagesCubit; + TableDBArrayProtobufCubit? _reconciledMessagesCubit; late final MessageReconciliation _reconciliation; - late final PersistentQueue _sendingMessagesQueue; + late final PersistentQueue _unsentMessagesQueue; StreamSubscription>? _sentSubscription; StreamSubscription>? _rcvdSubscription; - StreamSubscription>? + StreamSubscription>? _reconciledSubscription; } diff --git a/lib/chat/models/message_state.freezed.dart b/lib/chat/models/message_state.freezed.dart index a99f937..96c98e2 100644 --- a/lib/chat/models/message_state.freezed.dart +++ b/lib/chat/models/message_state.freezed.dart @@ -21,7 +21,7 @@ MessageState _$MessageStateFromJson(Map json) { /// @nodoc mixin _$MessageState { // Content of the message - @JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + @JsonKey(fromJson: messageFromJson, toJson: messageToJson) proto.Message get content => throw _privateConstructorUsedError; // Sent timestamp Timestamp get sentTimestamp => @@ -43,7 +43,7 @@ abstract class $MessageStateCopyWith<$Res> { _$MessageStateCopyWithImpl<$Res, MessageState>; @useResult $Res call( - {@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + {@JsonKey(fromJson: messageFromJson, toJson: messageToJson) proto.Message content, Timestamp sentTimestamp, Timestamp? reconciledTimestamp, @@ -98,7 +98,7 @@ abstract class _$$MessageStateImplCopyWith<$Res> @override @useResult $Res call( - {@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + {@JsonKey(fromJson: messageFromJson, toJson: messageToJson) proto.Message content, Timestamp sentTimestamp, Timestamp? reconciledTimestamp, @@ -146,7 +146,7 @@ class __$$MessageStateImplCopyWithImpl<$Res> @JsonSerializable() class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { const _$MessageStateImpl( - {@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + {@JsonKey(fromJson: messageFromJson, toJson: messageToJson) required this.content, required this.sentTimestamp, required this.reconciledTimestamp, @@ -157,7 +157,7 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { // Content of the message @override - @JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + @JsonKey(fromJson: messageFromJson, toJson: messageToJson) final proto.Message content; // Sent timestamp @override @@ -220,7 +220,7 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { abstract class _MessageState implements MessageState { const factory _MessageState( - {@JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + {@JsonKey(fromJson: messageFromJson, toJson: messageToJson) required final proto.Message content, required final Timestamp sentTimestamp, required final Timestamp? reconciledTimestamp, @@ -230,7 +230,7 @@ abstract class _MessageState implements MessageState { _$MessageStateImpl.fromJson; @override // Content of the message - @JsonKey(fromJson: proto.messageFromJson, toJson: proto.messageToJson) + @JsonKey(fromJson: messageFromJson, toJson: messageToJson) proto.Message get content; @override // Sent timestamp Timestamp get sentTimestamp; diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 1e296e9..327b82e 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -271,18 +271,18 @@ class ChatComponent extends StatelessWidget { child: DecoratedBox( decoration: const BoxDecoration(), child: Chat( - theme: chatTheme, - // emojiEnlargementBehavior: - // EmojiEnlargementBehavior.multi, - messages: chatMessages, - //onAttachmentPressed: _handleAttachmentPressed, - //onMessageTap: _handleMessageTap, - //onPreviewDataFetched: _handlePreviewDataFetched, - onSendPressed: _handleSendPressed, - //showUserAvatars: false, - //showUserNames: true, - user: _localUser, - ), + theme: chatTheme, + // emojiEnlargementBehavior: + // EmojiEnlargementBehavior.multi, + messages: chatMessages, + //onAttachmentPressed: _handleAttachmentPressed, + //onMessageTap: _handleMessageTap, + //onPreviewDataFetched: _handlePreviewDataFetched, + onSendPressed: _handleSendPressed, + //showUserAvatars: false, + //showUserNames: true, + user: _localUser, + emptyState: const EmptyChatWidget()), ), ), ], diff --git a/lib/chat/views/no_conversation_widget.dart b/lib/chat/views/no_conversation_widget.dart index 1b8545f..c19b535 100644 --- a/lib/chat/views/no_conversation_widget.dart +++ b/lib/chat/views/no_conversation_widget.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_translate/flutter_translate.dart'; + +import '../../theme/models/scale_scheme.dart'; class NoConversationWidget extends StatelessWidget { const NoConversationWidget({super.key}); @@ -7,28 +10,32 @@ class NoConversationWidget extends StatelessWidget { // 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, - ), - ), - ], - ), - ); + ) { + final theme = Theme.of(context); + final scale = theme.extension()!; + + return Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.diversity_3, + color: scale.primaryScale.subtleBorder, + size: 48, + ), + Text( + translate('chat.start_a_conversation'), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: scale.primaryScale.subtleBorder, + ), + ), + ], + ), + ); + } } 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 087bb34..587828a 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 @@ -31,7 +31,7 @@ class HomeAccountReadyChatState extends State { final activeChatLocalConversationKey = context.watch().state; if (activeChatLocalConversationKey == null) { - return const EmptyChatWidget(); + return const NoConversationWidget(); } return ChatComponent.builder( localConversationRecordKey: activeChatLocalConversationKey); 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 59eb00b..0a4b28e 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 @@ -69,7 +69,7 @@ class _HomeAccountReadyMainState extends State { final activeChatLocalConversationKey = context.watch().state; if (activeChatLocalConversationKey == null) { - return const EmptyChatWidget(); + return const NoConversationWidget(); } return ChatComponent.builder( localConversationRecordKey: activeChatLocalConversationKey); diff --git a/lib/main.dart b/lib/main.dart index ab3feed..d8bd6df 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:intl/date_symbol_data_local.dart'; +import 'package:stack_trace/stack_trace.dart'; import 'app.dart'; import 'settings/preferences_repository.dart'; @@ -52,7 +53,8 @@ void main() async { if (kDebugMode) { // In debug mode, run the app without catching exceptions for debugging - await mainFunc(); + // but do a much deeper async stack trace capture + await Chain.capture(mainFunc); } else { // Catch errors in production without killing the app await runZonedGuarded(mainFunc, (error, stackTrace) { diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart index f70a34c..b5a728b 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart @@ -12,22 +12,25 @@ import '../../../veilid_support.dart'; @immutable class DHTLogStateData extends Equatable { const DHTLogStateData( - {required this.elements, - required this.tail, - required this.count, + {required this.length, + required this.window, + required this.windowTail, + required this.windowSize, required this.follow}); - // The view of the elements in the dhtlog - // Span is from [tail-length, tail) - final IList> elements; - // One past the end of the last element - final int tail; - // The total number of elements to try to keep in 'elements' - final int count; - // If we should have the tail following the log + // The total number of elements in the whole log + final int length; + // The view window of the elements in the dhtlog + // Span is from [tail - window.length, tail) + final IList> window; + // The position of the view window, one past the last element + final int windowTail; + // The total number of elements to try to keep in the window + final int windowSize; + // If we have the window following the log final bool follow; @override - List get props => [elements, tail, count, follow]; + List get props => [length, window, windowTail, windowSize, follow]; } typedef DHTLogState = AsyncValue>; @@ -58,13 +61,16 @@ class DHTLogCubit extends Cubit> // If tail is positive, the position is absolute from the head of the log // If follow is enabled, the tail offset will update when the log changes Future setWindow( - {int? tail, int? count, bool? follow, bool forceRefresh = false}) async { + {int? windowTail, + int? windowSize, + bool? follow, + bool forceRefresh = false}) async { await _initWait(); - if (tail != null) { - _tail = tail; + if (windowTail != null) { + _windowTail = windowTail; } - if (count != null) { - _count = count; + if (windowSize != null) { + _windowSize = windowSize; } if (follow != null) { _follow = follow; @@ -82,8 +88,13 @@ class DHTLogCubit extends Cubit> Future _refreshInner(void Function(AsyncValue>) emit, {bool forceRefresh = false}) async { - final avElements = await operate( - (reader) => loadElementsFromReader(reader, _tail, _count)); + late final AsyncValue>> avElements; + late final int length; + await _log.operate((reader) async { + length = reader.length; + avElements = + await loadElementsFromReader(reader, _windowTail, _windowSize); + }); final err = avElements.asError; if (err != null) { emit(AsyncValue.error(err.error, err.stackTrace)); @@ -94,9 +105,13 @@ class DHTLogCubit extends Cubit> emit(const AsyncValue.loading()); return; } - final elements = avElements.asData!.value; + final window = avElements.asData!.value; emit(AsyncValue.data(DHTLogStateData( - elements: elements, tail: _tail, count: _count, follow: _follow))); + length: length, + window: window, + windowTail: _windowTail, + windowSize: _windowSize, + follow: _follow))); } // Tail is one past the last element to load @@ -105,6 +120,9 @@ class DHTLogCubit extends Cubit> {bool forceRefresh = false}) async { try { final length = reader.length; + if (length == 0) { + return const AsyncValue.data(IList.empty()); + } final end = ((tail - 1) % length) + 1; final start = (count < end) ? end - count : 0; @@ -138,18 +156,18 @@ class DHTLogCubit extends Cubit> _sspUpdate.busyUpdate>(busy, (emit) async { // apply follow if (_follow) { - if (_tail <= 0) { + if (_windowTail <= 0) { // Negative tail is already following tail changes } else { // Positive tail is measured from the head, so apply deltas - _tail = (_tail + _tailDelta - _headDelta) % upd.length; + _windowTail = (_windowTail + _tailDelta - _headDelta) % upd.length; } } else { - if (_tail <= 0) { + if (_windowTail <= 0) { // Negative tail is following tail changes so apply deltas - var posTail = _tail + upd.length; + var posTail = _windowTail + upd.length; posTail = (posTail + _tailDelta - _headDelta) % upd.length; - _tail = posTail - upd.length; + _windowTail = posTail - upd.length; } else { // Positive tail is measured from head so not following tail } @@ -202,7 +220,7 @@ class DHTLogCubit extends Cubit> var _tailDelta = 0; // Cubit window into the DHTLog - var _tail = 0; - var _count = DHTShortArray.maxElements; + var _windowTail = 0; + var _windowSize = DHTShortArray.maxElements; var _follow = true; } diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index a47602a..6b5665d 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -451,48 +451,53 @@ class _DHTLogSpine { /////////////////////////////////////////// // API for public interfaces - Future<_DHTLogPosition?> lookupPosition(int pos) async { - assert(_spineMutex.isLocked, 'should be locked'); - return _spineCacheMutex.protect(() async { - // Check if our position is in bounds - final endPos = length; - if (pos < 0 || pos >= endPos) { - throw IndexError.withLength(pos, endPos); - } + Future<_DHTLogPosition?> lookupPositionBySegmentNumber( + int segmentNumber, int segmentPos) async => + _spineCacheMutex.protect(() async { + // Get the segment shortArray + final openedSegment = _openedSegments[segmentNumber]; + late final DHTShortArray shortArray; + if (openedSegment != null) { + openedSegment.openCount++; + shortArray = openedSegment.shortArray; + } else { + final newShortArray = (_spineRecord.writer == null) + ? await _openSegment(segmentNumber) + : await _openOrCreateSegment(segmentNumber); + if (newShortArray == null) { + return null; + } - // Calculate absolute position, ring-buffer style - final absolutePosition = (_head + pos) % _positionLimit; + _openedSegments[segmentNumber] = + _OpenedSegment._(shortArray: newShortArray); - // Determine the segment number and position within the segment - final segmentNumber = absolutePosition ~/ DHTShortArray.maxElements; - final segmentPos = absolutePosition % DHTShortArray.maxElements; - - // Get the segment shortArray - final openedSegment = _openedSegments[segmentNumber]; - late final DHTShortArray shortArray; - if (openedSegment != null) { - openedSegment.openCount++; - shortArray = openedSegment.shortArray; - } else { - final newShortArray = (_spineRecord.writer == null) - ? await _openSegment(segmentNumber) - : await _openOrCreateSegment(segmentNumber); - if (newShortArray == null) { - return null; + shortArray = newShortArray; } - _openedSegments[segmentNumber] = - _OpenedSegment._(shortArray: newShortArray); + return _DHTLogPosition._( + dhtLogSpine: this, + shortArray: shortArray, + pos: segmentPos, + segmentNumber: segmentNumber); + }); - shortArray = newShortArray; - } + Future<_DHTLogPosition?> lookupPosition(int pos) async { + assert(_spineMutex.isLocked, 'should be locked'); - return _DHTLogPosition._( - dhtLogSpine: this, - shortArray: shortArray, - pos: segmentPos, - segmentNumber: segmentNumber); - }); + // Check if our position is in bounds + final endPos = length; + if (pos < 0 || pos >= endPos) { + throw IndexError.withLength(pos, endPos); + } + + // Calculate absolute position, ring-buffer style + final absolutePosition = (_head + pos) % _positionLimit; + + // Determine the segment number and position within the segment + final segmentNumber = absolutePosition ~/ DHTShortArray.maxElements; + final segmentPos = absolutePosition % DHTShortArray.maxElements; + + return lookupPositionBySegmentNumber(segmentNumber, segmentPos); } Future _segmentClosed(int segmentNumber) async { @@ -660,6 +665,34 @@ class _DHTLogSpine { final oldHead = _head; final oldTail = _tail; await _updateHead(headData); + + // Lookup tail position segments that have changed + // and force their short arrays to refresh their heads + final segmentsToRefresh = <_DHTLogPosition>[]; + int? lastSegmentNumber; + for (var curTail = oldTail; + curTail != _tail; + curTail = (curTail + 1) % _positionLimit) { + final segmentNumber = curTail ~/ DHTShortArray.maxElements; + final segmentPos = curTail % DHTShortArray.maxElements; + if (segmentNumber == lastSegmentNumber) { + continue; + } + lastSegmentNumber = segmentNumber; + final dhtLogPosition = + await lookupPositionBySegmentNumber(segmentNumber, segmentPos); + if (dhtLogPosition == null) { + throw Exception('missing segment in dht log'); + } + segmentsToRefresh.add(dhtLogPosition); + } + + // Refresh the segments that have probably changed + await segmentsToRefresh.map((p) async { + await p.shortArray.refresh(); + await p.close(); + }).wait; + sendUpdate(oldHead, oldTail); }); } 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 0732255..a84f02d 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 @@ -185,6 +185,17 @@ class DHTShortArray implements DHTDeleteable { /// Get the record pointer foir this shortarray OwnedDHTRecordPointer get recordPointer => _head.recordPointer; + /// Refresh this DHTShortArray + /// Useful if you aren't 'watching' the array and want to poll for an update + Future refresh() async { + if (!isOpen) { + throw StateError('short array is not open"'); + } + await _head.operate((head) async { + await head._loadHead(); + }); + } + /// Runs a closure allowing read-only access to the shortarray Future operate( Future Function(DHTShortArrayReadOperations) closure) async { diff --git a/packages/veilid_support/lib/src/table_db_array.dart b/packages/veilid_support/lib/src/table_db_array.dart index 8bcd146..bc0eca5 100644 --- a/packages/veilid_support/lib/src/table_db_array.dart +++ b/packages/veilid_support/lib/src/table_db_array.dart @@ -23,8 +23,8 @@ class TableDBArrayUpdate extends Equatable { List get props => [headDelta, tailDelta, length]; } -class TableDBArray { - TableDBArray({ +class _TableDBArrayBase { + _TableDBArrayBase({ required String table, required VeilidCrypto crypto, }) : _table = table, @@ -32,14 +32,14 @@ class TableDBArray { _initWait.add(_init); } - static Future make({ - required String table, - required VeilidCrypto crypto, - }) async { - final out = TableDBArray(table: table, crypto: crypto); - await out._initWait(); - return out; - } + // static Future make({ + // required String table, + // required VeilidCrypto crypto, + // }) async { + // final out = TableDBArray(table: table, crypto: crypto); + // await out._initWait(); + // return out; + // } Future initWait() async { await _initWait(); @@ -99,27 +99,27 @@ class TableDBArray { bool get isOpen => _open; - Future add(Uint8List value) async { + Future _add(Uint8List value) async { await _initWait(); return _writeTransaction((t) async => _addInner(t, value)); } - Future addAll(List values) async { + Future _addAll(List values) async { await _initWait(); return _writeTransaction((t) async => _addAllInner(t, values)); } - Future insert(int pos, Uint8List value) async { + Future _insert(int pos, Uint8List value) async { await _initWait(); return _writeTransaction((t) async => _insertInner(t, pos, value)); } - Future insertAll(int pos, List values) async { + Future _insertAll(int pos, List values) async { await _initWait(); return _writeTransaction((t) async => _insertAllInner(t, pos, values)); } - Future get(int pos) async { + Future _get(int pos) async { await _initWait(); return _mutex.protect(() async { if (!_open) { @@ -129,7 +129,7 @@ class TableDBArray { }); } - Future> getRange(int start, [int? end]) async { + Future> _getRange(int start, [int? end]) async { await _initWait(); return _mutex.protect(() async { if (!_open) { @@ -139,12 +139,12 @@ class TableDBArray { }); } - Future remove(int pos, {Output? out}) async { + Future _remove(int pos, {Output? out}) async { await _initWait(); return _writeTransaction((t) async => _removeInner(t, pos, out: out)); } - Future removeRange(int start, int end, + Future _removeRange(int start, int end, {Output>? out}) async { await _initWait(); return _writeTransaction( @@ -374,7 +374,9 @@ class TableDBArray { Future _loadEntry(int entry) async { final encryptedValue = await _tableDB.load(0, _entryKey(entry)); - return (encryptedValue == null) ? null : _crypto.decrypt(encryptedValue); + return (encryptedValue == null) + ? null + : await _crypto.decrypt(encryptedValue); } Future _getIndexEntry(int pos) async { @@ -631,77 +633,170 @@ class TableDBArray { StreamController.broadcast(); } -extension TableDBArrayExt on TableDBArray { - /// Convenience function: - /// Like get but also parses the returned element as JSON - Future getJson( - T Function(dynamic) fromJson, +////////////////////////////////////////////////////////////////////////////// + +class TableDBArray extends _TableDBArrayBase { + TableDBArray({ + required super.table, + required super.crypto, + }); + + static Future make({ + required String table, + required VeilidCrypto crypto, + }) async { + final out = TableDBArray(table: table, crypto: crypto); + await out._initWait(); + return out; + } + + //////////////////////////////////////////////////////////// + // Public interface + + Future add(Uint8List value) => _add(value); + + Future addAll(List values) => _addAll(values); + + Future insert(int pos, Uint8List value) => _insert(pos, value); + + Future insertAll(int pos, List values) => + _insertAll(pos, values); + + Future get( int pos, ) => - get( - pos, - ).then((out) => jsonDecodeOptBytes(fromJson, out)); + _get(pos); - /// Convenience function: - /// Like getRange but also parses the returned elements as JSON - Future?> getRangeJson(T Function(dynamic) fromJson, int start, - [int? end]) => - getRange(start, end ?? _length).then((out) => out.map(fromJson).toList()); + Future> getRange(int start, [int? end]) => + _getRange(start, end); - /// Convenience function: - /// Like get but also parses the returned element as a protobuf object - Future getProtobuf( - T Function(List) fromBuffer, - int pos, - ) => - get(pos).then(fromBuffer); + Future remove(int pos, {Output? out}) => + _remove(pos, out: out); - /// Convenience function: - /// Like getRange but also parses the returned elements as protobuf objects - Future?> getRangeProtobuf( - T Function(List) fromBuffer, int start, [int? end]) => - getRange(start, end ?? _length) - .then((out) => out.map(fromBuffer).toList()); - - /// Convenience function: - /// Like add but for a JSON value - Future addJson(T value) async => add(jsonEncodeBytes(value)); - - /// Convenience function: - /// Like add but for a Protobuf value - Future addProtobuf(T value) => - add(value.writeToBuffer()); - - /// Convenience function: - /// Like addAll but for a JSON value - Future addAllJson(List values) async => - addAll(values.map(jsonEncodeBytes).toList()); - - /// Convenience function: - /// Like addAll but for a Protobuf value - Future addAllProtobuf( - List values) async => - addAll(values.map((x) => x.writeToBuffer()).toList()); - - /// Convenience function: - /// Like insert but for a JSON value - Future insertJson(int pos, T value) async => - insert(pos, jsonEncodeBytes(value)); - - /// Convenience function: - /// Like insert but for a Protobuf value - Future insertProtobuf( - int pos, T value) async => - insert(pos, value.writeToBuffer()); - - /// Convenience function: - /// Like insertAll but for a JSON value - Future insertAllJson(int pos, List values) async => - insertAll(pos, values.map(jsonEncodeBytes).toList()); - - /// Convenience function: - /// Like insertAll but for a Protobuf value - Future insertAllProtobuf( - int pos, List values) async => - insertAll(pos, values.map((x) => x.writeToBuffer()).toList()); + Future removeRange(int start, int end, + {Output>? out}) => + _removeRange(start, end, out: out); +} +////////////////////////////////////////////////////////////////////////////// + +class TableDBArrayJson extends _TableDBArrayBase { + TableDBArrayJson( + {required super.table, + required super.crypto, + required T Function(dynamic) fromJson}) + : _fromJson = fromJson; + + static Future> make( + {required String table, + required VeilidCrypto crypto, + required T Function(dynamic) fromJson}) async { + final out = + TableDBArrayJson(table: table, crypto: crypto, fromJson: fromJson); + await out._initWait(); + return out; + } + + //////////////////////////////////////////////////////////// + // Public interface + + Future add(T value) => _add(jsonEncodeBytes(value)); + + Future addAll(List values) async => + _addAll(values.map(jsonEncodeBytes).toList()); + + Future insert(int pos, T value) async => + _insert(pos, jsonEncodeBytes(value)); + + Future insertAll(int pos, List values) async => + _insertAll(pos, values.map(jsonEncodeBytes).toList()); + + Future get( + int pos, + ) => + _get(pos).then((out) => jsonDecodeOptBytes(_fromJson, out)); + + Future> getRange(int start, [int? end]) => + _getRange(start, end).then((out) => out.map(_fromJson).toList()); + + Future remove(int pos, {Output? out}) async { + final outJson = (out != null) ? Output() : null; + await _remove(pos, out: outJson); + if (outJson != null && outJson.value != null) { + out!.save(jsonDecodeBytes(_fromJson, outJson.value!)); + } + } + + Future removeRange(int start, int end, {Output>? out}) async { + final outJson = (out != null) ? Output>() : null; + await _removeRange(start, end, out: outJson); + if (outJson != null && outJson.value != null) { + out!.save( + outJson.value!.map((x) => jsonDecodeBytes(_fromJson, x)).toList()); + } + } + + //////////////////////////////////////////////////////////////////////////// + final T Function(dynamic) _fromJson; +} + +////////////////////////////////////////////////////////////////////////////// + +class TableDBArrayProtobuf + extends _TableDBArrayBase { + TableDBArrayProtobuf( + {required super.table, + required super.crypto, + required T Function(List) fromBuffer}) + : _fromBuffer = fromBuffer; + + static Future> make( + {required String table, + required VeilidCrypto crypto, + required T Function(List) fromBuffer}) async { + final out = TableDBArrayProtobuf( + table: table, crypto: crypto, fromBuffer: fromBuffer); + await out._initWait(); + return out; + } + + //////////////////////////////////////////////////////////// + // Public interface + + Future add(T value) => _add(value.writeToBuffer()); + + Future addAll(List values) async => + _addAll(values.map((x) => x.writeToBuffer()).toList()); + + Future insert(int pos, T value) async => + _insert(pos, value.writeToBuffer()); + + Future insertAll(int pos, List values) async => + _insertAll(pos, values.map((x) => x.writeToBuffer()).toList()); + + Future get( + int pos, + ) => + _get(pos).then(_fromBuffer); + + Future> getRange(int start, [int? end]) => + _getRange(start, end).then((out) => out.map(_fromBuffer).toList()); + + Future remove(int pos, {Output? out}) async { + final outProto = (out != null) ? Output() : null; + await _remove(pos, out: outProto); + if (outProto != null && outProto.value != null) { + out!.save(_fromBuffer(outProto.value!)); + } + } + + Future removeRange(int start, int end, {Output>? out}) async { + final outProto = (out != null) ? Output>() : null; + await _removeRange(start, end, out: outProto); + if (outProto != null && outProto.value != null) { + out!.save(outProto.value!.map(_fromBuffer).toList()); + } + } + + //////////////////////////////////////////////////////////////////////////// + final T Function(List) _fromBuffer; } diff --git a/packages/veilid_support/lib/src/table_db_array_cubit.dart b/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart similarity index 81% rename from packages/veilid_support/lib/src/table_db_array_cubit.dart rename to packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart index c4ff507..927ca59 100644 --- a/packages/veilid_support/lib/src/table_db_array_cubit.dart +++ b/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart @@ -6,12 +6,14 @@ import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; 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 TableDBArrayStateData extends Equatable { - const TableDBArrayStateData( +class TableDBArrayProtobufStateData + extends Equatable { + const TableDBArrayProtobufStateData( {required this.elements, required this.tail, required this.count, @@ -30,16 +32,17 @@ class TableDBArrayStateData extends Equatable { List get props => [elements, tail, count, follow]; } -typedef TableDBArrayState = AsyncValue>; -typedef TableDBArrayBusyState = BlocBusyState>; +typedef TableDBArrayProtobufState + = AsyncValue>; +typedef TableDBArrayProtobufBusyState + = BlocBusyState>; -class TableDBArrayCubit extends Cubit> - with BlocBusyWrapper> { - TableDBArrayCubit({ - required Future Function() open, - required T Function(List data) decodeElement, - }) : _decodeElement = decodeElement, - super(const BlocBusyState(AsyncValue.loading())) { +class TableDBArrayProtobufCubit + extends Cubit> + with BlocBusyWrapper> { + TableDBArrayProtobufCubit({ + required Future> Function() open, + }) : super(const BlocBusyState(AsyncValue.loading())) { _initWait.add(() async { // Open table db array _array = await open(); @@ -81,7 +84,7 @@ class TableDBArrayCubit extends Cubit> busy((emit) async => _refreshInner(emit, forceRefresh: forceRefresh)); Future _refreshInner( - void Function(AsyncValue>) emit, + void Function(AsyncValue>) emit, {bool forceRefresh = false}) async { final avElements = await _loadElements(_tail, _count); final err = avElements.asError; @@ -95,7 +98,7 @@ class TableDBArrayCubit extends Cubit> return; } final elements = avElements.asData!.value; - emit(AsyncValue.data(TableDBArrayStateData( + emit(AsyncValue.data(TableDBArrayProtobufStateData( elements: elements, tail: _tail, count: _count, follow: _follow))); } @@ -110,8 +113,7 @@ class TableDBArrayCubit extends Cubit> } final end = ((tail - 1) % length) + 1; final start = (count < end) ? end - count : 0; - final allItems = - (await _array.getRange(start, end)).map(_decodeElement).toIList(); + final allItems = (await _array.getRange(start, end)).toIList(); return AsyncValue.data(allItems); } on Exception catch (e, st) { return AsyncValue.error(e, st); @@ -128,7 +130,7 @@ class TableDBArrayCubit extends Cubit> _headDelta += upd.headDelta; _tailDelta += upd.tailDelta; - _sspUpdate.busyUpdate>(busy, (emit) async { + _sspUpdate.busyUpdate>(busy, (emit) async { // apply follow if (_follow) { if (_tail <= 0) { @@ -165,14 +167,14 @@ class TableDBArrayCubit extends Cubit> await super.close(); } - Future operate(Future Function(TableDBArray) closure) async { + Future operate( + Future Function(TableDBArrayProtobuf) closure) async { await _initWait(); return closure(_array); } final WaitSet _initWait = WaitSet(); - late final TableDBArray _array; - final T Function(List data) _decodeElement; + late final TableDBArrayProtobuf _array; StreamSubscription? _subscription; bool _wantsCloseArray = false; final _sspUpdate = SingleStatelessProcessor(); diff --git a/packages/veilid_support/lib/veilid_support.dart b/packages/veilid_support/lib/veilid_support.dart index 0f19bf3..6d10049 100644 --- a/packages/veilid_support/lib/veilid_support.dart +++ b/packages/veilid_support/lib/veilid_support.dart @@ -16,6 +16,6 @@ export 'src/persistent_queue.dart'; export 'src/protobuf_tools.dart'; export 'src/table_db.dart'; export 'src/table_db_array.dart'; -export 'src/table_db_array_cubit.dart'; +export 'src/table_db_array_protobuf_cubit.dart'; export 'src/veilid_crypto.dart'; export 'src/veilid_log.dart' hide veilidLoggy; diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index db70f07..f74ee28 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -36,10 +36,9 @@ packages: async_tools: dependency: "direct main" description: - name: async_tools - sha256: e783ac6ed5645c86da34240389bb3a000fc5e3ae6589c6a482eb24ece7217681 - url: "https://pub.dev" - source: hosted + path: "../../../dart_async_tools" + relative: true + source: path version: "0.1.1" bloc: dependency: "direct main" @@ -52,10 +51,9 @@ packages: bloc_advanced_tools: dependency: "direct main" description: - name: bloc_advanced_tools - sha256: "09f8a121d950575f1f2980c8b10df46b2ac6c72c8cbe48cc145871e5882ed430" - url: "https://pub.dev" - source: hosted + path: "../../../bloc_advanced_tools" + relative: true + source: path version: "0.1.1" boolean_selector: dependency: transitive diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index e598f8c..eeab762 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -24,6 +24,12 @@ dependencies: # veilid: ^0.0.1 path: ../../../veilid/veilid-flutter +dependency_overrides: + async_tools: + path: ../../../dart_async_tools + bloc_advanced_tools: + path: ../../../bloc_advanced_tools + dev_dependencies: build_runner: ^2.4.10 freezed: ^2.5.2 From 4082d1dd767636039b405a487aa1e69146889d27 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 2 Jun 2024 12:53:52 -0400 Subject: [PATCH 124/270] bug cleanup --- .../reconciliation/author_input_queue.dart | 12 ++++--- .../message_reconciliation.dart | 36 +++++++++---------- .../src/dht_log/dht_log_spine.dart | 8 +++-- .../dht_short_array/dht_short_array_head.dart | 9 +++-- .../lib/src/table_db_array.dart | 6 ++-- 5 files changed, 41 insertions(+), 30 deletions(-) diff --git a/lib/chat/cubits/reconciliation/author_input_queue.dart b/lib/chat/cubits/reconciliation/author_input_queue.dart index 009604d..d7be3eb 100644 --- a/lib/chat/cubits/reconciliation/author_input_queue.dart +++ b/lib/chat/cubits/reconciliation/author_input_queue.dart @@ -109,7 +109,7 @@ class AuthorInputQueue { // Internal implementation // Walk backward from the tail of the input queue to find the first - // message newer than our last reconcicled message from this author + // message newer than our last reconciled message from this author // Returns false if no work is needed Future _findStartOfWork() async { // Iterate windows over the inputSource @@ -121,10 +121,14 @@ class AuthorInputQueue { i--, _currentPosition--) { final elem = _inputSource.currentWindow.elements[i]; - // If we've found an input element that is older than our last - // reconciled message for this author, then we stop + // If we've found an input element that is older or same time as our + // last reconciled message for this author, or we find the message + // itself then we stop if (_lastMessage != null) { - if (elem.value.timestamp < _lastMessage!.timestamp) { + if (elem.value.authorUniqueIdBytes + .compare(_lastMessage!.authorUniqueIdBytes) == + 0 || + elem.value.timestamp <= _lastMessage!.timestamp) { break outer; } } diff --git a/lib/chat/cubits/reconciliation/message_reconciliation.dart b/lib/chat/cubits/reconciliation/message_reconciliation.dart index aa53f49..f0b8c4c 100644 --- a/lib/chat/cubits/reconciliation/message_reconciliation.dart +++ b/lib/chat/cubits/reconciliation/message_reconciliation.dart @@ -75,8 +75,7 @@ class MessageReconciliation { return inputQueue; } - // Get the position of our most recent - // reconciled message from this author + // Get the position of our most recent reconciled message from this author // XXX: For a group chat, this should find when the author // was added to the membership so we don't just go back in time forever Future _findLastOutputPosition( @@ -85,9 +84,6 @@ class MessageReconciliation { var pos = arr.length - 1; while (pos >= 0) { final message = await arr.get(pos); - if (message == null) { - throw StateError('should have gotten last message'); - } if (message.content.author.toVeilid() == author) { return OutputPosition(message, pos); } @@ -120,13 +116,7 @@ class MessageReconciliation { }); // Start at the earliest position we know about in all the queues - final firstOutputPos = inputQueues.first.outputPosition?.pos; - // Get the timestamp for this output position - var currentOutputMessage = firstOutputPos == null - ? null - : await reconciledArray.get(firstOutputPos); - - var currentOutputPos = firstOutputPos ?? 0; + var currentOutputPosition = inputQueues.first.outputPosition; final toInsert = SortedList(proto.MessageExt.compareTimestamp); @@ -141,8 +131,9 @@ class MessageReconciliation { var someQueueEmpty = false; for (final inputQueue in inputQueues) { final inputCurrent = inputQueue.current!; - if (currentOutputMessage == null || - inputCurrent.timestamp < currentOutputMessage.content.timestamp) { + if (currentOutputPosition == null || + inputCurrent.timestamp < + currentOutputPosition.message.content.timestamp) { toInsert.add(inputCurrent); added = true; @@ -174,15 +165,22 @@ class MessageReconciliation { ..content = message) .toList(); - await reconciledArray.insertAll(currentOutputPos, reconciledInserts); + await reconciledArray.insertAll( + currentOutputPosition?.pos ?? reconciledArray.length, + reconciledInserts); toInsert.clear(); } else { // If there's nothing to insert at this position move to the next one - currentOutputPos++; - currentOutputMessage = (currentOutputPos == reconciledArray.length) - ? null - : await reconciledArray.get(currentOutputPos); + final nextOutputPos = (currentOutputPosition != null) + ? currentOutputPosition.pos + 1 + : reconciledArray.length; + if (nextOutputPos == reconciledArray.length) { + currentOutputPosition = null; + } else { + currentOutputPosition = OutputPosition( + await reconciledArray.get(nextOutputPos), nextOutputPos); + } } } } diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index 6b5665d..bad7f80 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -200,8 +200,12 @@ class _DHTLogSpine { throw TimeoutException('timeout reached'); } } - if (await closure(this)) { - break; + try { + if (await closure(this)) { + break; + } + } on DHTExceptionTryAgain { + // } // Failed to write in closure resets state _head = oldHead; 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 501892d..68c2a18 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 @@ -139,9 +139,14 @@ class _DHTShortArrayHead { throw TimeoutException('timeout reached'); } } - if (await closure(this)) { - break; + try { + if (await closure(this)) { + break; + } + } on DHTExceptionTryAgain { + // } + // Failed to write in closure resets state _linkedRecords = List.of(oldLinkedRecords); _index = List.of(oldIndex); diff --git a/packages/veilid_support/lib/src/table_db_array.dart b/packages/veilid_support/lib/src/table_db_array.dart index bc0eca5..ad4c586 100644 --- a/packages/veilid_support/lib/src/table_db_array.dart +++ b/packages/veilid_support/lib/src/table_db_array.dart @@ -710,10 +710,10 @@ class TableDBArrayJson extends _TableDBArrayBase { Future insertAll(int pos, List values) async => _insertAll(pos, values.map(jsonEncodeBytes).toList()); - Future get( + Future get( int pos, ) => - _get(pos).then((out) => jsonDecodeOptBytes(_fromJson, out)); + _get(pos).then((out) => jsonDecodeBytes(_fromJson, out)); Future> getRange(int start, [int? end]) => _getRange(start, end).then((out) => out.map(_fromJson).toList()); @@ -773,7 +773,7 @@ class TableDBArrayProtobuf Future insertAll(int pos, List values) async => _insertAll(pos, values.map((x) => x.writeToBuffer()).toList()); - Future get( + Future get( int pos, ) => _get(pos).then(_fromBuffer); From 5473bd2ee40043ae5169bee2e0a207d339e88323 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 3 Jun 2024 21:20:00 -0400 Subject: [PATCH 125/270] pagination work --- .../cubits/single_contact_messages_cubit.dart | 60 +++- lib/chat/models/messages_state.dart | 27 ++ lib/chat/models/messages_state.freezed.dart | 268 ++++++++++++++++++ lib/chat/models/messages_state.g.dart | 28 ++ lib/chat/models/models.dart | 1 + lib/chat/views/chat_component.dart | 42 ++- lib/theme/models/radix_generator.dart | 23 ++ lib/tools/misc.dart | 18 ++ lib/tools/state_logger.dart | 3 +- lib/tools/tools.dart | 1 + .../src/table_db_array_protobuf_cubit.dart | 22 +- 11 files changed, 469 insertions(+), 24 deletions(-) create mode 100644 lib/chat/models/messages_state.dart create mode 100644 lib/chat/models/messages_state.freezed.dart create mode 100644 lib/chat/models/messages_state.g.dart create mode 100644 lib/tools/misc.dart diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index bc2d8c5..9ee68f7 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -2,11 +2,13 @@ import 'dart:async'; import 'package:async_tools/async_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/foundation.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; +import '../../tools/tools.dart'; import '../models/models.dart'; import 'reconciliation/reconciliation.dart'; @@ -42,7 +44,7 @@ class RenderStateElement { bool sentOffline; } -typedef SingleContactMessagesState = AsyncValue>; +typedef SingleContactMessagesState = AsyncValue; // Cubit that processes single-contact chats // Builds the reconciled chat record from the local and remote conversation keys @@ -60,6 +62,7 @@ class SingleContactMessagesCubit extends Cubit { _localMessagesRecordKey = localMessagesRecordKey, _remoteConversationRecordKey = remoteConversationRecordKey, _remoteMessagesRecordKey = remoteMessagesRecordKey, + _commandController = StreamController(), super(const AsyncValue.loading()) { // Async Init _initWait.add(_init); @@ -69,6 +72,8 @@ class SingleContactMessagesCubit extends Cubit { Future close() async { await _initWait(); + await _commandController.close(); + await _commandRunnerFut; await _unsentMessagesQueue.close(); await _sentSubscription?.cancel(); await _rcvdSubscription?.cancel(); @@ -99,6 +104,9 @@ class SingleContactMessagesCubit extends Cubit { // Remote messages key await _initRcvdMessagesCubit(); + + // Command execution background process + _commandRunnerFut = Future.delayed(Duration.zero, _commandRunner); } // Make crypto @@ -191,6 +199,34 @@ class SingleContactMessagesCubit extends Cubit { _sendMessage(message: message); } + // Run a chat command + void runCommand(String command) { + final (cmd, rest) = command.splitOnce(' '); + + if (kDebugMode) { + if (cmd == '/repeat' && rest != null) { + final (countStr, text) = rest.splitOnce(' '); + final count = int.tryParse(countStr); + if (count != null) { + runCommandRepeat(count, text ?? ''); + } + } + } + } + + // Run a repeat command + void runCommandRepeat(int count, String text) { + _commandController.sink.add(() async { + for (var i = 0; i < count; i++) { + final protoMessageText = proto.Message_Text() + ..text = text.replaceAll(RegExp(r'\$n\b'), i.toString()); + final message = proto.Message()..text = protoMessageText; + _sendMessage(message: message); + await Future.delayed(const Duration(milliseconds: 50)); + } + }); + } + //////////////////////////////////////////////////////////////////////////// // Internal implementation @@ -220,9 +256,6 @@ class SingleContactMessagesCubit extends Cubit { _reconciliation.reconcileMessages( _remoteIdentityPublicKey, rcvdMessages, _rcvdMessagesCubit!); - - // Update the view - _renderState(); } // Called when the reconciled messages window gets a change @@ -296,7 +329,7 @@ class SingleContactMessagesCubit extends Cubit { final renderedElements = []; - for (final m in reconciledMessages.elements) { + for (final m in reconciledMessages.windowElements) { final isLocal = m.content.author.toVeilid() == _activeAccountInfo.localAccount.identityMaster .identityPublicTypedKey(); @@ -316,7 +349,7 @@ class SingleContactMessagesCubit extends Cubit { } // Render the state - final renderedState = renderedElements + final messages = renderedElements .map((x) => MessageState( content: x.message, sentTimestamp: Timestamp.fromInt64(x.message.timestamp), @@ -325,7 +358,12 @@ class SingleContactMessagesCubit extends Cubit { .toIList(); // Emit the rendered state - emit(AsyncValue.data(renderedState)); + emit(AsyncValue.data(MessagesState( + windowMessages: messages, + length: reconciledMessages.length, + windowTail: reconciledMessages.windowTail, + windowCount: reconciledMessages.windowCount, + follow: reconciledMessages.follow))); } void _sendMessage({required proto.Message message}) { @@ -344,6 +382,12 @@ class SingleContactMessagesCubit extends Cubit { _renderState(); } + Future _commandRunner() async { + await for (final command in _commandController.stream) { + await command(); + } + } + ///////////////////////////////////////////////////////////////////////// // Static utility functions @@ -383,4 +427,6 @@ class SingleContactMessagesCubit extends Cubit { StreamSubscription>? _rcvdSubscription; StreamSubscription>? _reconciledSubscription; + final StreamController Function()> _commandController; + late final Future _commandRunnerFut; } diff --git a/lib/chat/models/messages_state.dart b/lib/chat/models/messages_state.dart new file mode 100644 index 0000000..4a08376 --- /dev/null +++ b/lib/chat/models/messages_state.dart @@ -0,0 +1,27 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import 'message_state.dart'; + +part 'messages_state.freezed.dart'; +part 'messages_state.g.dart'; + +@freezed +class MessagesState with _$MessagesState { + const factory MessagesState({ + // List of messages in the window + required IList windowMessages, + // Total number of messages + required int length, + // One past the end of the last element + required int windowTail, + // The total number of elements to try to keep in 'messages' + required int windowCount, + // If we should have the tail following the array + required bool follow, + }) = _MessagesState; + + factory MessagesState.fromJson(dynamic json) => + _$MessagesStateFromJson(json as Map); +} diff --git a/lib/chat/models/messages_state.freezed.dart b/lib/chat/models/messages_state.freezed.dart new file mode 100644 index 0000000..368ca94 --- /dev/null +++ b/lib/chat/models/messages_state.freezed.dart @@ -0,0 +1,268 @@ +// 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 'messages_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#adding-getters-and-methods-to-our-models'); + +MessagesState _$MessagesStateFromJson(Map json) { + return _MessagesState.fromJson(json); +} + +/// @nodoc +mixin _$MessagesState { +// List of messages in the window + IList get windowMessages => + throw _privateConstructorUsedError; // Total number of messages + int get length => + throw _privateConstructorUsedError; // One past the end of the last element + int get windowTail => + throw _privateConstructorUsedError; // The total number of elements to try to keep in 'messages' + int get windowCount => + throw _privateConstructorUsedError; // If we should have the tail following the array + bool get follow => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $MessagesStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $MessagesStateCopyWith<$Res> { + factory $MessagesStateCopyWith( + MessagesState value, $Res Function(MessagesState) then) = + _$MessagesStateCopyWithImpl<$Res, MessagesState>; + @useResult + $Res call( + {IList windowMessages, + int length, + int windowTail, + int windowCount, + bool follow}); +} + +/// @nodoc +class _$MessagesStateCopyWithImpl<$Res, $Val extends MessagesState> + implements $MessagesStateCopyWith<$Res> { + _$MessagesStateCopyWithImpl(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? windowMessages = null, + Object? length = null, + Object? windowTail = null, + Object? windowCount = null, + Object? follow = null, + }) { + return _then(_value.copyWith( + windowMessages: null == windowMessages + ? _value.windowMessages + : windowMessages // ignore: cast_nullable_to_non_nullable + as IList, + length: null == length + ? _value.length + : length // ignore: cast_nullable_to_non_nullable + as int, + windowTail: null == windowTail + ? _value.windowTail + : windowTail // ignore: cast_nullable_to_non_nullable + as int, + windowCount: null == windowCount + ? _value.windowCount + : windowCount // ignore: cast_nullable_to_non_nullable + as int, + follow: null == follow + ? _value.follow + : follow // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$MessagesStateImplCopyWith<$Res> + implements $MessagesStateCopyWith<$Res> { + factory _$$MessagesStateImplCopyWith( + _$MessagesStateImpl value, $Res Function(_$MessagesStateImpl) then) = + __$$MessagesStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {IList windowMessages, + int length, + int windowTail, + int windowCount, + bool follow}); +} + +/// @nodoc +class __$$MessagesStateImplCopyWithImpl<$Res> + extends _$MessagesStateCopyWithImpl<$Res, _$MessagesStateImpl> + implements _$$MessagesStateImplCopyWith<$Res> { + __$$MessagesStateImplCopyWithImpl( + _$MessagesStateImpl _value, $Res Function(_$MessagesStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? windowMessages = null, + Object? length = null, + Object? windowTail = null, + Object? windowCount = null, + Object? follow = null, + }) { + return _then(_$MessagesStateImpl( + windowMessages: null == windowMessages + ? _value.windowMessages + : windowMessages // ignore: cast_nullable_to_non_nullable + as IList, + length: null == length + ? _value.length + : length // ignore: cast_nullable_to_non_nullable + as int, + windowTail: null == windowTail + ? _value.windowTail + : windowTail // ignore: cast_nullable_to_non_nullable + as int, + windowCount: null == windowCount + ? _value.windowCount + : windowCount // ignore: cast_nullable_to_non_nullable + as int, + follow: null == follow + ? _value.follow + : follow // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$MessagesStateImpl + with DiagnosticableTreeMixin + implements _MessagesState { + const _$MessagesStateImpl( + {required this.windowMessages, + required this.length, + required this.windowTail, + required this.windowCount, + required this.follow}); + + factory _$MessagesStateImpl.fromJson(Map json) => + _$$MessagesStateImplFromJson(json); + +// List of messages in the window + @override + final IList windowMessages; +// Total number of messages + @override + final int length; +// One past the end of the last element + @override + final int windowTail; +// The total number of elements to try to keep in 'messages' + @override + final int windowCount; +// If we should have the tail following the array + @override + final bool follow; + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'MessagesState(windowMessages: $windowMessages, length: $length, windowTail: $windowTail, windowCount: $windowCount, follow: $follow)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'MessagesState')) + ..add(DiagnosticsProperty('windowMessages', windowMessages)) + ..add(DiagnosticsProperty('length', length)) + ..add(DiagnosticsProperty('windowTail', windowTail)) + ..add(DiagnosticsProperty('windowCount', windowCount)) + ..add(DiagnosticsProperty('follow', follow)); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$MessagesStateImpl && + const DeepCollectionEquality() + .equals(other.windowMessages, windowMessages) && + (identical(other.length, length) || other.length == length) && + (identical(other.windowTail, windowTail) || + other.windowTail == windowTail) && + (identical(other.windowCount, windowCount) || + other.windowCount == windowCount) && + (identical(other.follow, follow) || other.follow == follow)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(windowMessages), + length, + windowTail, + windowCount, + follow); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$MessagesStateImplCopyWith<_$MessagesStateImpl> get copyWith => + __$$MessagesStateImplCopyWithImpl<_$MessagesStateImpl>(this, _$identity); + + @override + Map toJson() { + return _$$MessagesStateImplToJson( + this, + ); + } +} + +abstract class _MessagesState implements MessagesState { + const factory _MessagesState( + {required final IList windowMessages, + required final int length, + required final int windowTail, + required final int windowCount, + required final bool follow}) = _$MessagesStateImpl; + + factory _MessagesState.fromJson(Map json) = + _$MessagesStateImpl.fromJson; + + @override // List of messages in the window + IList get windowMessages; + @override // Total number of messages + int get length; + @override // One past the end of the last element + int get windowTail; + @override // The total number of elements to try to keep in 'messages' + int get windowCount; + @override // If we should have the tail following the array + bool get follow; + @override + @JsonKey(ignore: true) + _$$MessagesStateImplCopyWith<_$MessagesStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/chat/models/messages_state.g.dart b/lib/chat/models/messages_state.g.dart new file mode 100644 index 0000000..cf44e5b --- /dev/null +++ b/lib/chat/models/messages_state.g.dart @@ -0,0 +1,28 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'messages_state.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$MessagesStateImpl _$$MessagesStateImplFromJson(Map json) => + _$MessagesStateImpl( + windowMessages: IList.fromJson( + json['window_messages'], (value) => MessageState.fromJson(value)), + length: (json['length'] as num).toInt(), + windowTail: (json['window_tail'] as num).toInt(), + windowCount: (json['window_count'] as num).toInt(), + follow: json['follow'] as bool, + ); + +Map _$$MessagesStateImplToJson(_$MessagesStateImpl instance) => + { + 'window_messages': instance.windowMessages.toJson( + (value) => value.toJson(), + ), + 'length': instance.length, + 'window_tail': instance.windowTail, + 'window_count': instance.windowCount, + 'follow': instance.follow, + }; diff --git a/lib/chat/models/models.dart b/lib/chat/models/models.dart index 2d92e01..3620563 100644 --- a/lib/chat/models/models.dart +++ b/lib/chat/models/models.dart @@ -1 +1,2 @@ export 'message_state.dart'; +export 'messages_state.dart'; diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 327b82e..ee339d3 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -1,8 +1,9 @@ -import 'dart:typed_data'; +import 'dart:math'; import 'package:async_tools/async_tools.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:fixnum/fixnum.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; @@ -166,8 +167,9 @@ class ChatComponent extends StatelessWidget { _messagesCubit.sendTextMessage(messageText: protoMessageText); } - void _handleSendPressed(types.PartialText message) { + void _sendMessage(types.PartialText message) { final text = message.text; + final replyId = (message.repliedMessage != null) ? base64UrlNoPadDecode(message.repliedMessage!.id) : null; @@ -200,6 +202,17 @@ class ChatComponent extends StatelessWidget { attachments: attachments ?? []); } + void _handleSendPressed(types.PartialText message) { + final text = message.text; + + if (text.startsWith('/')) { + _messagesCubit.runCommand(text); + return; + } + + _sendMessage(message); + } + // void _handleAttachmentPressed() async { // // // } @@ -211,15 +224,15 @@ class ChatComponent extends StatelessWidget { final textTheme = Theme.of(context).textTheme; final chatTheme = makeChatTheme(scale, textTheme); - final messages = _messagesState.asData?.value; - if (messages == null) { + final messagesState = _messagesState.asData?.value; + if (messagesState == null) { return _messagesState.buildNotData(); } // Convert protobuf messages to chat messages final chatMessages = []; final tsSet = {}; - for (final message in messages) { + for (final message in messagesState.windowMessages) { final chatMessage = messageStateToChatMessage(message); if (chatMessage == null) { continue; @@ -228,12 +241,17 @@ class ChatComponent extends StatelessWidget { if (!tsSet.add(chatMessage.id)) { // ignore: avoid_print print('duplicate id found: ${chatMessage.id}:\n' - 'Messages:\n$messages\n' + 'Messages:\n${messagesState.windowMessages}\n' 'ChatMessages:\n$chatMessages'); assert(false, 'should not have duplicate id'); } } + final isLastPage = + (messagesState.windowTail - messagesState.windowMessages.length) <= 0; + final follow = messagesState.windowTail == 0 || + messagesState.windowTail == messagesState.length; xxx finish calculating pagination and get scroll position here somehow + return DefaultTextStyle( style: textTheme.bodySmall!, child: Align( @@ -272,9 +290,17 @@ class ChatComponent extends StatelessWidget { decoration: const BoxDecoration(), child: Chat( theme: chatTheme, - // emojiEnlargementBehavior: - // EmojiEnlargementBehavior.multi, messages: chatMessages, + onEndReached: () async { + final tail = await _messagesCubit.setWindow( + tail: max( + 0, + (messagesState.windowTail - + (messagesState.windowCount ~/ 2))), + count: messagesState.windowCount, + follow: follow); + }, + isLastPage: isLastPage, //onAttachmentPressed: _handleAttachmentPressed, //onMessageTap: _handleMessageTap, //onPreviewDataFetched: _handlePreviewDataFetched, diff --git a/lib/theme/models/radix_generator.dart b/lib/theme/models/radix_generator.dart index b1f510a..4bd593f 100644 --- a/lib/theme/models/radix_generator.dart +++ b/lib/theme/models/radix_generator.dart @@ -609,6 +609,29 @@ ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) { final themeData = ThemeData.from( colorScheme: colorScheme, textTheme: textTheme, useMaterial3: true); return themeData.copyWith( + scrollbarTheme: themeData.scrollbarTheme.copyWith( + thumbColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.pressed)) { + return scaleScheme.primaryScale.border; + } else if (states.contains(WidgetState.hovered)) { + return scaleScheme.primaryScale.hoverBorder; + } + return scaleScheme.primaryScale.subtleBorder; + }), trackColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.pressed)) { + return scaleScheme.primaryScale.activeElementBackground; + } else if (states.contains(WidgetState.hovered)) { + return scaleScheme.primaryScale.hoverElementBackground; + } + return scaleScheme.primaryScale.elementBackground; + }), trackBorderColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.pressed)) { + return scaleScheme.primaryScale.subtleBorder; + } else if (states.contains(WidgetState.hovered)) { + return scaleScheme.primaryScale.subtleBorder; + } + return scaleScheme.primaryScale.subtleBorder; + })), bottomSheetTheme: themeData.bottomSheetTheme.copyWith( elevation: 0, modalElevation: 0, diff --git a/lib/tools/misc.dart b/lib/tools/misc.dart new file mode 100644 index 0000000..01dcbc0 --- /dev/null +++ b/lib/tools/misc.dart @@ -0,0 +1,18 @@ +extension StringExt on String { + (String, String?) splitOnce(Pattern p) { + final pos = indexOf(p); + if (pos == -1) { + return (this, null); + } + final rest = substring(pos); + var offset = 0; + while (true) { + final match = p.matchAsPrefix(rest, offset); + if (match == null) { + break; + } + offset = match.end; + } + return (substring(0, pos), rest.substring(offset)); + } +} diff --git a/lib/tools/state_logger.dart b/lib/tools/state_logger.dart index 4c8e17a..db0ea2a 100644 --- a/lib/tools/state_logger.dart +++ b/lib/tools/state_logger.dart @@ -6,8 +6,9 @@ const Map _blocChangeLogLevels = { 'ConnectionStateCubit': LogLevel.off, 'ActiveSingleContactChatBlocMapCubit': LogLevel.off, 'ActiveConversationsBlocMapCubit': LogLevel.off, - 'DHTShortArrayCubit': LogLevel.off, 'PersistentQueueCubit': LogLevel.off, + 'TableDBArrayProtobufCubit': LogLevel.off, + 'DHTLogCubit': LogLevel.off, 'SingleContactMessagesCubit': LogLevel.off, }; const Map _blocCreateCloseLogLevels = {}; diff --git a/lib/tools/tools.dart b/lib/tools/tools.dart index c556f98..6b48001 100644 --- a/lib/tools/tools.dart +++ b/lib/tools/tools.dart @@ -2,6 +2,7 @@ export 'animations.dart'; export 'enter_password.dart'; export 'enter_pin.dart'; export 'loggy.dart'; +export 'misc.dart'; export 'phono_byte.dart'; export 'pop_control.dart'; export 'responsive.dart'; diff --git a/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart b/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart index 927ca59..702a2ad 100644 --- a/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart +++ b/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart @@ -14,22 +14,25 @@ import '../../../veilid_support.dart'; class TableDBArrayProtobufStateData extends Equatable { const TableDBArrayProtobufStateData( - {required this.elements, - required this.tail, - required this.count, + {required this.windowElements, + required this.length, + required this.windowTail, + required this.windowCount, required this.follow}); // The view of the elements in the dhtlog // Span is from [tail-length, tail) - final IList elements; + final IList windowElements; + // The length of the entire array + final int length; // One past the end of the last element - final int tail; + final int windowTail; // The total number of elements to try to keep in 'elements' - final int count; + final int windowCount; // If we should have the tail following the array final bool follow; @override - List get props => [elements, tail, count, follow]; + List get props => [windowElements, windowTail, windowCount, follow]; } typedef TableDBArrayProtobufState @@ -99,7 +102,10 @@ class TableDBArrayProtobufCubit } final elements = avElements.asData!.value; emit(AsyncValue.data(TableDBArrayProtobufStateData( - elements: elements, tail: _tail, count: _count, follow: _follow))); + windowElements: elements, + windowTail: _tail, + windowCount: _count, + follow: _follow))); } Future>> _loadElements( From 2c38fc64894a74efa312bc549dbb237378fc3538 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 6 Jun 2024 00:19:07 -0400 Subject: [PATCH 126/270] pagination --- .../models/active_account_info.dart | 2 - lib/chat/cubits/active_chat_cubit.dart | 3 + lib/chat/cubits/chat_component_cubit.dart | 272 +++++++++++++++ lib/chat/cubits/cubits.dart | 1 + .../cubits/single_contact_messages_cubit.dart | 9 +- lib/chat/models/chat_component_state.dart | 34 ++ .../models/chat_component_state.freezed.dart | 267 +++++++++++++++ lib/chat/models/messages_state.dart | 27 -- lib/chat/models/messages_state.g.dart | 28 -- lib/chat/models/models.dart | 3 +- lib/chat/models/window_state.dart | 27 ++ ...freezed.dart => window_state.freezed.dart} | 147 ++++---- lib/chat/views/chat_component.dart | 320 ------------------ lib/chat/views/chat_component_widget.dart | 294 ++++++++++++++++ lib/chat/views/views.dart | 2 +- .../home_account_ready_chat.dart | 5 +- .../home_account_ready_main.dart | 15 +- lib/tools/state_logger.dart | 2 + .../src/dht_log/dht_log_write.dart | 22 +- .../src/table_db_array_protobuf_cubit.dart | 1 + pubspec.lock | 95 +++--- pubspec.yaml | 40 ++- 22 files changed, 1071 insertions(+), 545 deletions(-) create mode 100644 lib/chat/cubits/chat_component_cubit.dart create mode 100644 lib/chat/models/chat_component_state.dart create mode 100644 lib/chat/models/chat_component_state.freezed.dart delete mode 100644 lib/chat/models/messages_state.dart delete mode 100644 lib/chat/models/messages_state.g.dart create mode 100644 lib/chat/models/window_state.dart rename lib/chat/models/{messages_state.freezed.dart => window_state.freezed.dart} (59%) delete mode 100644 lib/chat/views/chat_component.dart create mode 100644 lib/chat/views/chat_component_widget.dart diff --git a/lib/account_manager/models/active_account_info.dart b/lib/account_manager/models/active_account_info.dart index e4a5beb..0e1a0ef 100644 --- a/lib/account_manager/models/active_account_info.dart +++ b/lib/account_manager/models/active_account_info.dart @@ -11,7 +11,6 @@ class ActiveAccountInfo { const ActiveAccountInfo({ required this.localAccount, required this.userLogin, - //required this.accountRecord, }); // @@ -41,5 +40,4 @@ class ActiveAccountInfo { // final LocalAccount localAccount; final UserLogin userLogin; - //final DHTRecord accountRecord; } diff --git a/lib/chat/cubits/active_chat_cubit.dart b/lib/chat/cubits/active_chat_cubit.dart index a1872c2..2e72abc 100644 --- a/lib/chat/cubits/active_chat_cubit.dart +++ b/lib/chat/cubits/active_chat_cubit.dart @@ -1,6 +1,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:veilid_support/veilid_support.dart'; +// XXX: if we ever want to have more than one chat 'open', we should put the +// operations and state for that here. + class ActiveChatCubit extends Cubit { ActiveChatCubit(super.initialState); diff --git a/lib/chat/cubits/chat_component_cubit.dart b/lib/chat/cubits/chat_component_cubit.dart new file mode 100644 index 0000000..6ac2726 --- /dev/null +++ b/lib/chat/cubits/chat_component_cubit.dart @@ -0,0 +1,272 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:async_tools/async_tools.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:fixnum/fixnum.dart'; +import 'package:flutter/widgets.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:scroll_to_index/scroll_to_index.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../account_manager/account_manager.dart'; +import '../../chat_list/chat_list.dart'; +import '../../proto/proto.dart' as proto; +import '../models/chat_component_state.dart'; +import '../models/message_state.dart'; +import '../models/window_state.dart'; +import 'cubits.dart'; + +const metadataKeyIdentityPublicKey = 'identityPublicKey'; +const metadataKeyExpirationDuration = 'expiration'; +const metadataKeyViewLimit = 'view_limit'; +const metadataKeyAttachments = 'attachments'; + +class ChatComponentCubit extends Cubit { + ChatComponentCubit._({ + required SingleContactMessagesCubit messagesCubit, + required types.User localUser, + required IMap remoteUsers, + }) : _messagesCubit = messagesCubit, + super(ChatComponentState( + chatKey: GlobalKey(), + scrollController: AutoScrollController(), + localUser: localUser, + remoteUsers: remoteUsers, + messageWindow: const AsyncLoading(), + title: '', + )) { + // Async Init + _initWait.add(_init); + } + + // ignore: prefer_constructors_over_static_methods + static ChatComponentCubit singleContact( + {required ActiveAccountInfo activeAccountInfo, + required proto.Account accountRecordInfo, + required ActiveConversationState activeConversationState, + required SingleContactMessagesCubit messagesCubit}) { + // Make local 'User' + final localUserIdentityKey = + activeAccountInfo.localAccount.identityMaster.identityPublicTypedKey(); + final localUser = types.User( + id: localUserIdentityKey.toString(), + firstName: accountRecordInfo.profile.name, + metadata: {metadataKeyIdentityPublicKey: localUserIdentityKey}); + // Make remote 'User's + final remoteUsers = { + activeConversationState.contact.identityPublicKey.toVeilid(): types.User( + id: activeConversationState.contact.identityPublicKey + .toVeilid() + .toString(), + firstName: activeConversationState.contact.editedProfile.name, + metadata: { + metadataKeyIdentityPublicKey: + activeConversationState.contact.identityPublicKey.toVeilid() + }) + }.toIMap(); + + return ChatComponentCubit._( + messagesCubit: messagesCubit, + localUser: localUser, + remoteUsers: remoteUsers, + ); + } + + Future _init() async { + _messagesSubscription = _messagesCubit.stream.listen((messagesState) { + emit(state.copyWith( + messageWindow: _convertMessages(messagesState), + )); + }); + emit(state.copyWith( + messageWindow: _convertMessages(_messagesCubit.state), + title: _getTitle(), + )); + } + + @override + Future close() async { + await _initWait(); + await _messagesSubscription.cancel(); + await super.close(); + } + + //////////////////////////////////////////////////////////////////////////// + // Public Interface + + // Set the tail position of the log for pagination. + // If tail is 0, the end of the log is used. + // If tail is negative, the position is subtracted from the current log + // length. + // If tail is positive, the position is absolute from the head of the log + // If follow is enabled, the tail offset will update when the log changes + Future setWindow( + {int? tail, int? count, bool? follow, bool forceRefresh = false}) async { + //await _initWait(); + await _messagesCubit.setWindow( + tail: tail, count: count, follow: follow, forceRefresh: forceRefresh); + } + + // Send a message + void sendMessage(types.PartialText message) { + final text = message.text; + + final replyId = (message.repliedMessage != null) + ? base64UrlNoPadDecode(message.repliedMessage!.id) + : null; + Timestamp? expiration; + int? viewLimit; + List? attachments; + final metadata = message.metadata; + if (metadata != null) { + final expirationValue = + metadata[metadataKeyExpirationDuration] as TimestampDuration?; + if (expirationValue != null) { + expiration = Veilid.instance.now().offset(expirationValue); + } + final viewLimitValue = metadata[metadataKeyViewLimit] as int?; + if (viewLimitValue != null) { + viewLimit = viewLimitValue; + } + final attachmentsValue = + metadata[metadataKeyAttachments] as List?; + if (attachmentsValue != null) { + attachments = attachmentsValue; + } + } + + _addTextMessage( + text: text, + replyId: replyId, + expiration: expiration, + viewLimit: viewLimit, + attachments: attachments ?? []); + } + + // Run a chat command + void runCommand(String command) { + _messagesCubit.runCommand(command); + } + + //////////////////////////////////////////////////////////////////////////// + // Private Implementation + + String _getTitle() { + if (state.remoteUsers.length == 1) { + final remoteUser = state.remoteUsers.values.first; + return remoteUser.firstName ?? ''; + } else { + return ''; + } + } + + types.Message? _messageStateToChatMessage(MessageState message) { + final authorIdentityPublicKey = message.content.author.toVeilid(); + final author = + state.remoteUsers[authorIdentityPublicKey] ?? state.localUser; + + types.Status? status; + if (message.sendState != null) { + assert(author == state.localUser, + 'send state should only be on sent messages'); + switch (message.sendState!) { + case MessageSendState.sending: + status = types.Status.sending; + case MessageSendState.sent: + status = types.Status.sent; + case MessageSendState.delivered: + status = types.Status.delivered; + } + } + + switch (message.content.whichKind()) { + case proto.Message_Kind.text: + final contextText = message.content.text; + final textMessage = types.TextMessage( + author: author, + createdAt: + (message.sentTimestamp.value ~/ BigInt.from(1000)).toInt(), + id: message.content.authorUniqueIdString, + text: contextText.text, + showStatus: status != null, + status: status); + return textMessage; + case proto.Message_Kind.secret: + case proto.Message_Kind.delete: + case proto.Message_Kind.erase: + case proto.Message_Kind.settings: + case proto.Message_Kind.permissions: + case proto.Message_Kind.membership: + case proto.Message_Kind.moderation: + case proto.Message_Kind.notSet: + return null; + } + } + + AsyncValue> _convertMessages( + AsyncValue> avMessagesState) { + final asError = avMessagesState.asError; + if (asError != null) { + return AsyncValue.error(asError.error, asError.stackTrace); + } else if (avMessagesState.asLoading != null) { + return const AsyncValue.loading(); + } + final messagesState = avMessagesState.asData!.value; + + // Convert protobuf messages to chat messages + final chatMessages = []; + final tsSet = {}; + for (final message in messagesState.window) { + final chatMessage = _messageStateToChatMessage(message); + if (chatMessage == null) { + continue; + } + chatMessages.insert(0, chatMessage); + if (!tsSet.add(chatMessage.id)) { + // ignore: avoid_print + print('duplicate id found: ${chatMessage.id}:\n' + 'Messages:\n${messagesState.window}\n' + 'ChatMessages:\n$chatMessages'); + assert(false, 'should not have duplicate id'); + } + } + return AsyncValue.data(WindowState( + window: chatMessages.toIList(), + length: messagesState.length, + windowTail: messagesState.windowTail, + windowCount: messagesState.windowCount, + follow: messagesState.follow)); + } + + void _addTextMessage( + {required String text, + String? topic, + Uint8List? replyId, + Timestamp? expiration, + int? viewLimit, + List attachments = const []}) { + final protoMessageText = proto.Message_Text()..text = text; + if (topic != null) { + protoMessageText.topic = topic; + } + if (replyId != null) { + protoMessageText.replyId = replyId; + } + protoMessageText + ..expiration = expiration?.toInt64() ?? Int64.ZERO + ..viewLimit = viewLimit ?? 0; + protoMessageText.attachments.addAll(attachments); + + _messagesCubit.sendTextMessage(messageText: protoMessageText); + } + + //////////////////////////////////////////////////////////////////////////// + + final _initWait = WaitSet(); + final SingleContactMessagesCubit _messagesCubit; + late StreamSubscription _messagesSubscription; + double scrollOffset = 0; +} diff --git a/lib/chat/cubits/cubits.dart b/lib/chat/cubits/cubits.dart index b80767f..ae6b95d 100644 --- a/lib/chat/cubits/cubits.dart +++ b/lib/chat/cubits/cubits.dart @@ -1,2 +1,3 @@ export 'active_chat_cubit.dart'; +export 'chat_component_cubit.dart'; export 'single_contact_messages_cubit.dart'; diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 9ee68f7..3fc8e91 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -44,7 +44,7 @@ class RenderStateElement { bool sentOffline; } -typedef SingleContactMessagesState = AsyncValue; +typedef SingleContactMessagesState = AsyncValue>; // Cubit that processes single-contact chats // Builds the reconciled chat record from the local and remote conversation keys @@ -189,6 +189,9 @@ class SingleContactMessagesCubit extends Cubit { Future setWindow( {int? tail, int? count, bool? follow, bool forceRefresh = false}) async { await _initWait(); + + print('setWindow: tail=$tail count=$count, follow=$follow'); + await _reconciledMessagesCubit!.setWindow( tail: tail, count: count, follow: follow, forceRefresh: forceRefresh); } @@ -358,8 +361,8 @@ class SingleContactMessagesCubit extends Cubit { .toIList(); // Emit the rendered state - emit(AsyncValue.data(MessagesState( - windowMessages: messages, + emit(AsyncValue.data(WindowState( + window: messages, length: reconciledMessages.length, windowTail: reconciledMessages.windowTail, windowCount: reconciledMessages.windowCount, diff --git a/lib/chat/models/chat_component_state.dart b/lib/chat/models/chat_component_state.dart new file mode 100644 index 0000000..b8da8d4 --- /dev/null +++ b/lib/chat/models/chat_component_state.dart @@ -0,0 +1,34 @@ +import 'package:async_tools/async_tools.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_chat_types/flutter_chat_types.dart' show Message, User; +import 'package:flutter_chat_ui/flutter_chat_ui.dart' show ChatState; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:scroll_to_index/scroll_to_index.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import 'window_state.dart'; + +part 'chat_component_state.freezed.dart'; + +@freezed +class ChatComponentState with _$ChatComponentState { + const factory ChatComponentState( + { + // GlobalKey for the chat + required GlobalKey chatKey, + // ScrollController for the chat + required AutoScrollController scrollController, + // Local user + required User localUser, + // Remote users + required IMap remoteUsers, + // Messages state + required AsyncValue> messageWindow, + // Title of the chat + required String title}) = _ChatComponentState; +} + +extension ChatComponentStateExt on ChatComponentState { + // +} diff --git a/lib/chat/models/chat_component_state.freezed.dart b/lib/chat/models/chat_component_state.freezed.dart new file mode 100644 index 0000000..859f363 --- /dev/null +++ b/lib/chat/models/chat_component_state.freezed.dart @@ -0,0 +1,267 @@ +// 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 'chat_component_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#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$ChatComponentState { +// GlobalKey for the chat + GlobalKey get chatKey => + throw _privateConstructorUsedError; // ScrollController for the chat + AutoScrollController get scrollController => + throw _privateConstructorUsedError; // Local user + User get localUser => throw _privateConstructorUsedError; // Remote users + IMap, User> get remoteUsers => + throw _privateConstructorUsedError; // Messages state + AsyncValue> get messageWindow => + throw _privateConstructorUsedError; // Title of the chat + String get title => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $ChatComponentStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ChatComponentStateCopyWith<$Res> { + factory $ChatComponentStateCopyWith( + ChatComponentState value, $Res Function(ChatComponentState) then) = + _$ChatComponentStateCopyWithImpl<$Res, ChatComponentState>; + @useResult + $Res call( + {GlobalKey chatKey, + AutoScrollController scrollController, + User localUser, + IMap, User> remoteUsers, + AsyncValue> messageWindow, + String title}); + + $AsyncValueCopyWith, $Res> get messageWindow; +} + +/// @nodoc +class _$ChatComponentStateCopyWithImpl<$Res, $Val extends ChatComponentState> + implements $ChatComponentStateCopyWith<$Res> { + _$ChatComponentStateCopyWithImpl(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? chatKey = null, + Object? scrollController = null, + Object? localUser = null, + Object? remoteUsers = null, + Object? messageWindow = null, + Object? title = null, + }) { + return _then(_value.copyWith( + chatKey: null == chatKey + ? _value.chatKey + : chatKey // ignore: cast_nullable_to_non_nullable + as GlobalKey, + scrollController: null == scrollController + ? _value.scrollController + : scrollController // ignore: cast_nullable_to_non_nullable + as AutoScrollController, + localUser: null == localUser + ? _value.localUser + : localUser // ignore: cast_nullable_to_non_nullable + as User, + remoteUsers: null == remoteUsers + ? _value.remoteUsers + : remoteUsers // ignore: cast_nullable_to_non_nullable + as IMap, User>, + messageWindow: null == messageWindow + ? _value.messageWindow + : messageWindow // ignore: cast_nullable_to_non_nullable + as AsyncValue>, + title: null == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $AsyncValueCopyWith, $Res> get messageWindow { + return $AsyncValueCopyWith, $Res>(_value.messageWindow, + (value) { + return _then(_value.copyWith(messageWindow: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$ChatComponentStateImplCopyWith<$Res> + implements $ChatComponentStateCopyWith<$Res> { + factory _$$ChatComponentStateImplCopyWith(_$ChatComponentStateImpl value, + $Res Function(_$ChatComponentStateImpl) then) = + __$$ChatComponentStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {GlobalKey chatKey, + AutoScrollController scrollController, + User localUser, + IMap, User> remoteUsers, + AsyncValue> messageWindow, + String title}); + + @override + $AsyncValueCopyWith, $Res> get messageWindow; +} + +/// @nodoc +class __$$ChatComponentStateImplCopyWithImpl<$Res> + extends _$ChatComponentStateCopyWithImpl<$Res, _$ChatComponentStateImpl> + implements _$$ChatComponentStateImplCopyWith<$Res> { + __$$ChatComponentStateImplCopyWithImpl(_$ChatComponentStateImpl _value, + $Res Function(_$ChatComponentStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? chatKey = null, + Object? scrollController = null, + Object? localUser = null, + Object? remoteUsers = null, + Object? messageWindow = null, + Object? title = null, + }) { + return _then(_$ChatComponentStateImpl( + chatKey: null == chatKey + ? _value.chatKey + : chatKey // ignore: cast_nullable_to_non_nullable + as GlobalKey, + scrollController: null == scrollController + ? _value.scrollController + : scrollController // ignore: cast_nullable_to_non_nullable + as AutoScrollController, + localUser: null == localUser + ? _value.localUser + : localUser // ignore: cast_nullable_to_non_nullable + as User, + remoteUsers: null == remoteUsers + ? _value.remoteUsers + : remoteUsers // ignore: cast_nullable_to_non_nullable + as IMap, User>, + messageWindow: null == messageWindow + ? _value.messageWindow + : messageWindow // ignore: cast_nullable_to_non_nullable + as AsyncValue>, + title: null == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$ChatComponentStateImpl implements _ChatComponentState { + const _$ChatComponentStateImpl( + {required this.chatKey, + required this.scrollController, + required this.localUser, + required this.remoteUsers, + required this.messageWindow, + required this.title}); + +// GlobalKey for the chat + @override + final GlobalKey chatKey; +// ScrollController for the chat + @override + final AutoScrollController scrollController; +// Local user + @override + final User localUser; +// Remote users + @override + final IMap, User> remoteUsers; +// Messages state + @override + final AsyncValue> messageWindow; +// Title of the chat + @override + final String title; + + @override + String toString() { + return 'ChatComponentState(chatKey: $chatKey, scrollController: $scrollController, localUser: $localUser, remoteUsers: $remoteUsers, messageWindow: $messageWindow, title: $title)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ChatComponentStateImpl && + (identical(other.chatKey, chatKey) || other.chatKey == chatKey) && + (identical(other.scrollController, scrollController) || + other.scrollController == scrollController) && + (identical(other.localUser, localUser) || + other.localUser == localUser) && + (identical(other.remoteUsers, remoteUsers) || + other.remoteUsers == remoteUsers) && + (identical(other.messageWindow, messageWindow) || + other.messageWindow == messageWindow) && + (identical(other.title, title) || other.title == title)); + } + + @override + int get hashCode => Object.hash(runtimeType, chatKey, scrollController, + localUser, remoteUsers, messageWindow, title); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ChatComponentStateImplCopyWith<_$ChatComponentStateImpl> get copyWith => + __$$ChatComponentStateImplCopyWithImpl<_$ChatComponentStateImpl>( + this, _$identity); +} + +abstract class _ChatComponentState implements ChatComponentState { + const factory _ChatComponentState( + {required final GlobalKey chatKey, + required final AutoScrollController scrollController, + required final User localUser, + required final IMap, User> remoteUsers, + required final AsyncValue> messageWindow, + required final String title}) = _$ChatComponentStateImpl; + + @override // GlobalKey for the chat + GlobalKey get chatKey; + @override // ScrollController for the chat + AutoScrollController get scrollController; + @override // Local user + User get localUser; + @override // Remote users + IMap, User> get remoteUsers; + @override // Messages state + AsyncValue> get messageWindow; + @override // Title of the chat + String get title; + @override + @JsonKey(ignore: true) + _$$ChatComponentStateImplCopyWith<_$ChatComponentStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/chat/models/messages_state.dart b/lib/chat/models/messages_state.dart deleted file mode 100644 index 4a08376..0000000 --- a/lib/chat/models/messages_state.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter/foundation.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -import 'message_state.dart'; - -part 'messages_state.freezed.dart'; -part 'messages_state.g.dart'; - -@freezed -class MessagesState with _$MessagesState { - const factory MessagesState({ - // List of messages in the window - required IList windowMessages, - // Total number of messages - required int length, - // One past the end of the last element - required int windowTail, - // The total number of elements to try to keep in 'messages' - required int windowCount, - // If we should have the tail following the array - required bool follow, - }) = _MessagesState; - - factory MessagesState.fromJson(dynamic json) => - _$MessagesStateFromJson(json as Map); -} diff --git a/lib/chat/models/messages_state.g.dart b/lib/chat/models/messages_state.g.dart deleted file mode 100644 index cf44e5b..0000000 --- a/lib/chat/models/messages_state.g.dart +++ /dev/null @@ -1,28 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'messages_state.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -_$MessagesStateImpl _$$MessagesStateImplFromJson(Map json) => - _$MessagesStateImpl( - windowMessages: IList.fromJson( - json['window_messages'], (value) => MessageState.fromJson(value)), - length: (json['length'] as num).toInt(), - windowTail: (json['window_tail'] as num).toInt(), - windowCount: (json['window_count'] as num).toInt(), - follow: json['follow'] as bool, - ); - -Map _$$MessagesStateImplToJson(_$MessagesStateImpl instance) => - { - 'window_messages': instance.windowMessages.toJson( - (value) => value.toJson(), - ), - 'length': instance.length, - 'window_tail': instance.windowTail, - 'window_count': instance.windowCount, - 'follow': instance.follow, - }; diff --git a/lib/chat/models/models.dart b/lib/chat/models/models.dart index 3620563..30698cd 100644 --- a/lib/chat/models/models.dart +++ b/lib/chat/models/models.dart @@ -1,2 +1,3 @@ +export 'chat_component_state.dart'; export 'message_state.dart'; -export 'messages_state.dart'; +export 'window_state.dart'; diff --git a/lib/chat/models/window_state.dart b/lib/chat/models/window_state.dart new file mode 100644 index 0000000..91cde8a --- /dev/null +++ b/lib/chat/models/window_state.dart @@ -0,0 +1,27 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'window_state.freezed.dart'; + +@freezed +class WindowState with _$WindowState { + const factory WindowState({ + // List of objects in the window + required IList window, + // Total number of objects (windowTail max) + required int length, + // One past the end of the last element + required int windowTail, + // The total number of elements to try to keep in the window + required int windowCount, + // If we should have the tail following the array + required bool follow, + }) = _WindowState; +} + +extension WindowStateExt on WindowState { + int get windowEnd => (length == 0) ? -1 : (windowTail - 1) % length; + int get windowStart => + (length == 0) ? 0 : (windowTail - window.length) % length; +} diff --git a/lib/chat/models/messages_state.freezed.dart b/lib/chat/models/window_state.freezed.dart similarity index 59% rename from lib/chat/models/messages_state.freezed.dart rename to lib/chat/models/window_state.freezed.dart index 368ca94..604931d 100644 --- a/lib/chat/models/messages_state.freezed.dart +++ b/lib/chat/models/window_state.freezed.dart @@ -3,7 +3,7 @@ // 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 'messages_state.dart'; +part of 'window_state.dart'; // ************************************************************************** // FreezedGenerator @@ -14,37 +14,32 @@ 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'); -MessagesState _$MessagesStateFromJson(Map json) { - return _MessagesState.fromJson(json); -} - /// @nodoc -mixin _$MessagesState { -// List of messages in the window - IList get windowMessages => - throw _privateConstructorUsedError; // Total number of messages +mixin _$WindowState { +// List of objects in the window + IList get window => + throw _privateConstructorUsedError; // Total number of objects (windowTail max) int get length => throw _privateConstructorUsedError; // One past the end of the last element int get windowTail => - throw _privateConstructorUsedError; // The total number of elements to try to keep in 'messages' + throw _privateConstructorUsedError; // The total number of elements to try to keep in the window int get windowCount => throw _privateConstructorUsedError; // If we should have the tail following the array bool get follow => throw _privateConstructorUsedError; - Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) - $MessagesStateCopyWith get copyWith => + $WindowStateCopyWith> get copyWith => throw _privateConstructorUsedError; } /// @nodoc -abstract class $MessagesStateCopyWith<$Res> { - factory $MessagesStateCopyWith( - MessagesState value, $Res Function(MessagesState) then) = - _$MessagesStateCopyWithImpl<$Res, MessagesState>; +abstract class $WindowStateCopyWith { + factory $WindowStateCopyWith( + WindowState value, $Res Function(WindowState) then) = + _$WindowStateCopyWithImpl>; @useResult $Res call( - {IList windowMessages, + {IList window, int length, int windowTail, int windowCount, @@ -52,9 +47,9 @@ abstract class $MessagesStateCopyWith<$Res> { } /// @nodoc -class _$MessagesStateCopyWithImpl<$Res, $Val extends MessagesState> - implements $MessagesStateCopyWith<$Res> { - _$MessagesStateCopyWithImpl(this._value, this._then); +class _$WindowStateCopyWithImpl> + implements $WindowStateCopyWith { + _$WindowStateCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; @@ -64,17 +59,17 @@ class _$MessagesStateCopyWithImpl<$Res, $Val extends MessagesState> @pragma('vm:prefer-inline') @override $Res call({ - Object? windowMessages = null, + Object? window = null, Object? length = null, Object? windowTail = null, Object? windowCount = null, Object? follow = null, }) { return _then(_value.copyWith( - windowMessages: null == windowMessages - ? _value.windowMessages - : windowMessages // ignore: cast_nullable_to_non_nullable - as IList, + window: null == window + ? _value.window + : window // ignore: cast_nullable_to_non_nullable + as IList, length: null == length ? _value.length : length // ignore: cast_nullable_to_non_nullable @@ -96,15 +91,15 @@ class _$MessagesStateCopyWithImpl<$Res, $Val extends MessagesState> } /// @nodoc -abstract class _$$MessagesStateImplCopyWith<$Res> - implements $MessagesStateCopyWith<$Res> { - factory _$$MessagesStateImplCopyWith( - _$MessagesStateImpl value, $Res Function(_$MessagesStateImpl) then) = - __$$MessagesStateImplCopyWithImpl<$Res>; +abstract class _$$WindowStateImplCopyWith + implements $WindowStateCopyWith { + factory _$$WindowStateImplCopyWith(_$WindowStateImpl value, + $Res Function(_$WindowStateImpl) then) = + __$$WindowStateImplCopyWithImpl; @override @useResult $Res call( - {IList windowMessages, + {IList window, int length, int windowTail, int windowCount, @@ -112,27 +107,27 @@ abstract class _$$MessagesStateImplCopyWith<$Res> } /// @nodoc -class __$$MessagesStateImplCopyWithImpl<$Res> - extends _$MessagesStateCopyWithImpl<$Res, _$MessagesStateImpl> - implements _$$MessagesStateImplCopyWith<$Res> { - __$$MessagesStateImplCopyWithImpl( - _$MessagesStateImpl _value, $Res Function(_$MessagesStateImpl) _then) +class __$$WindowStateImplCopyWithImpl + extends _$WindowStateCopyWithImpl> + implements _$$WindowStateImplCopyWith { + __$$WindowStateImplCopyWithImpl( + _$WindowStateImpl _value, $Res Function(_$WindowStateImpl) _then) : super(_value, _then); @pragma('vm:prefer-inline') @override $Res call({ - Object? windowMessages = null, + Object? window = null, Object? length = null, Object? windowTail = null, Object? windowCount = null, Object? follow = null, }) { - return _then(_$MessagesStateImpl( - windowMessages: null == windowMessages - ? _value.windowMessages - : windowMessages // ignore: cast_nullable_to_non_nullable - as IList, + return _then(_$WindowStateImpl( + window: null == window + ? _value.window + : window // ignore: cast_nullable_to_non_nullable + as IList, length: null == length ? _value.length : length // ignore: cast_nullable_to_non_nullable @@ -154,30 +149,27 @@ class __$$MessagesStateImplCopyWithImpl<$Res> } /// @nodoc -@JsonSerializable() -class _$MessagesStateImpl + +class _$WindowStateImpl with DiagnosticableTreeMixin - implements _MessagesState { - const _$MessagesStateImpl( - {required this.windowMessages, + implements _WindowState { + const _$WindowStateImpl( + {required this.window, required this.length, required this.windowTail, required this.windowCount, required this.follow}); - factory _$MessagesStateImpl.fromJson(Map json) => - _$$MessagesStateImplFromJson(json); - -// List of messages in the window +// List of objects in the window @override - final IList windowMessages; -// Total number of messages + final IList window; +// Total number of objects (windowTail max) @override final int length; // One past the end of the last element @override final int windowTail; -// The total number of elements to try to keep in 'messages' +// The total number of elements to try to keep in the window @override final int windowCount; // If we should have the tail following the array @@ -186,15 +178,15 @@ class _$MessagesStateImpl @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'MessagesState(windowMessages: $windowMessages, length: $length, windowTail: $windowTail, windowCount: $windowCount, follow: $follow)'; + return 'WindowState<$T>(window: $window, length: $length, windowTail: $windowTail, windowCount: $windowCount, follow: $follow)'; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties - ..add(DiagnosticsProperty('type', 'MessagesState')) - ..add(DiagnosticsProperty('windowMessages', windowMessages)) + ..add(DiagnosticsProperty('type', 'WindowState<$T>')) + ..add(DiagnosticsProperty('window', window)) ..add(DiagnosticsProperty('length', length)) ..add(DiagnosticsProperty('windowTail', windowTail)) ..add(DiagnosticsProperty('windowCount', windowCount)) @@ -205,9 +197,8 @@ class _$MessagesStateImpl bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$MessagesStateImpl && - const DeepCollectionEquality() - .equals(other.windowMessages, windowMessages) && + other is _$WindowStateImpl && + const DeepCollectionEquality().equals(other.window, window) && (identical(other.length, length) || other.length == length) && (identical(other.windowTail, windowTail) || other.windowTail == windowTail) && @@ -216,11 +207,10 @@ class _$MessagesStateImpl (identical(other.follow, follow) || other.follow == follow)); } - @JsonKey(ignore: true) @override int get hashCode => Object.hash( runtimeType, - const DeepCollectionEquality().hash(windowMessages), + const DeepCollectionEquality().hash(window), length, windowTail, windowCount, @@ -229,40 +219,31 @@ class _$MessagesStateImpl @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') - _$$MessagesStateImplCopyWith<_$MessagesStateImpl> get copyWith => - __$$MessagesStateImplCopyWithImpl<_$MessagesStateImpl>(this, _$identity); - - @override - Map toJson() { - return _$$MessagesStateImplToJson( - this, - ); - } + _$$WindowStateImplCopyWith> get copyWith => + __$$WindowStateImplCopyWithImpl>( + this, _$identity); } -abstract class _MessagesState implements MessagesState { - const factory _MessagesState( - {required final IList windowMessages, +abstract class _WindowState implements WindowState { + const factory _WindowState( + {required final IList window, required final int length, required final int windowTail, required final int windowCount, - required final bool follow}) = _$MessagesStateImpl; + required final bool follow}) = _$WindowStateImpl; - factory _MessagesState.fromJson(Map json) = - _$MessagesStateImpl.fromJson; - - @override // List of messages in the window - IList get windowMessages; - @override // Total number of messages + @override // List of objects in the window + IList get window; + @override // Total number of objects (windowTail max) int get length; @override // One past the end of the last element int get windowTail; - @override // The total number of elements to try to keep in 'messages' + @override // The total number of elements to try to keep in the window int get windowCount; @override // If we should have the tail following the array bool get follow; @override @JsonKey(ignore: true) - _$$MessagesStateImplCopyWith<_$MessagesStateImpl> get copyWith => + _$$WindowStateImplCopyWith> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart deleted file mode 100644 index ee339d3..0000000 --- a/lib/chat/views/chat_component.dart +++ /dev/null @@ -1,320 +0,0 @@ -import 'dart:math'; - -import 'package:async_tools/async_tools.dart'; -import 'package:awesome_extensions/awesome_extensions.dart'; -import 'package:fixnum/fixnum.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'; -import '../chat.dart'; - -const String metadataKeyExpirationDuration = 'expiration'; -const String metadataKeyViewLimit = 'view_limit'; -const String metadataKeyAttachments = 'attachments'; - -class ChatComponent extends StatelessWidget { - const ChatComponent._( - {required TypedKey localUserIdentityKey, - required SingleContactMessagesCubit messagesCubit, - required SingleContactMessagesState messagesState, - required types.User localUser, - required types.User remoteUser, - super.key}) - : _localUserIdentityKey = localUserIdentityKey, - _messagesCubit = messagesCubit, - _messagesState = messagesState, - _localUser = localUser, - _remoteUser = remoteUser; - - final TypedKey _localUserIdentityKey; - final SingleContactMessagesCubit _messagesCubit; - final SingleContactMessagesState _messagesState; - final types.User _localUser; - final types.User _remoteUser; - - // Builder wrapper function that takes care of state management requirements - static Widget builder( - {required TypedKey localConversationRecordKey, Key? key}) => - Builder(builder: (context) { - // Get all watched dependendies - final activeAccountInfo = context.watch(); - final accountRecordInfo = - context.watch().state.asData?.value; - if (accountRecordInfo == null) { - return debugPage('should always have an account record here'); - } - final contactList = - context.watch().state.state.asData?.value; - if (contactList == null) { - return debugPage('should always have a contact list here'); - } - final avconversation = context.select?>( - (x) => x.state[localConversationRecordKey]); - if (avconversation == null) { - return waitingPage(); - } - final conversation = avconversation.asData?.value; - if (conversation == null) { - return avconversation.buildNotData(); - } - - // Make flutter_chat_ui 'User's - final localUserIdentityKey = activeAccountInfo - .localAccount.identityMaster - .identityPublicTypedKey(); - - final localUser = types.User( - id: localUserIdentityKey.toString(), - firstName: accountRecordInfo.profile.name, - ); - final editedName = conversation.contact.editedProfile.name; - final remoteUser = types.User( - id: conversation.contact.identityPublicKey.toVeilid().toString(), - firstName: editedName); - - // Get the messages cubit - final messages = context.select( - (x) => x.tryOperate(localConversationRecordKey, - closure: (cubit) => (cubit, cubit.state))); - - // Get the messages to display - // and ensure it is safe to operate() on the MessageCubit for this chat - if (messages == null) { - return waitingPage(); - } - - return ChatComponent._( - localUserIdentityKey: localUserIdentityKey, - messagesCubit: messages.$1, - messagesState: messages.$2, - localUser: localUser, - remoteUser: remoteUser, - key: key); - }); - - ///////////////////////////////////////////////////////////////////// - - types.Message? messageStateToChatMessage(MessageState message) { - final isLocal = message.content.author.toVeilid() == _localUserIdentityKey; - - types.Status? status; - if (message.sendState != null) { - assert(isLocal, 'send state should only be on sent messages'); - switch (message.sendState!) { - case MessageSendState.sending: - status = types.Status.sending; - case MessageSendState.sent: - status = types.Status.sent; - case MessageSendState.delivered: - status = types.Status.delivered; - } - } - - switch (message.content.whichKind()) { - case proto.Message_Kind.text: - final contextText = message.content.text; - final textMessage = types.TextMessage( - author: isLocal ? _localUser : _remoteUser, - createdAt: - (message.sentTimestamp.value ~/ BigInt.from(1000)).toInt(), - id: message.content.authorUniqueIdString, - text: contextText.text, - showStatus: status != null, - status: status); - return textMessage; - case proto.Message_Kind.secret: - case proto.Message_Kind.delete: - case proto.Message_Kind.erase: - case proto.Message_Kind.settings: - case proto.Message_Kind.permissions: - case proto.Message_Kind.membership: - case proto.Message_Kind.moderation: - case proto.Message_Kind.notSet: - return null; - } - } - - void _addTextMessage( - {required String text, - String? topic, - Uint8List? replyId, - Timestamp? expiration, - int? viewLimit, - List attachments = const []}) { - final protoMessageText = proto.Message_Text()..text = text; - if (topic != null) { - protoMessageText.topic = topic; - } - if (replyId != null) { - protoMessageText.replyId = replyId; - } - protoMessageText - ..expiration = expiration?.toInt64() ?? Int64.ZERO - ..viewLimit = viewLimit ?? 0; - protoMessageText.attachments.addAll(attachments); - - _messagesCubit.sendTextMessage(messageText: protoMessageText); - } - - void _sendMessage(types.PartialText message) { - final text = message.text; - - final replyId = (message.repliedMessage != null) - ? base64UrlNoPadDecode(message.repliedMessage!.id) - : null; - Timestamp? expiration; - int? viewLimit; - List? attachments; - final metadata = message.metadata; - if (metadata != null) { - final expirationValue = - metadata[metadataKeyExpirationDuration] as TimestampDuration?; - if (expirationValue != null) { - expiration = Veilid.instance.now().offset(expirationValue); - } - final viewLimitValue = metadata[metadataKeyViewLimit] as int?; - if (viewLimitValue != null) { - viewLimit = viewLimitValue; - } - final attachmentsValue = - metadata[metadataKeyAttachments] as List?; - if (attachmentsValue != null) { - attachments = attachmentsValue; - } - } - - _addTextMessage( - text: text, - replyId: replyId, - expiration: expiration, - viewLimit: viewLimit, - attachments: attachments ?? []); - } - - void _handleSendPressed(types.PartialText message) { - final text = message.text; - - if (text.startsWith('/')) { - _messagesCubit.runCommand(text); - return; - } - - _sendMessage(message); - } - - // void _handleAttachmentPressed() async { - // // - // } - - @override - 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 messagesState = _messagesState.asData?.value; - if (messagesState == null) { - return _messagesState.buildNotData(); - } - - // Convert protobuf messages to chat messages - final chatMessages = []; - final tsSet = {}; - for (final message in messagesState.windowMessages) { - final chatMessage = messageStateToChatMessage(message); - if (chatMessage == null) { - continue; - } - chatMessages.insert(0, chatMessage); - if (!tsSet.add(chatMessage.id)) { - // ignore: avoid_print - print('duplicate id found: ${chatMessage.id}:\n' - 'Messages:\n${messagesState.windowMessages}\n' - 'ChatMessages:\n$chatMessages'); - assert(false, 'should not have duplicate id'); - } - } - - final isLastPage = - (messagesState.windowTail - messagesState.windowMessages.length) <= 0; - final follow = messagesState.windowTail == 0 || - messagesState.windowTail == messagesState.length; xxx finish calculating pagination and get scroll position here somehow - - 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(_remoteUser.firstName!, - textAlign: TextAlign.start, - style: textTheme.titleMedium!.copyWith( - color: scale.primaryScale.borderText)), - )), - const Spacer(), - IconButton( - icon: Icon(Icons.close, - color: scale.primaryScale.borderText), - onPressed: () async { - context.read().setActiveChat(null); - }).paddingLTRB(16, 0, 16, 0) - ]), - ), - Expanded( - child: DecoratedBox( - decoration: const BoxDecoration(), - child: Chat( - theme: chatTheme, - messages: chatMessages, - onEndReached: () async { - final tail = await _messagesCubit.setWindow( - tail: max( - 0, - (messagesState.windowTail - - (messagesState.windowCount ~/ 2))), - count: messagesState.windowCount, - follow: follow); - }, - isLastPage: isLastPage, - //onAttachmentPressed: _handleAttachmentPressed, - //onMessageTap: _handleMessageTap, - //onPreviewDataFetched: _handlePreviewDataFetched, - onSendPressed: _handleSendPressed, - //showUserAvatars: false, - //showUserNames: true, - user: _localUser, - emptyState: const EmptyChatWidget()), - ), - ), - ], - ), - ], - ), - )); - } -} diff --git a/lib/chat/views/chat_component_widget.dart b/lib/chat/views/chat_component_widget.dart new file mode 100644 index 0000000..a3b2e33 --- /dev/null +++ b/lib/chat/views/chat_component_widget.dart @@ -0,0 +1,294 @@ +import 'dart:math'; + +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'; +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 '../../theme/theme.dart'; +import '../chat.dart'; + +const onEndReachedThreshold = 0.75; + +class ChatComponentWidget extends StatelessWidget { + const ChatComponentWidget._({required super.key}); + + // Builder wrapper function that takes care of state management requirements + static Widget builder( + {required TypedKey localConversationRecordKey, Key? key}) => + Builder(builder: (context) { + // Get all watched dependendies + final activeAccountInfo = context.watch(); + final accountRecordInfo = + context.watch().state.asData?.value; + if (accountRecordInfo == null) { + return debugPage('should always have an account record here'); + } + + final avconversation = context.select?>( + (x) => x.state[localConversationRecordKey]); + if (avconversation == null) { + return waitingPage(); + } + + final activeConversationState = avconversation.asData?.value; + if (activeConversationState == null) { + return avconversation.buildNotData(); + } + + // Get the messages cubit + final messagesCubit = context.select< + ActiveSingleContactChatBlocMapCubit, + SingleContactMessagesCubit?>( + (x) => x.tryOperate(localConversationRecordKey, + closure: (cubit) => cubit)); + if (messagesCubit == null) { + return waitingPage(); + } + + // Make chat component state + return BlocProvider( + create: (context) => ChatComponentCubit.singleContact( + activeAccountInfo: activeAccountInfo, + accountRecordInfo: accountRecordInfo, + activeConversationState: activeConversationState, + messagesCubit: messagesCubit, + ), + child: ChatComponentWidget._(key: key)); + }); + + ///////////////////////////////////////////////////////////////////// + + void _handleSendPressed( + ChatComponentCubit chatComponentCubit, types.PartialText message) { + final text = message.text; + + if (text.startsWith('/')) { + chatComponentCubit.runCommand(text); + return; + } + + chatComponentCubit.sendMessage(message); + } + + // void _handleAttachmentPressed() async { + // // + // } + + Future _handlePageForward( + ChatComponentCubit chatComponentCubit, + WindowState messageWindow, + ScrollNotification notification) async { + print( + '_handlePageForward: messagesState.length=${messageWindow.length} messagesState.windowTail=${messageWindow.windowTail} messagesState.windowCount=${messageWindow.windowCount} ScrollNotification=$notification'); + + // Go forward a page + final tail = min(messageWindow.length, + messageWindow.windowTail + (messageWindow.windowCount ~/ 4)) % + messageWindow.length; + + // Set follow + final follow = messageWindow.length == 0 || + tail == 0; // xxx incorporate scroll position + + // final scrollOffset = (notification.metrics.maxScrollExtent - + // notification.metrics.minScrollExtent) * + // (1.0 - onEndReachedThreshold); + + // chatComponentCubit.scrollOffset = scrollOffset; + + await chatComponentCubit.setWindow( + tail: tail, count: messageWindow.windowCount, follow: follow); + + // chatComponentCubit.state.scrollController.position.jumpTo( + // chatComponentCubit.state.scrollController.offset + scrollOffset); + + //chatComponentCubit.scrollOffset = 0; + } + + Future _handlePageBackward( + ChatComponentCubit chatComponentCubit, + WindowState messageWindow, + ScrollNotification notification, + ) async { + print( + '_handlePageBackward: messagesState.length=${messageWindow.length} messagesState.windowTail=${messageWindow.windowTail} messagesState.windowCount=${messageWindow.windowCount} ScrollNotification=$notification'); + + // Go back a page + final tail = max( + messageWindow.windowCount, + (messageWindow.windowTail - (messageWindow.windowCount ~/ 4)) % + messageWindow.length); + + // Set follow + final follow = messageWindow.length == 0 || + tail == 0; // xxx incorporate scroll position + + // final scrollOffset = -(notification.metrics.maxScrollExtent - + // notification.metrics.minScrollExtent) * + // (1.0 - onEndReachedThreshold); + + // chatComponentCubit.scrollOffset = scrollOffset; + + await chatComponentCubit.setWindow( + tail: tail, count: messageWindow.windowCount, follow: follow); + + // chatComponentCubit.scrollOffset = scrollOffset; + + // chatComponentCubit.state.scrollController.position.jumpTo( + // chatComponentCubit.state.scrollController.offset + scrollOffset); + + //chatComponentCubit.scrollOffset = 0; + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final textTheme = Theme.of(context).textTheme; + final chatTheme = makeChatTheme(scale, textTheme); + + // Get the enclosing chat component cubit that contains our state + // (created by ChatComponentWidget.builder()) + final chatComponentCubit = context.watch(); + final chatComponentState = chatComponentCubit.state; + + final messageWindow = chatComponentState.messageWindow.asData?.value; + if (messageWindow == null) { + return chatComponentState.messageWindow.buildNotData(); + } + final isLastPage = messageWindow.windowStart == 0; + final isFirstPage = messageWindow.windowEnd == messageWindow.length - 1; + final title = chatComponentState.title; + + if (chatComponentCubit.scrollOffset != 0) { + chatComponentState.scrollController.position.correctPixels( + chatComponentState.scrollController.position.pixels + + chatComponentCubit.scrollOffset); + + chatComponentCubit.scrollOffset = 0; + } + + 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(title, + textAlign: TextAlign.start, + style: textTheme.titleMedium!.copyWith( + color: scale.primaryScale.borderText)), + )), + const Spacer(), + IconButton( + icon: Icon(Icons.close, + color: scale.primaryScale.borderText), + onPressed: () async { + context.read().setActiveChat(null); + }).paddingLTRB(16, 0, 16, 0) + ]), + ), + Expanded( + child: DecoratedBox( + decoration: const BoxDecoration(), + child: NotificationListener( + onNotification: (notification) { + if (chatComponentCubit.scrollOffset != 0) { + return false; + } + + if (!isFirstPage && + notification.metrics.pixels <= + ((notification.metrics.maxScrollExtent - + notification + .metrics.minScrollExtent) * + (1.0 - onEndReachedThreshold) + + notification.metrics.minScrollExtent)) { + // + final scrollOffset = (notification + .metrics.maxScrollExtent - + notification.metrics.minScrollExtent) * + (1.0 - onEndReachedThreshold); + + chatComponentCubit.scrollOffset = scrollOffset; + + // + singleFuture(chatComponentState.chatKey, + () async { + await _handlePageForward(chatComponentCubit, + messageWindow, notification); + }); + } else if (!isLastPage && + notification.metrics.pixels >= + ((notification.metrics.maxScrollExtent - + notification + .metrics.minScrollExtent) * + onEndReachedThreshold + + notification.metrics.minScrollExtent)) { + // + final scrollOffset = -(notification + .metrics.maxScrollExtent - + notification.metrics.minScrollExtent) * + (1.0 - onEndReachedThreshold); + + chatComponentCubit.scrollOffset = scrollOffset; + // + singleFuture(chatComponentState.chatKey, + () async { + await _handlePageBackward(chatComponentCubit, + messageWindow, notification); + }); + } + return false; + }, + child: Chat( + key: chatComponentState.chatKey, + theme: chatTheme, + messages: messageWindow.window.toList(), + scrollToBottomOnSend: isFirstPage, + scrollController: + chatComponentState.scrollController, + // isLastPage: isLastPage, + // onEndReached: () async { + // await _handlePageBackward( + // chatComponentCubit, messageWindow); + // }, + //onEndReachedThreshold: onEndReachedThreshold, + //onAttachmentPressed: _handleAttachmentPressed, + //onMessageTap: _handleMessageTap, + //onPreviewDataFetched: _handlePreviewDataFetched, + onSendPressed: (pt) => + _handleSendPressed(chatComponentCubit, pt), + //showUserAvatars: false, + //showUserNames: true, + user: chatComponentState.localUser, + emptyState: const EmptyChatWidget())), + ), + ), + ], + ), + ], + ), + )); + } +} diff --git a/lib/chat/views/views.dart b/lib/chat/views/views.dart index 1999862..7e8adce 100644 --- a/lib/chat/views/views.dart +++ b/lib/chat/views/views.dart @@ -1,4 +1,4 @@ -export 'chat_component.dart'; +export 'chat_component_widget.dart'; export 'empty_chat_widget.dart'; export 'new_chat_bottom_sheet.dart'; export 'no_conversation_widget.dart'; 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 587828a..6e1868c 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 @@ -33,8 +33,9 @@ class HomeAccountReadyChatState extends State { if (activeChatLocalConversationKey == null) { return const NoConversationWidget(); } - return ChatComponent.builder( - localConversationRecordKey: activeChatLocalConversationKey); + return ChatComponentWidget.builder( + localConversationRecordKey: activeChatLocalConversationKey, + key: ValueKey(activeChatLocalConversationKey)); } @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 0a4b28e..9fec3ce 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 @@ -40,12 +40,10 @@ class _HomeAccountReadyMainState extends State { color: scale.secondaryScale.borderText, constraints: const BoxConstraints.expand(height: 64, width: 64), style: ButtonStyle( - backgroundColor: MaterialStateProperty.all( - scale.primaryScale.hoverBorder), - shape: MaterialStateProperty.all( - const RoundedRectangleBorder( - borderRadius: - BorderRadius.all(Radius.circular(16))))), + backgroundColor: + WidgetStateProperty.all(scale.primaryScale.hoverBorder), + shape: WidgetStateProperty.all(const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16))))), tooltip: translate('app_bar.settings_tooltip'), onPressed: () async { await GoRouterHelper(context).push('/settings'); @@ -71,8 +69,9 @@ class _HomeAccountReadyMainState extends State { if (activeChatLocalConversationKey == null) { return const NoConversationWidget(); } - return ChatComponent.builder( - localConversationRecordKey: activeChatLocalConversationKey); + return ChatComponentWidget.builder( + localConversationRecordKey: activeChatLocalConversationKey, + ); } // ignore: prefer_expression_function_bodies diff --git a/lib/tools/state_logger.dart b/lib/tools/state_logger.dart index db0ea2a..08e32b3 100644 --- a/lib/tools/state_logger.dart +++ b/lib/tools/state_logger.dart @@ -10,7 +10,9 @@ const Map _blocChangeLogLevels = { 'TableDBArrayProtobufCubit': LogLevel.off, 'DHTLogCubit': LogLevel.off, 'SingleContactMessagesCubit': LogLevel.off, + 'ChatComponentCubit': LogLevel.off, }; + const Map _blocCreateCloseLogLevels = {}; const Map _blocErrorLogLevels = {}; diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart index 49acce0..a688453 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart @@ -39,11 +39,13 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { } final bLookup = await _spine.lookupPosition(bPos); if (bLookup == null) { + await aLookup.close(); throw StateError("can't lookup position b in swap of dht log"); } // Swap items in the segments if (aLookup.shortArray == bLookup.shortArray) { + await bLookup.close(); await aLookup.scope((sa) => sa.operateWriteEventual((aWrite) async { await aWrite.swap(aLookup.pos, bLookup.pos); return true; @@ -76,7 +78,9 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { } // Write item to the segment - return lookup.scope((sa) => sa.operateWrite((write) async { + return lookup.scope((sa) async { + try { + return sa.operateWrite((write) async { // If this a new segment, then clear it in case we have wrapped around if (lookup.pos == 0) { await write.clear(); @@ -85,7 +89,11 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { throw StateError('appending should be at the end'); } return write.tryAdd(value); - })); + }); + } on DHTExceptionTryAgain { + return false; + } + }); } @override @@ -110,7 +118,9 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { final sublistValues = values.sublist(valueIdx, valueIdx + sacount); dws.add(() async { - final ok = await lookup.scope((sa) => sa.operateWrite((write) async { + final ok = await lookup.scope((sa) async { + try { + return sa.operateWrite((write) async { // If this a new segment, then clear it in // case we have wrapped around if (lookup.pos == 0) { @@ -120,7 +130,11 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { throw StateError('appending should be at the end'); } return write.tryAddAll(sublistValues); - })); + }); + } on DHTExceptionTryAgain { + return false; + } + }); if (!ok) { success = false; } diff --git a/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart b/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart index 702a2ad..606ded5 100644 --- a/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart +++ b/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart @@ -103,6 +103,7 @@ class TableDBArrayProtobufCubit final elements = avElements.asData!.value; emit(AsyncValue.data(TableDBArrayProtobufStateData( windowElements: elements, + length: _array.length, windowTail: _tail, windowCount: _count, follow: _follow))); diff --git a/pubspec.lock b/pubspec.lock index 8a70f22..8bf7560 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: "direct main" description: name: archive - sha256: ecf4273855368121b1caed0d10d4513c7241dfc813f7d3c8933b36622ae9b265 + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d url: "https://pub.dev" source: hosted - version: "3.5.1" + version: "3.6.1" args: dependency: transitive description: @@ -155,18 +155,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "1414d6d733a85d8ad2f1dfcb3ea7945759e35a123cb99ccfac75d0758f75edfa" + sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7" url: "https://pub.dev" source: hosted - version: "2.4.10" + version: "2.4.11" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" + sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.3.1" built_collection: dependency: transitive description: @@ -219,10 +219,10 @@ packages: dependency: transitive description: name: camera_android - sha256: b350ac087f111467e705b2b76cc1322f7f5bdc122aa83b4b243b0872f390d229 + sha256: "3af7f0b55f184d392d2eec238aaa30552ebeef2915e5e094f5488bf50d6d7ca2" url: "https://pub.dev" source: hosted - version: "0.10.9+2" + version: "0.10.9+3" camera_avfoundation: dependency: transitive description: @@ -251,10 +251,10 @@ packages: dependency: "direct main" description: name: change_case - sha256: "47c48c36f95f20c6d0ba03efabceff261d05026cca322cc2c4c01c343371b5bb" + sha256: "99cfdf2018c627c8a3af5a23ea4c414eb69c75c31322d23b9660ebc3cf30b514" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" characters: dependency: transitive description: @@ -403,10 +403,10 @@ packages: dependency: "direct main" description: name: fast_immutable_collections - sha256: "533806a7f0c624c2e479d05d3fdce4c87109a7cd0db39b8cc3830d3a2e8dedc7" + sha256: c3c73f4f989d3302066e4ec94e6ec73b5dc872592d02194f49f1352d64126b8c url: "https://pub.dev" source: hosted - version: "10.2.3" + version: "10.2.4" ffi: dependency: transitive description: @@ -471,19 +471,18 @@ packages: flutter_chat_ui: dependency: "direct main" description: - name: flutter_chat_ui - sha256: "40fb37acc328dd179eadc3d67bf8bd2d950dc0da34464aa8d48e8707e0234c09" - url: "https://pub.dev" - source: hosted + path: "../flutter_chat_ui" + relative: true + source: path version: "1.6.13" flutter_form_builder: dependency: "direct main" description: name: flutter_form_builder - sha256: "560eb5e367d81170c6ade1e7ae63ecc5167936ae2cdfaae8a345e91bce19d2f2" + sha256: "447f8808f68070f7df968e8063aada3c9d2e90e789b5b70f3b44e4b315212656" url: "https://pub.dev" source: hosted - version: "9.2.1" + version: "9.3.0" flutter_hooks: dependency: "direct main" description: @@ -533,10 +532,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f" + sha256: c6b0b4c05c458e1c01ad9bcc14041dd7b1f6783d487be4386f793f47a8a4d03e url: "https://pub.dev" source: hosted - version: "2.0.19" + version: "2.0.20" flutter_shaders: dependency: transitive description: @@ -573,10 +572,10 @@ packages: dependency: "direct main" description: name: flutter_translate - sha256: "8b1c449bf6d17753e6f188185f735ebc0a328d21d745878a43be66857de8ebb3" + sha256: bc09db690098879e3f90eb3aac3499e5282f32d5f9d8f1cc597d67bbc1e065ef url: "https://pub.dev" source: hosted - version: "4.0.4" + version: "4.1.0" flutter_web_plugins: dependency: transitive description: flutter @@ -586,10 +585,10 @@ packages: dependency: "direct main" description: name: form_builder_validators - sha256: "19aa5282b7cede82d0025ab031a98d0554b84aa2ba40f12013471a3b3e22bf02" + sha256: "475853a177bfc832ec12551f752fd0001278358a6d42d2364681ff15f48f67cf" url: "https://pub.dev" source: hosted - version: "9.1.0" + version: "10.0.1" freezed: dependency: "direct dev" description: @@ -634,10 +633,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: aa073287b8f43553678e6fa9e8bb9c83212ff76e09542129a8099bbc8db4df65 + sha256: abec47eb8c8c36ebf41d0a4c64dbbe7f956e39a012b3aafc530e951bdc12fe3f url: "https://pub.dev" source: hosted - version: "14.1.2" + version: "14.1.4" graphs: dependency: transitive description: @@ -706,10 +705,10 @@ packages: dependency: "direct main" description: name: image - sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" url: "https://pub.dev" source: hosted - version: "4.1.7" + version: "4.2.0" intl: dependency: "direct main" description: @@ -826,10 +825,10 @@ packages: dependency: "direct main" description: name: motion_toast - sha256: "4763b2aa3499d0bf00ffd9737479b73141d0397f532542893156efb4a5ac1994" + sha256: "8dc8af93c606d0a08f2592591164f4a761099c5470e589f25689de6c601f124e" url: "https://pub.dev" source: hosted - version: "2.9.1" + version: "2.10.0" nested: dependency: transitive description: @@ -890,10 +889,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.2.5" path_provider_foundation: dependency: transitive description: @@ -1018,10 +1017,10 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.3.0" qr: dependency: transitive description: @@ -1095,7 +1094,7 @@ packages: source: hosted version: "0.1.9" scroll_to_index: - dependency: transitive + dependency: "direct main" description: name: scroll_to_index sha256: b707546e7500d9f070d63e5acf74fd437ec7eeeb68d3412ef7b0afada0b4f176 @@ -1106,10 +1105,10 @@ packages: dependency: "direct main" description: name: searchable_listview - sha256: dfa6358f5e097f45b5b51a160cb6189e112e3abe0f728f4740349cd3b6575617 + sha256: "5e547f2a3f88f99a798cfe64882cfce51496d8f3177bea4829dd7bcf3fa8910d" url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.14.0" share_plus: dependency: "direct main" description: @@ -1138,10 +1137,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" + sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.3" shared_preferences_foundation: dependency: transitive description: @@ -1392,26 +1391,26 @@ packages: dependency: transitive description: name: universal_platform - sha256: d315be0f6641898b280ffa34e2ddb14f3d12b1a37882557869646e0cc363d0cc + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" url: "https://pub.dev" source: hosted - version: "1.0.0+1" + version: "1.1.0" url_launcher: dependency: transitive description: name: url_launcher - sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" + sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" url: "https://pub.dev" source: hosted - version: "6.2.6" + version: "6.3.0" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "17cd5e205ea615e2c6ea7a77323a11712dffa0720a8a90540db57a01347f9ad9" + sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.3.3" url_launcher_ios: dependency: transitive description: @@ -1542,10 +1541,10 @@ packages: dependency: transitive description: name: web_socket - sha256: "217f49b5213796cb508d6a942a5dc604ce1cb6a0a6b3d8cb3f0c314f0ecea712" + sha256: "24301d8c293ce6fe327ffe6f59d8fd8834735f0ec36e4fd383ec7ff8a64aa078" url: "https://pub.dev" source: hosted - version: "0.1.4" + version: "0.1.5" web_socket_channel: dependency: transitive description: @@ -1628,4 +1627,4 @@ packages: version: "1.1.2" sdks: dart: ">=3.4.0 <4.0.0" - flutter: ">=3.19.1" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index 133d482..32c8121 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,30 +10,33 @@ environment: dependencies: animated_theme_switcher: ^2.0.10 ansicolor: ^2.0.2 - archive: ^3.5.1 + archive: ^3.6.1 async_tools: ^0.1.1 - awesome_extensions: ^2.0.14 + awesome_extensions: ^2.0.16 badges: ^3.1.2 basic_utils: ^5.7.0 bloc: ^8.1.4 bloc_advanced_tools: ^0.1.1 blurry_modal_progress_hud: ^1.1.1 - change_case: ^2.0.1 + change_case: ^2.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.8 equatable: ^2.0.5 - fast_immutable_collections: ^10.2.2 + fast_immutable_collections: ^10.2.4 fixnum: ^1.1.0 flutter: sdk: flutter flutter_animate: ^4.5.0 flutter_bloc: ^8.1.5 flutter_chat_types: ^3.6.2 - flutter_chat_ui: ^1.6.12 - flutter_form_builder: ^9.2.1 + flutter_chat_ui: + git: + url: https://gitlab.com/veilid/flutter_chat_ui.git + ref: main + flutter_form_builder: ^9.3.0 flutter_hooks: ^0.20.5 flutter_localizations: sdk: flutter @@ -41,18 +44,18 @@ dependencies: 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 + flutter_translate: ^4.1.0 + form_builder_validators: ^10.0.1 freezed_annotation: ^2.4.1 - go_router: ^14.1.2 + go_router: ^14.1.4 hydrated_bloc: ^9.1.5 - image: ^4.1.7 - intl: ^0.18.1 + image: ^4.2.0 + intl: ^0.19.0 json_annotation: ^4.9.0 loggy: ^2.0.3 - meta: ^1.11.0 + meta: ^1.12.0 mobile_scanner: ^5.1.1 - motion_toast: ^2.9.1 + motion_toast: ^2.10.0 pasteboard: ^0.2.0 path: ^1.9.0 path_provider: ^2.1.3 @@ -65,7 +68,8 @@ dependencies: quickalert: ^1.1.0 radix_colors: ^1.0.4 reorderable_grid: ^1.0.10 - searchable_listview: ^2.12.0 + scroll_to_index: ^3.0.1 + searchable_listview: ^2.14.0 share_plus: ^9.0.0 shared_preferences: ^2.2.3 signal_strength_indicator: ^0.4.1 @@ -83,7 +87,7 @@ dependencies: path: ../veilid/veilid-flutter veilid_support: path: packages/veilid_support - window_manager: ^0.3.8 + window_manager: ^0.3.9 xterm: ^4.0.0 zxing2: ^0.2.3 @@ -92,12 +96,12 @@ dependency_overrides: path: ../dart_async_tools bloc_advanced_tools: path: ../bloc_advanced_tools - # REMOVE ONCE form_builder_validators HAS A FIX UPSTREAM - intl: 0.19.0 + flutter_chat_ui: + path: ../flutter_chat_ui dev_dependencies: - build_runner: ^2.4.9 + build_runner: ^2.4.11 freezed: ^2.5.2 icons_launcher: ^2.1.7 json_serializable: ^6.8.0 From f4119e077ae78fcc3014c418c202131ca8dabc78 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 6 Jun 2024 07:48:37 -0400 Subject: [PATCH 127/270] update pubs and versions --- pubspec.lock | 2 +- pubspec.yaml | 19 +++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 8bf7560..eae680c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -102,7 +102,7 @@ packages: path: "../bloc_advanced_tools" relative: true source: path - version: "0.1.1" + version: "0.1.2" blurry_modal_progress_hud: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 32c8121..082c556 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ version: 0.2.0+10 environment: sdk: '>=3.2.0 <4.0.0' - flutter: '>=3.19.1' + flutter: '>=3.22.1' dependencies: animated_theme_switcher: ^2.0.10 @@ -16,7 +16,7 @@ dependencies: badges: ^3.1.2 basic_utils: ^5.7.0 bloc: ^8.1.4 - bloc_advanced_tools: ^0.1.1 + bloc_advanced_tools: ^0.1.2 blurry_modal_progress_hud: ^1.1.1 change_case: ^2.1.0 charcode: ^1.3.1 @@ -91,14 +91,13 @@ dependencies: xterm: ^4.0.0 zxing2: ^0.2.3 -dependency_overrides: - async_tools: - path: ../dart_async_tools - bloc_advanced_tools: - path: ../bloc_advanced_tools - flutter_chat_ui: - path: ../flutter_chat_ui - +# dependency_overrides: +# async_tools: +# path: ../dart_async_tools +# bloc_advanced_tools: +# path: ../bloc_advanced_tools +# flutter_chat_ui: +# path: ../flutter_chat_ui dev_dependencies: build_runner: ^2.4.11 From 4d32d51dd7e712252943237cbca1ab80c2b72050 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 6 Jun 2024 07:51:30 -0400 Subject: [PATCH 128/270] fix readme --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 82631b7..dc02697 100644 --- a/README.md +++ b/README.md @@ -12,17 +12,17 @@ While this is still in development, you must have a clone of the Veilid source c ### For Linux Systems: ``` -./setup_linux.sh +./dev-setup/setup_linux.sh ``` ### For Mac Systems: ``` -./setup_macos.sh +./dev-setup/setup_macos.sh ``` ## Updating Code ### To update the WASM binary from `veilid-wasm`: -* Debug WASM: run `./wasm_update.sh` -* Release WASM: run `/wasm_update.sh release` +* Debug WASM: run `./dev-setup/wasm_update.sh` +* Release WASM: run `./dev-setup/wasm_update.sh release` From 7f4b0166fe9d94e63afcd082af821969a44ee98d Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 6 Jun 2024 07:55:49 -0400 Subject: [PATCH 129/270] fix pubspec --- pubspec.lock | 24 ++++++++++++++---------- pubspec.yaml | 2 +- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index eae680c..bfd5be1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -60,9 +60,10 @@ packages: async_tools: dependency: "direct main" description: - path: "../dart_async_tools" - relative: true - source: path + name: async_tools + sha256: e783ac6ed5645c86da34240389bb3a000fc5e3ae6589c6a482eb24ece7217681 + url: "https://pub.dev" + source: hosted version: "0.1.1" awesome_extensions: dependency: "direct main" @@ -99,9 +100,10 @@ packages: bloc_advanced_tools: dependency: "direct main" description: - path: "../bloc_advanced_tools" - relative: true - source: path + name: bloc_advanced_tools + sha256: "0cf9b3a73a67addfe22ec3f97a1ac240f6ad53870d6b21a980260f390d7901cd" + url: "https://pub.dev" + source: hosted version: "0.1.2" blurry_modal_progress_hud: dependency: "direct main" @@ -471,9 +473,11 @@ packages: flutter_chat_ui: dependency: "direct main" description: - path: "../flutter_chat_ui" - relative: true - source: path + path: "." + ref: main + resolved-ref: "6712d897e25041de38fb53ec06dc7a12cc6bff7d" + url: "https://gitlab.com/veilid/flutter-chat-ui.git" + source: git version: "1.6.13" flutter_form_builder: dependency: "direct main" @@ -1627,4 +1631,4 @@ packages: version: "1.1.2" sdks: dart: ">=3.4.0 <4.0.0" - flutter: ">=3.22.0" + flutter: ">=3.22.1" diff --git a/pubspec.yaml b/pubspec.yaml index 082c556..56c7685 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,7 +34,7 @@ dependencies: flutter_chat_types: ^3.6.2 flutter_chat_ui: git: - url: https://gitlab.com/veilid/flutter_chat_ui.git + url: https://gitlab.com/veilid/flutter-chat-ui.git ref: main flutter_form_builder: ^9.3.0 flutter_hooks: ^0.20.5 From ddc02f67713bb860caa76ef84761f9dfe658ca9e Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 7 Jun 2024 14:42:04 -0400 Subject: [PATCH 130/270] big identity refactor --- assets/i18n/en.json | 3 +- doc/invitations.md | 8 +- .../models/active_account_info.dart | 16 +- .../models/local_account/local_account.dart | 14 +- .../local_account/local_account.freezed.dart | 67 +- .../models/local_account/local_account.g.dart | 4 +- .../models/user_login/user_login.dart | 7 +- .../models/user_login/user_login.freezed.dart | 47 +- .../models/user_login/user_login.g.dart | 6 +- .../account_repository.dart | 106 ++-- .../new_account_page.dart | 134 ++-- lib/account_manager/views/views.dart | 2 +- lib/app.dart | 35 ++ lib/chat/cubits/chat_component_cubit.dart | 3 +- .../cubits/single_contact_messages_cubit.dart | 22 +- .../cubits/contact_invitation_list_cubit.dart | 44 +- .../cubits/waiting_invitation_cubit.dart | 17 +- .../models/accepted_contact.dart | 2 +- .../models/valid_contact_invitation.dart | 39 +- .../views/invitation_dialog.dart | 7 +- lib/contacts/cubits/contact_list_cubit.dart | 10 +- lib/contacts/cubits/conversation_cubit.dart | 10 +- .../home_account_ready_shell.dart | 2 +- lib/proto/veilidchat.pb.dart | 44 +- lib/proto/veilidchat.pbjson.dart | 42 +- lib/proto/veilidchat.proto | 16 +- .../models/processor_connection_state.dart | 2 + .../identity_support/account_record_info.dart | 19 + .../account_record_info.freezed.dart | 170 +++++ .../account_record_info.g.dart | 19 + .../lib/identity_support/exceptions.dart | 13 + .../lib/identity_support/identity.dart | 25 + .../identity_support/identity.freezed.dart | 156 +++++ .../lib/identity_support/identity.g.dart | 26 + .../identity_support/identity_instance.dart | 274 +++++++++ .../identity_instance.freezed.dart | 274 +++++++++ .../identity_support/identity_instance.g.dart | 29 + .../identity_support/identity_support.dart | 6 + .../lib/identity_support/super_identity.dart | 174 ++++++ .../super_identity.freezed.dart | 380 ++++++++++++ .../identity_support/super_identity.g.dart | 34 + .../writable_super_identity.dart | 158 +++++ packages/veilid_support/lib/src/identity.dart | 333 ---------- .../lib/src/identity.freezed.dart | 579 ------------------ .../veilid_support/lib/src/identity.g.dart | 63 -- .../veilid_support/lib/veilid_support.dart | 2 +- 46 files changed, 2143 insertions(+), 1300 deletions(-) rename lib/account_manager/views/{new_account_page => }/new_account_page.dart (52%) create mode 100644 packages/veilid_support/lib/identity_support/account_record_info.dart create mode 100644 packages/veilid_support/lib/identity_support/account_record_info.freezed.dart create mode 100644 packages/veilid_support/lib/identity_support/account_record_info.g.dart create mode 100644 packages/veilid_support/lib/identity_support/exceptions.dart create mode 100644 packages/veilid_support/lib/identity_support/identity.dart create mode 100644 packages/veilid_support/lib/identity_support/identity.freezed.dart create mode 100644 packages/veilid_support/lib/identity_support/identity.g.dart create mode 100644 packages/veilid_support/lib/identity_support/identity_instance.dart create mode 100644 packages/veilid_support/lib/identity_support/identity_instance.freezed.dart create mode 100644 packages/veilid_support/lib/identity_support/identity_instance.g.dart create mode 100644 packages/veilid_support/lib/identity_support/identity_support.dart create mode 100644 packages/veilid_support/lib/identity_support/super_identity.dart create mode 100644 packages/veilid_support/lib/identity_support/super_identity.freezed.dart create mode 100644 packages/veilid_support/lib/identity_support/super_identity.g.dart create mode 100644 packages/veilid_support/lib/identity_support/writable_super_identity.dart delete mode 100644 packages/veilid_support/lib/src/identity.dart delete mode 100644 packages/veilid_support/lib/src/identity.freezed.dart delete mode 100644 packages/veilid_support/lib/src/identity.g.dart diff --git a/assets/i18n/en.json b/assets/i18n/en.json index eeaa476..d2dcd8c 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -31,7 +31,8 @@ "cancel": "Cancel", "delete": "Delete", "accept": "Accept", - "reject": "Reject" + "reject": "Reject", + "waiting_for_network": "Waiting For Network" }, "toast": { "error": "Error", diff --git a/doc/invitations.md b/doc/invitations.md index a914dc3..f76dee8 100644 --- a/doc/invitations.md +++ b/doc/invitations.md @@ -16,27 +16,27 @@ 2. Get the ContactRequest record unicastinbox DHT record owner subkey from the network 3. Decrypt the writer secret with the password if necessary 4. Decrypt the ContactRequestPrivate chunk with the writer secret -5. Get the contact's AccountMaster record key +5. Get the contact's SuperIdentity record key 6. Verify identity signature on the SignedContactInvitation 7. Verify expiration 8. Display the profile and ask if the user wants to accept or reject the invitation ## Accepting an invitation 1. Create a Local Chat DHT record (no content yet, will be encrypted with DH of contact identity key) -2. Create ContactResponse with chat dht record and account master +2. Create ContactResponse with chat dht record and superidentity 3. Create SignedContactResponse with accept=true signed with identity 4. Set ContactRequest unicastinbox DHT record writer subkey with SignedContactResponse, encrypted with writer secret 5. Add a local contact with the remote chat dht record, updating from the remote profile in it ## Rejecting an invitation -1. Create ContactResponse with account master +1. Create ContactResponse with superidentity 2. Create SignedContactResponse with accept=false signed with identity 3. Set ContactRequest unicastinbox DHT record writer subkey with SignedContactResponse, encrypted with writer secret ## Receiving an accept/reject 1. Open and get SignedContactResponse from ContactRequest unicastinbox DHT record 2. Decrypt with writer secret -3. Get DHT record for contact's AccountMaster +3. Get DHT record for contact's SuperIdentity 4. Validate the SignedContactResponse signature If accept == false: diff --git a/lib/account_manager/models/active_account_info.dart b/lib/account_manager/models/active_account_info.dart index 0e1a0ef..5f69e32 100644 --- a/lib/account_manager/models/active_account_info.dart +++ b/lib/account_manager/models/active_account_info.dart @@ -14,14 +14,18 @@ class ActiveAccountInfo { }); // + TypedKey get superIdentityRecordKey => localAccount.superIdentity.recordKey; 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); - } + TypedKey get identityTypedPublicKey => + localAccount.superIdentity.currentInstance.typedPublicKey; + PublicKey get identityPublicKey => + localAccount.superIdentity.currentInstance.publicKey; + SecretKey get identitySecretKey => userLogin.identitySecret.value; + KeyPair get identityWriter => + KeyPair(key: identityPublicKey, secret: identitySecretKey); + Future get identityCryptoSystem => + localAccount.superIdentity.currentInstance.cryptoSystem; Future makeConversationCrypto( TypedKey remoteIdentityPublicKey) async { diff --git a/lib/account_manager/models/local_account/local_account.dart b/lib/account_manager/models/local_account/local_account.dart index 1998961..76070ae 100644 --- a/lib/account_manager/models/local_account/local_account.dart +++ b/lib/account_manager/models/local_account/local_account.dart @@ -11,25 +11,31 @@ 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 +// Stores a copy of the most recent SuperIdentity 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 super identity key record for the account, + // containing the publicKey in the currentIdentity + required SuperIdentity superIdentity, + + // The encrypted currentIdentity 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; 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 781d1de..92e376f 100644 --- a/lib/account_manager/models/local_account/local_account.freezed.dart +++ b/lib/account_manager/models/local_account/local_account.freezed.dart @@ -20,9 +20,10 @@ LocalAccount _$LocalAccountFromJson(Map json) { /// @nodoc mixin _$LocalAccount { -// The master key record for the account, containing the identityPublicKey - IdentityMaster get identityMaster => - throw _privateConstructorUsedError; // The encrypted identity secret that goes with +// The super identity key record for the account, +// containing the publicKey in the currentIdentity + SuperIdentity get superIdentity => + throw _privateConstructorUsedError; // The encrypted currentIdentity secret that goes with // the identityPublicKey with appended salt @Uint8ListJsonConverter() Uint8List get identitySecretBytes => @@ -49,14 +50,14 @@ abstract class $LocalAccountCopyWith<$Res> { _$LocalAccountCopyWithImpl<$Res, LocalAccount>; @useResult $Res call( - {IdentityMaster identityMaster, + {SuperIdentity superIdentity, @Uint8ListJsonConverter() Uint8List identitySecretBytes, EncryptionKeyType encryptionKeyType, bool biometricsEnabled, bool hiddenAccount, String name}); - $IdentityMasterCopyWith<$Res> get identityMaster; + $SuperIdentityCopyWith<$Res> get superIdentity; } /// @nodoc @@ -72,7 +73,7 @@ class _$LocalAccountCopyWithImpl<$Res, $Val extends LocalAccount> @pragma('vm:prefer-inline') @override $Res call({ - Object? identityMaster = null, + Object? superIdentity = null, Object? identitySecretBytes = null, Object? encryptionKeyType = null, Object? biometricsEnabled = null, @@ -80,10 +81,10 @@ class _$LocalAccountCopyWithImpl<$Res, $Val extends LocalAccount> Object? name = null, }) { return _then(_value.copyWith( - identityMaster: null == identityMaster - ? _value.identityMaster - : identityMaster // ignore: cast_nullable_to_non_nullable - as IdentityMaster, + superIdentity: null == superIdentity + ? _value.superIdentity + : superIdentity // ignore: cast_nullable_to_non_nullable + as SuperIdentity, identitySecretBytes: null == identitySecretBytes ? _value.identitySecretBytes : identitySecretBytes // ignore: cast_nullable_to_non_nullable @@ -109,9 +110,9 @@ class _$LocalAccountCopyWithImpl<$Res, $Val extends LocalAccount> @override @pragma('vm:prefer-inline') - $IdentityMasterCopyWith<$Res> get identityMaster { - return $IdentityMasterCopyWith<$Res>(_value.identityMaster, (value) { - return _then(_value.copyWith(identityMaster: value) as $Val); + $SuperIdentityCopyWith<$Res> get superIdentity { + return $SuperIdentityCopyWith<$Res>(_value.superIdentity, (value) { + return _then(_value.copyWith(superIdentity: value) as $Val); }); } } @@ -125,7 +126,7 @@ abstract class _$$LocalAccountImplCopyWith<$Res> @override @useResult $Res call( - {IdentityMaster identityMaster, + {SuperIdentity superIdentity, @Uint8ListJsonConverter() Uint8List identitySecretBytes, EncryptionKeyType encryptionKeyType, bool biometricsEnabled, @@ -133,7 +134,7 @@ abstract class _$$LocalAccountImplCopyWith<$Res> String name}); @override - $IdentityMasterCopyWith<$Res> get identityMaster; + $SuperIdentityCopyWith<$Res> get superIdentity; } /// @nodoc @@ -147,7 +148,7 @@ class __$$LocalAccountImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? identityMaster = null, + Object? superIdentity = null, Object? identitySecretBytes = null, Object? encryptionKeyType = null, Object? biometricsEnabled = null, @@ -155,10 +156,10 @@ class __$$LocalAccountImplCopyWithImpl<$Res> Object? name = null, }) { return _then(_$LocalAccountImpl( - identityMaster: null == identityMaster - ? _value.identityMaster - : identityMaster // ignore: cast_nullable_to_non_nullable - as IdentityMaster, + superIdentity: null == superIdentity + ? _value.superIdentity + : superIdentity // ignore: cast_nullable_to_non_nullable + as SuperIdentity, identitySecretBytes: null == identitySecretBytes ? _value.identitySecretBytes : identitySecretBytes // ignore: cast_nullable_to_non_nullable @@ -187,7 +188,7 @@ class __$$LocalAccountImplCopyWithImpl<$Res> @JsonSerializable() class _$LocalAccountImpl implements _LocalAccount { const _$LocalAccountImpl( - {required this.identityMaster, + {required this.superIdentity, @Uint8ListJsonConverter() required this.identitySecretBytes, required this.encryptionKeyType, required this.biometricsEnabled, @@ -197,10 +198,11 @@ class _$LocalAccountImpl implements _LocalAccount { factory _$LocalAccountImpl.fromJson(Map json) => _$$LocalAccountImplFromJson(json); -// The master key record for the account, containing the identityPublicKey +// The super identity key record for the account, +// containing the publicKey in the currentIdentity @override - final IdentityMaster identityMaster; -// The encrypted identity secret that goes with + final SuperIdentity superIdentity; +// The encrypted currentIdentity secret that goes with // the identityPublicKey with appended salt @override @Uint8ListJsonConverter() @@ -221,7 +223,7 @@ class _$LocalAccountImpl implements _LocalAccount { @override String toString() { - return 'LocalAccount(identityMaster: $identityMaster, identitySecretBytes: $identitySecretBytes, encryptionKeyType: $encryptionKeyType, biometricsEnabled: $biometricsEnabled, hiddenAccount: $hiddenAccount, name: $name)'; + return 'LocalAccount(superIdentity: $superIdentity, identitySecretBytes: $identitySecretBytes, encryptionKeyType: $encryptionKeyType, biometricsEnabled: $biometricsEnabled, hiddenAccount: $hiddenAccount, name: $name)'; } @override @@ -229,8 +231,8 @@ class _$LocalAccountImpl implements _LocalAccount { return identical(this, other) || (other.runtimeType == runtimeType && other is _$LocalAccountImpl && - (identical(other.identityMaster, identityMaster) || - other.identityMaster == identityMaster) && + (identical(other.superIdentity, superIdentity) || + other.superIdentity == superIdentity) && const DeepCollectionEquality() .equals(other.identitySecretBytes, identitySecretBytes) && (identical(other.encryptionKeyType, encryptionKeyType) || @@ -246,7 +248,7 @@ class _$LocalAccountImpl implements _LocalAccount { @override int get hashCode => Object.hash( runtimeType, - identityMaster, + superIdentity, const DeepCollectionEquality().hash(identitySecretBytes), encryptionKeyType, biometricsEnabled, @@ -269,7 +271,7 @@ class _$LocalAccountImpl implements _LocalAccount { abstract class _LocalAccount implements LocalAccount { const factory _LocalAccount( - {required final IdentityMaster identityMaster, + {required final SuperIdentity superIdentity, @Uint8ListJsonConverter() required final Uint8List identitySecretBytes, required final EncryptionKeyType encryptionKeyType, required final bool biometricsEnabled, @@ -279,9 +281,10 @@ abstract class _LocalAccount implements LocalAccount { factory _LocalAccount.fromJson(Map json) = _$LocalAccountImpl.fromJson; - @override // The master key record for the account, containing the identityPublicKey - IdentityMaster get identityMaster; - @override // The encrypted identity secret that goes with + @override // The super identity key record for the account, +// containing the publicKey in the currentIdentity + SuperIdentity get superIdentity; + @override // The encrypted currentIdentity secret that goes with // the identityPublicKey with appended salt @Uint8ListJsonConverter() Uint8List get identitySecretBytes; diff --git a/lib/account_manager/models/local_account/local_account.g.dart b/lib/account_manager/models/local_account/local_account.g.dart index 4e8a7b2..b60c226 100644 --- a/lib/account_manager/models/local_account/local_account.g.dart +++ b/lib/account_manager/models/local_account/local_account.g.dart @@ -8,7 +8,7 @@ part of 'local_account.dart'; _$LocalAccountImpl _$$LocalAccountImplFromJson(Map json) => _$LocalAccountImpl( - identityMaster: IdentityMaster.fromJson(json['identity_master']), + superIdentity: SuperIdentity.fromJson(json['super_identity']), identitySecretBytes: const Uint8ListJsonConverter() .fromJson(json['identity_secret_bytes']), encryptionKeyType: @@ -20,7 +20,7 @@ _$LocalAccountImpl _$$LocalAccountImplFromJson(Map json) => Map _$$LocalAccountImplToJson(_$LocalAccountImpl instance) => { - 'identity_master': instance.identityMaster.toJson(), + 'super_identity': instance.superIdentity.toJson(), 'identity_secret_bytes': const Uint8ListJsonConverter().toJson(instance.identitySecretBytes), 'encryption_key_type': instance.encryptionKeyType.toJson(), diff --git a/lib/account_manager/models/user_login/user_login.dart b/lib/account_manager/models/user_login/user_login.dart index 4e23184..7c024cf 100644 --- a/lib/account_manager/models/user_login/user_login.dart +++ b/lib/account_manager/models/user_login/user_login.dart @@ -7,12 +7,13 @@ part 'user_login.g.dart'; // Represents a currently logged in account // User logins are stored in the user_logins tablestore table -// indexed by the accountMasterKey +// indexed by the accountSuperIdentityRecordKey @freezed class UserLogin with _$UserLogin { const factory UserLogin({ - // Master record key for the user used to index the local accounts table - required TypedKey accountMasterRecordKey, + // SuperIdentity record key for the user + // used to index the local accounts table + required TypedKey superIdentityRecordKey, // The identity secret as unlocked from the local accounts table required TypedSecret identitySecret, // The account record key, owner key and secret pulled from the identity 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 a25b4ab..c93ee7b 100644 --- a/lib/account_manager/models/user_login/user_login.freezed.dart +++ b/lib/account_manager/models/user_login/user_login.freezed.dart @@ -20,8 +20,9 @@ UserLogin _$UserLoginFromJson(Map json) { /// @nodoc mixin _$UserLogin { -// Master record key for the user used to index the local accounts table - Typed get accountMasterRecordKey => +// SuperIdentity record key for the user +// used to index the local accounts table + Typed get superIdentityRecordKey => throw _privateConstructorUsedError; // The identity secret as unlocked from the local accounts table Typed get identitySecret => throw _privateConstructorUsedError; // The account record key, owner key and secret pulled from the identity @@ -41,7 +42,7 @@ abstract class $UserLoginCopyWith<$Res> { _$UserLoginCopyWithImpl<$Res, UserLogin>; @useResult $Res call( - {Typed accountMasterRecordKey, + {Typed superIdentityRecordKey, Typed identitySecret, AccountRecordInfo accountRecordInfo, Timestamp lastActive}); @@ -62,15 +63,15 @@ class _$UserLoginCopyWithImpl<$Res, $Val extends UserLogin> @pragma('vm:prefer-inline') @override $Res call({ - Object? accountMasterRecordKey = null, + Object? superIdentityRecordKey = null, Object? identitySecret = null, Object? accountRecordInfo = null, Object? lastActive = null, }) { return _then(_value.copyWith( - accountMasterRecordKey: null == accountMasterRecordKey - ? _value.accountMasterRecordKey - : accountMasterRecordKey // ignore: cast_nullable_to_non_nullable + superIdentityRecordKey: null == superIdentityRecordKey + ? _value.superIdentityRecordKey + : superIdentityRecordKey // ignore: cast_nullable_to_non_nullable as Typed, identitySecret: null == identitySecret ? _value.identitySecret @@ -105,7 +106,7 @@ abstract class _$$UserLoginImplCopyWith<$Res> @override @useResult $Res call( - {Typed accountMasterRecordKey, + {Typed superIdentityRecordKey, Typed identitySecret, AccountRecordInfo accountRecordInfo, Timestamp lastActive}); @@ -125,15 +126,15 @@ class __$$UserLoginImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? accountMasterRecordKey = null, + Object? superIdentityRecordKey = null, Object? identitySecret = null, Object? accountRecordInfo = null, Object? lastActive = null, }) { return _then(_$UserLoginImpl( - accountMasterRecordKey: null == accountMasterRecordKey - ? _value.accountMasterRecordKey - : accountMasterRecordKey // ignore: cast_nullable_to_non_nullable + superIdentityRecordKey: null == superIdentityRecordKey + ? _value.superIdentityRecordKey + : superIdentityRecordKey // ignore: cast_nullable_to_non_nullable as Typed, identitySecret: null == identitySecret ? _value.identitySecret @@ -155,7 +156,7 @@ class __$$UserLoginImplCopyWithImpl<$Res> @JsonSerializable() class _$UserLoginImpl implements _UserLogin { const _$UserLoginImpl( - {required this.accountMasterRecordKey, + {required this.superIdentityRecordKey, required this.identitySecret, required this.accountRecordInfo, required this.lastActive}); @@ -163,9 +164,10 @@ class _$UserLoginImpl implements _UserLogin { factory _$UserLoginImpl.fromJson(Map json) => _$$UserLoginImplFromJson(json); -// Master record key for the user used to index the local accounts table +// SuperIdentity record key for the user +// used to index the local accounts table @override - final Typed accountMasterRecordKey; + final Typed superIdentityRecordKey; // The identity secret as unlocked from the local accounts table @override final Typed identitySecret; @@ -178,7 +180,7 @@ class _$UserLoginImpl implements _UserLogin { @override String toString() { - return 'UserLogin(accountMasterRecordKey: $accountMasterRecordKey, identitySecret: $identitySecret, accountRecordInfo: $accountRecordInfo, lastActive: $lastActive)'; + return 'UserLogin(superIdentityRecordKey: $superIdentityRecordKey, identitySecret: $identitySecret, accountRecordInfo: $accountRecordInfo, lastActive: $lastActive)'; } @override @@ -186,8 +188,8 @@ class _$UserLoginImpl implements _UserLogin { return identical(this, other) || (other.runtimeType == runtimeType && other is _$UserLoginImpl && - (identical(other.accountMasterRecordKey, accountMasterRecordKey) || - other.accountMasterRecordKey == accountMasterRecordKey) && + (identical(other.superIdentityRecordKey, superIdentityRecordKey) || + other.superIdentityRecordKey == superIdentityRecordKey) && (identical(other.identitySecret, identitySecret) || other.identitySecret == identitySecret) && (identical(other.accountRecordInfo, accountRecordInfo) || @@ -198,7 +200,7 @@ class _$UserLoginImpl implements _UserLogin { @JsonKey(ignore: true) @override - int get hashCode => Object.hash(runtimeType, accountMasterRecordKey, + int get hashCode => Object.hash(runtimeType, superIdentityRecordKey, identitySecret, accountRecordInfo, lastActive); @JsonKey(ignore: true) @@ -217,7 +219,7 @@ class _$UserLoginImpl implements _UserLogin { abstract class _UserLogin implements UserLogin { const factory _UserLogin( - {required final Typed accountMasterRecordKey, + {required final Typed superIdentityRecordKey, required final Typed identitySecret, required final AccountRecordInfo accountRecordInfo, required final Timestamp lastActive}) = _$UserLoginImpl; @@ -225,8 +227,9 @@ abstract class _UserLogin implements UserLogin { factory _UserLogin.fromJson(Map json) = _$UserLoginImpl.fromJson; - @override // Master record key for the user used to index the local accounts table - Typed get accountMasterRecordKey; + @override // SuperIdentity record key for the user +// used to index the local accounts table + Typed get superIdentityRecordKey; @override // The identity secret as unlocked from the local accounts table Typed get identitySecret; @override // The account record key, owner key and secret pulled from the identity diff --git a/lib/account_manager/models/user_login/user_login.g.dart b/lib/account_manager/models/user_login/user_login.g.dart index 267fc55..173d853 100644 --- a/lib/account_manager/models/user_login/user_login.g.dart +++ b/lib/account_manager/models/user_login/user_login.g.dart @@ -8,8 +8,8 @@ part of 'user_login.dart'; _$UserLoginImpl _$$UserLoginImplFromJson(Map json) => _$UserLoginImpl( - accountMasterRecordKey: Typed.fromJson( - json['account_master_record_key']), + superIdentityRecordKey: Typed.fromJson( + json['super_identity_record_key']), identitySecret: Typed.fromJson(json['identity_secret']), accountRecordInfo: @@ -19,7 +19,7 @@ _$UserLoginImpl _$$UserLoginImplFromJson(Map json) => Map _$$UserLoginImplToJson(_$UserLoginImpl instance) => { - 'account_master_record_key': instance.accountMasterRecordKey.toJson(), + 'super_identity_record_key': instance.superIdentityRecordKey.toJson(), 'identity_secret': instance.identitySecret.toJson(), 'account_record_info': instance.accountRecordInfo.toJson(), 'last_active': instance.lastActive.toJson(), diff --git a/lib/account_manager/repository/account_repository/account_repository.dart b/lib/account_manager/repository/account_repository/account_repository.dart index 4691d52..ac29913 100644 --- a/lib/account_manager/repository/account_repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository/account_repository.dart @@ -87,30 +87,30 @@ class AccountRepository { : fetchUserLogin(activeLocalAccount); } - LocalAccount? fetchLocalAccount(TypedKey accountMasterRecordKey) { + LocalAccount? fetchLocalAccount(TypedKey accountSuperIdentityRecordKey) { final localAccounts = _localAccounts.value; final idx = localAccounts.indexWhere( - (e) => e.identityMaster.masterRecordKey == accountMasterRecordKey); + (e) => e.superIdentity.recordKey == accountSuperIdentityRecordKey); if (idx == -1) { return null; } return localAccounts[idx]; } - UserLogin? fetchUserLogin(TypedKey accountMasterRecordKey) { + UserLogin? fetchUserLogin(TypedKey superIdentityRecordKey) { final userLogins = _userLogins.value; final idx = userLogins - .indexWhere((e) => e.accountMasterRecordKey == accountMasterRecordKey); + .indexWhere((e) => e.superIdentityRecordKey == superIdentityRecordKey); if (idx == -1) { return null; } return userLogins[idx]; } - AccountInfo getAccountInfo(TypedKey? accountMasterRecordKey) { + AccountInfo getAccountInfo(TypedKey? superIdentityRecordKey) { // Get active account if we have one final activeLocalAccount = getActiveLocalAccount(); - if (accountMasterRecordKey == null) { + if (superIdentityRecordKey == null) { if (activeLocalAccount == null) { // No user logged in return const AccountInfo( @@ -118,12 +118,12 @@ class AccountRepository { active: false, activeAccountInfo: null); } - accountMasterRecordKey = activeLocalAccount; + superIdentityRecordKey = activeLocalAccount; } - final active = accountMasterRecordKey == activeLocalAccount; + final active = superIdentityRecordKey == activeLocalAccount; // Get which local account we want to fetch the profile for - final localAccount = fetchLocalAccount(accountMasterRecordKey); + final localAccount = fetchLocalAccount(superIdentityRecordKey); if (localAccount == null) { // account does not exist return AccountInfo( @@ -133,7 +133,7 @@ class AccountRepository { } // See if we've logged into this account or if it is locked - final userLogin = fetchUserLogin(accountMasterRecordKey); + final userLogin = fetchUserLogin(superIdentityRecordKey); if (userLogin == null) { // Account was locked return AccountInfo( @@ -165,33 +165,32 @@ class AccountRepository { _streamController.add(AccountRepositoryChange.localAccounts); } - /// 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 createWithNewMasterIdentity( - NewProfileSpec newProfileSpec) async { - log.debug('Creating master identity'); - final imws = await IdentityMasterWithSecrets.create(); + /// Creates a new super identity, an identity instance, an account associated + /// with the identity instance, stores the account in the identity key and + /// then logs into that account with no password set at this time + Future createWithNewSuperIdentity(NewProfileSpec newProfileSpec) async { + log.debug('Creating super identity'); + final wsi = await WritableSuperIdentity.create(); try { final localAccount = await _newLocalAccount( - identityMaster: imws.identityMaster, - identitySecret: imws.identitySecret, + superIdentity: wsi.superIdentity, + identitySecret: wsi.identitySecret, newProfileSpec: newProfileSpec); // Log in the new account by default with no pin - final ok = await login(localAccount.identityMaster.masterRecordKey, - EncryptionKeyType.none, ''); + final ok = await login( + localAccount.superIdentity.recordKey, EncryptionKeyType.none, ''); assert(ok, 'login with none should never fail'); } on Exception catch (_) { - await imws.delete(); + await wsi.delete(); rethrow; } } - /// Creates a new Account associated with master identity + /// Creates a new Account associated with the current instance of the identity /// Adds a logged-out LocalAccount to track its existence on this device Future _newLocalAccount( - {required IdentityMaster identityMaster, + {required SuperIdentity superIdentity, required SecretKey identitySecret, required NewProfileSpec newProfileSpec, EncryptionKeyType encryptionKeyType = EncryptionKeyType.none, @@ -201,8 +200,9 @@ class AccountRepository { final localAccounts = await _localAccounts.get(); // Add account with profile to DHT - await identityMaster.addAccountToIdentity( - identitySecret: identitySecret, + await superIdentity.currentInstance.addAccount( + superRecordKey: superIdentity.recordKey, + secretKey: identitySecret, accountKey: veilidChatAccountKey, createAccountCallback: (parent) async { // Make empty contact list @@ -235,13 +235,13 @@ class AccountRepository { ..contactList = contactList.toProto() ..contactInvitationRecords = contactInvitationRecords.toProto() ..chatList = chatRecords.toProto(); - return account; + return account.writeToBuffer(); }); // Encrypt identitySecret with key final identitySecretBytes = await encryptionKeyType.encryptSecretToBytes( secret: identitySecret, - cryptoKind: identityMaster.identityRecordKey.kind, + cryptoKind: superIdentity.currentInstance.recordKey.kind, encryptionKey: encryptionKey, ); @@ -250,7 +250,7 @@ class AccountRepository { // 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, + superIdentity: superIdentity, identitySecretBytes: identitySecretBytes, encryptionKeyType: encryptionKeyType, biometricsEnabled: false, @@ -269,12 +269,12 @@ class AccountRepository { } /// Remove an account and wipe the messages for this account from this device - Future deleteLocalAccount(TypedKey accountMasterRecordKey) async { - await logout(accountMasterRecordKey); + Future deleteLocalAccount(TypedKey superIdentityRecordKey) async { + await logout(superIdentityRecordKey); final localAccounts = await _localAccounts.get(); final newLocalAccounts = localAccounts.removeWhere( - (la) => la.identityMaster.masterRecordKey == accountMasterRecordKey); + (la) => la.superIdentity.recordKey == superIdentityRecordKey); await _localAccounts.set(newLocalAccounts); _streamController.add(AccountRepositoryChange.localAccounts); @@ -290,31 +290,35 @@ class AccountRepository { /// Delete an account from all devices - Future switchToAccount(TypedKey? accountMasterRecordKey) async { + Future switchToAccount(TypedKey? superIdentityRecordKey) async { final activeLocalAccount = await _activeLocalAccount.get(); - if (activeLocalAccount == accountMasterRecordKey) { + if (activeLocalAccount == superIdentityRecordKey) { // Nothing to do return; } - if (accountMasterRecordKey != null) { + if (superIdentityRecordKey != null) { // Assert the specified record key can be found, will throw if not final _ = _userLogins.value.firstWhere( - (ul) => ul.accountMasterRecordKey == accountMasterRecordKey); + (ul) => ul.superIdentityRecordKey == superIdentityRecordKey); } - await _activeLocalAccount.set(accountMasterRecordKey); + await _activeLocalAccount.set(superIdentityRecordKey); _streamController.add(AccountRepositoryChange.activeLocalAccount); } Future _decryptedLogin( - IdentityMaster identityMaster, SecretKey identitySecret) async { + SuperIdentity superIdentity, SecretKey identitySecret) async { // Verify identity secret works and return the valid cryptosystem - final cs = await identityMaster.validateIdentitySecret(identitySecret); + final cs = await superIdentity.currentInstance + .validateIdentitySecret(identitySecret); // Read the identity key to get the account keys - final accountRecordInfoList = await identityMaster.readAccountsFromIdentity( - identitySecret: identitySecret, accountKey: veilidChatAccountKey); + final accountRecordInfoList = await superIdentity.currentInstance + .readAccount( + superRecordKey: superIdentity.recordKey, + secretKey: identitySecret, + accountKey: veilidChatAccountKey); if (accountRecordInfoList.length > 1) { throw IdentityException.limitExceeded; } else if (accountRecordInfoList.isEmpty) { @@ -326,11 +330,11 @@ class AccountRepository { final userLogins = await _userLogins.get(); final now = Veilid.instance.now(); final newUserLogins = userLogins.replaceFirstWhere( - (ul) => ul.accountMasterRecordKey == identityMaster.masterRecordKey, + (ul) => ul.superIdentityRecordKey == superIdentity.recordKey, (ul) => ul != null ? ul.copyWith(lastActive: now) : UserLogin( - accountMasterRecordKey: identityMaster.masterRecordKey, + superIdentityRecordKey: superIdentity.recordKey, identitySecret: TypedSecret(kind: cs.kind(), value: identitySecret), accountRecordInfo: accountRecordInfo, @@ -338,7 +342,7 @@ class AccountRepository { addIfNotFound: true); await _userLogins.set(newUserLogins); - await _activeLocalAccount.set(identityMaster.masterRecordKey); + await _activeLocalAccount.set(superIdentity.recordKey); _streamController ..add(AccountRepositoryChange.userLogins) @@ -347,13 +351,13 @@ class AccountRepository { return true; } - Future login(TypedKey accountMasterRecordKey, + Future login(TypedKey accountSuperRecordKey, 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); + (la) => la.superIdentity.recordKey == accountSuperRecordKey); // Log in with this local account @@ -365,12 +369,12 @@ class AccountRepository { final identitySecret = await localAccount.encryptionKeyType.decryptSecretFromBytes( secretBytes: localAccount.identitySecretBytes, - cryptoKind: localAccount.identityMaster.identityRecordKey.kind, + cryptoKind: localAccount.superIdentity.currentInstance.recordKey.kind, encryptionKey: encryptionKey, ); // Validate this secret with the identity public key and log in - return _decryptedLogin(localAccount.identityMaster, identitySecret); + return _decryptedLogin(localAccount.superIdentity, identitySecret); } Future logout(TypedKey? accountMasterRecordKey) async { @@ -390,20 +394,20 @@ class AccountRepository { // Remove user from active logins list final newUserLogins = (await _userLogins.get()) - .removeWhere((ul) => ul.accountMasterRecordKey == logoutUser); + .removeWhere((ul) => ul.superIdentityRecordKey == logoutUser); await _userLogins.set(newUserLogins); _streamController.add(AccountRepositoryChange.userLogins); } Future openAccountRecord(UserLogin userLogin) async { - final localAccount = fetchLocalAccount(userLogin.accountMasterRecordKey)!; + final localAccount = fetchLocalAccount(userLogin.superIdentityRecordKey)!; // Record not yet open, do it final pool = DHTRecordPool.instance; final record = await pool.openRecordOwned( userLogin.accountRecordInfo.accountRecord, debugName: 'AccountRepository::openAccountRecord::AccountRecord', - parent: localAccount.identityMaster.identityRecordKey); + parent: localAccount.superIdentity.currentInstance.recordKey); return record; } diff --git a/lib/account_manager/views/new_account_page/new_account_page.dart b/lib/account_manager/views/new_account_page.dart similarity index 52% rename from lib/account_manager/views/new_account_page/new_account_page.dart rename to lib/account_manager/views/new_account_page.dart index 38664ba..7e15a32 100644 --- a/lib/account_manager/views/new_account_page/new_account_page.dart +++ b/lib/account_manager/views/new_account_page.dart @@ -1,16 +1,17 @@ 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:form_builder_validators/form_builder_validators.dart'; import 'package:go_router/go_router.dart'; -import '../../../layout/default_app_bar.dart'; -import '../../../theme/theme.dart'; -import '../../../tools/tools.dart'; -import '../../../veilid_processor/veilid_processor.dart'; -import '../../account_manager.dart'; +import '../../layout/default_app_bar.dart'; +import '../../theme/theme.dart'; +import '../../tools/tools.dart'; +import '../../veilid_processor/veilid_processor.dart'; +import '../account_manager.dart'; class NewAccountPage extends StatefulWidget { const NewAccountPage({super.key}); @@ -36,63 +37,76 @@ class NewAccountPageState extends State { } Widget _newAccountForm(BuildContext context, - {required Future Function(GlobalKey) - onSubmit}) => - FormBuilder( - key: _formKey, - child: ListView( - children: [ - Text(translate('new_account_page.header')) - .textStyle(context.headlineSmall) - .paddingSymmetric(vertical: 16), - FormBuilderTextField( - autofocus: true, - name: formFieldName, - decoration: - InputDecoration(labelText: translate('account.form_name')), - maxLength: 64, - // The validator receives the text that the user has entered. - 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(), - Text(translate('new_account_page.instructions')) - .toCenter() - .flexible(flex: 6), - const Spacer(), - ]).paddingSymmetric(vertical: 4), - ElevatedButton( - onPressed: () async { - if (_formKey.currentState?.saveAndValidate() ?? false) { - setState(() { - isInAsyncCall = true; - }); - try { - await onSubmit(_formKey); - } finally { - if (mounted) { + {required Future Function(GlobalKey) onSubmit}) { + final networkReady = context + .watch() + .state + .asData + ?.value + .isPublicInternetReady ?? + false; + final canSubmit = networkReady; + + return FormBuilder( + key: _formKey, + child: ListView( + children: [ + Text(translate('new_account_page.header')) + .textStyle(context.headlineSmall) + .paddingSymmetric(vertical: 16), + FormBuilderTextField( + autofocus: true, + name: formFieldName, + decoration: + InputDecoration(labelText: translate('account.form_name')), + maxLength: 64, + // The validator receives the text that the user has entered. + 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(), + Text(translate('new_account_page.instructions')) + .toCenter() + .flexible(flex: 6), + const Spacer(), + ]).paddingSymmetric(vertical: 4), + ElevatedButton( + onPressed: !canSubmit + ? null + : () async { + if (_formKey.currentState?.saveAndValidate() ?? false) { setState(() { - isInAsyncCall = false; + isInAsyncCall = true; }); + try { + await onSubmit(_formKey); + } finally { + if (mounted) { + setState(() { + isInAsyncCall = false; + }); + } + } } - } - } - }, - child: Text(translate('new_account_page.create')), - ).paddingSymmetric(vertical: 4).alignAtCenterRight(), - ], - ), - ); + }, + child: Text(translate(!networkReady + ? 'button.waiting_for_network' + : 'new_account_page.create')), + ).paddingSymmetric(vertical: 4).alignAtCenterRight(), + ], + ), + ); + } @override Widget build(BuildContext context) { @@ -127,7 +141,7 @@ class NewAccountPageState extends State { NewProfileSpec(name: name, pronouns: pronouns); await AccountRepository.instance - .createWithNewMasterIdentity(newProfileSpec); + .createWithNewSuperIdentity(newProfileSpec); } on Exception catch (e) { if (context.mounted) { await showErrorModal(context, translate('new_account_page.error'), diff --git a/lib/account_manager/views/views.dart b/lib/account_manager/views/views.dart index a10db1b..2acc537 100644 --- a/lib/account_manager/views/views.dart +++ b/lib/account_manager/views/views.dart @@ -1,2 +1,2 @@ -export 'new_account_page/new_account_page.dart'; +export 'new_account_page.dart'; export 'profile_widget.dart'; diff --git a/lib/app.dart b/lib/app.dart index e0c08c5..aaa0190 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,4 +1,5 @@ import 'package:animated_theme_switcher/animated_theme_switcher.dart'; +import 'package:async_tools/async_tools.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -7,6 +8,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 'package:provider/provider.dart'; +import 'package:veilid_support/veilid_support.dart'; import 'account_manager/account_manager.dart'; import 'init.dart'; @@ -22,6 +24,10 @@ class ReloadThemeIntent extends Intent { const ReloadThemeIntent(); } +class AttachDetachThemeIntent extends Intent { + const AttachDetachThemeIntent(); +} + class VeilidChatApp extends StatelessWidget { const VeilidChatApp({ required this.initialThemeData, @@ -37,6 +43,29 @@ class VeilidChatApp extends StatelessWidget { final theme = PreferencesRepository.instance.value.themePreferences.themeData(); ThemeSwitcher.of(context).changeTheme(theme: theme); + + // Hack to reload translations + final localizationDelegate = LocalizedApp.of(context).delegate; + singleFuture(this, () async { + await LocalizationDelegate.create( + fallbackLocale: localizationDelegate.fallbackLocale.toString(), + supportedLocales: localizationDelegate.supportedLocales + .map((x) => x.toString()) + .toList()); + }); + } + + void _attachDetachTheme(BuildContext context) { + singleFuture(this, () async { + if (ProcessorRepository.instance.processorConnectionState.isAttached) { + log.info('Detaching'); + await Veilid.instance.detach(); + } else if (ProcessorRepository + .instance.processorConnectionState.isDetached) { + log.info('Attaching'); + await Veilid.instance.attach(); + } + }); } Widget _buildShortcuts( @@ -48,10 +77,16 @@ class VeilidChatApp extends StatelessWidget { LogicalKeySet( LogicalKeyboardKey.alt, LogicalKeyboardKey.keyR): const ReloadThemeIntent(), + LogicalKeySet( + LogicalKeyboardKey.alt, LogicalKeyboardKey.keyD): + const AttachDetachThemeIntent(), }, child: Actions(actions: >{ ReloadThemeIntent: CallbackAction( onInvoke: (intent) => _reloadTheme(context)), + AttachDetachThemeIntent: + CallbackAction( + onInvoke: (intent) => _attachDetachTheme(context)), }, child: Focus(autofocus: true, child: builder(context))))); @override diff --git a/lib/chat/cubits/chat_component_cubit.dart b/lib/chat/cubits/chat_component_cubit.dart index 6ac2726..bc7431c 100644 --- a/lib/chat/cubits/chat_component_cubit.dart +++ b/lib/chat/cubits/chat_component_cubit.dart @@ -49,8 +49,7 @@ class ChatComponentCubit extends Cubit { required ActiveConversationState activeConversationState, required SingleContactMessagesCubit messagesCubit}) { // Make local 'User' - final localUserIdentityKey = - activeAccountInfo.localAccount.identityMaster.identityPublicTypedKey(); + final localUserIdentityKey = activeAccountInfo.identityTypedPublicKey; final localUser = types.User( id: localUserIdentityKey.toString(), firstName: accountRecordInfo.profile.name, diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 3fc8e91..b3c6325 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -114,13 +114,12 @@ class SingleContactMessagesCubit extends Cubit { _conversationCrypto = await _activeAccountInfo .makeConversationCrypto(_remoteIdentityPublicKey); _senderMessageIntegrity = await MessageIntegrity.create( - author: _activeAccountInfo.localAccount.identityMaster - .identityPublicTypedKey()); + author: _activeAccountInfo.identityTypedPublicKey); } // Open local messages key Future _initSentMessagesCubit() async { - final writer = _activeAccountInfo.conversationWriter; + final writer = _activeAccountInfo.identityWriter; _sentMessagesCubit = DHTLogCubit( open: () async => DHTLog.openWrite(_localMessagesRecordKey, writer, @@ -190,7 +189,7 @@ class SingleContactMessagesCubit extends Cubit { {int? tail, int? count, bool? follow, bool forceRefresh = false}) async { await _initWait(); - print('setWindow: tail=$tail count=$count, follow=$follow'); + // print('setWindow: tail=$tail count=$count, follow=$follow'); await _reconciledMessagesCubit!.setWindow( tail: tail, count: count, follow: follow, forceRefresh: forceRefresh); @@ -241,10 +240,8 @@ class SingleContactMessagesCubit extends Cubit { return; } - _reconciliation.reconcileMessages( - _activeAccountInfo.localAccount.identityMaster.identityPublicTypedKey(), - sentMessages, - _sentMessagesCubit!); + _reconciliation.reconcileMessages(_activeAccountInfo.identityTypedPublicKey, + sentMessages, _sentMessagesCubit!); // Update the view _renderState(); @@ -281,7 +278,7 @@ class SingleContactMessagesCubit extends Cubit { // Now sign it await _senderMessageIntegrity.signMessage( - message, _activeAccountInfo.userLogin.identitySecret.value); + message, _activeAccountInfo.identitySecretKey); } // Async process to send messages in the background @@ -334,8 +331,7 @@ class SingleContactMessagesCubit extends Cubit { for (final m in reconciledMessages.windowElements) { final isLocal = m.content.author.toVeilid() == - _activeAccountInfo.localAccount.identityMaster - .identityPublicTypedKey(); + _activeAccountInfo.identityTypedPublicKey; final reconciledTimestamp = Timestamp.fromInt64(m.reconciledTime); final sm = isLocal ? sentMessagesMap[m.content.authorUniqueIdString] : null; @@ -373,9 +369,7 @@ class SingleContactMessagesCubit extends Cubit { // Add common fields // id and signature will get set by _processMessageToSend message - ..author = _activeAccountInfo.localAccount.identityMaster - .identityPublicTypedKey() - .toProto() + ..author = _activeAccountInfo.identityTypedPublicKey.toProto() ..timestamp = Veilid.instance.now().toInt64(); // Put in the queue diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index 640d416..e278c6d 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -68,14 +68,16 @@ class ContactInvitationListCubit 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.conversationWriter; + final crcs = await pool.veilid.bestCryptoSystem(); + final contactRequestWriter = await crcs.generateKeyPair(); + + final idcs = await _activeAccountInfo.identityCryptoSystem; + final identityWriter = _activeAccountInfo.identityWriter; // Encrypt the writer secret with the encryption key final encryptedSecret = await encryptionKeyType.encryptSecretToBytes( secret: contactRequestWriter.secret, - cryptoKind: cs.kind(), + cryptoKind: crcs.kind(), encryptionKey: encryptionKey, ); @@ -89,21 +91,21 @@ class ContactInvitationListCubit debugName: 'ContactInvitationListCubit::createInvitation::' 'LocalConversation', parent: _activeAccountInfo.accountRecordKey, - schema: DHTSchema.smpl(oCnt: 0, members: [ - DHTSchemaMember(mKey: conversationWriter.key, mCnt: 1) - ]))) + schema: DHTSchema.smpl( + oCnt: 0, + members: [DHTSchemaMember(mKey: identityWriter.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 = _account.profile - ..identityMasterRecordKey = - _activeAccountInfo.userLogin.accountMasterRecordKey.toProto() + ..superIdentityRecordKey = + _activeAccountInfo.userLogin.superIdentityRecordKey.toProto() ..chatRecordKey = localConversation.key.toProto() ..expiration = expiration?.toInt64() ?? Int64.ZERO; final crprivbytes = crpriv.writeToBuffer(); - final encryptedContactRequestPrivate = await cs.encryptAeadWithNonce( + final encryptedContactRequestPrivate = await crcs.encryptAeadWithNonce( crprivbytes, contactRequestWriter.secret); // Create ContactRequest and embed contactrequestprivate @@ -140,9 +142,8 @@ class ContactInvitationListCubit final cinvbytes = cinv.writeToBuffer(); final scinv = proto.SignedContactInvitation() ..contactInvitation = cinvbytes - ..identitySignature = (await cs.sign( - conversationWriter.key, conversationWriter.secret, cinvbytes)) - .toProto(); + ..identitySignature = + (await idcs.signWithKeyPair(identityWriter, cinvbytes)).toProto(); signedContactInvitationBytes = scinv.writeToBuffer(); // Create ContactInvitationRecord @@ -237,8 +238,6 @@ class ContactInvitationListCubit ValidContactInvitation? out; - final cs = await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind); - // Compare the invitation's contact request // inbox with our list of extant invitations // If we're chatting to ourselves, @@ -257,6 +256,8 @@ class ContactInvitationListCubit final contactRequest = await contactRequestInbox .getProtobuf(proto.ContactRequest.fromBuffer); + final cs = await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind); + // Decrypt contact request private final encryptionKeyType = EncryptionKeyType.fromProto(contactRequest!.encryptionKeyType); @@ -276,16 +277,17 @@ class ContactInvitationListCubit final contactRequestPrivate = proto.ContactRequestPrivate.fromBuffer(contactRequestPrivateBytes); - final contactIdentityMasterRecordKey = - contactRequestPrivate.identityMasterRecordKey.toVeilid(); + final contactSuperIdentityRecordKey = + contactRequestPrivate.superIdentityRecordKey.toVeilid(); // Fetch the account master - final contactIdentityMaster = await openIdentityMaster( - identityMasterRecordKey: contactIdentityMasterRecordKey); + final contactSuperIdentity = await SuperIdentity.open( + superRecordKey: contactSuperIdentityRecordKey); // Verify + final idcs = await contactSuperIdentity.currentInstance.cryptoSystem; final signature = signedContactInvitation.identitySignature.toVeilid(); - await cs.verify(contactIdentityMaster.identityPublicKey, + await idcs.verify(contactSuperIdentity.currentInstance.publicKey, contactInvitationBytes, signature); final writer = KeyPair( @@ -297,7 +299,7 @@ class ContactInvitationListCubit account: _account, contactRequestInboxKey: contactRequestInboxKey, contactRequestPrivate: contactRequestPrivate, - contactIdentityMaster: contactIdentityMaster, + contactSuperIdentity: contactSuperIdentity, writer: writer); }); diff --git a/lib/contact_invitation/cubits/waiting_invitation_cubit.dart b/lib/contact_invitation/cubits/waiting_invitation_cubit.dart index f8c639f..120a2d7 100644 --- a/lib/contact_invitation/cubits/waiting_invitation_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitation_cubit.dart @@ -43,23 +43,21 @@ class WaitingInvitationCubit extends AsyncTransformerCubit _contactRequestPrivate.profile; @@ -33,8 +33,8 @@ class ValidContactInvitation { 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 isSelf = _contactSuperIdentity.currentInstance.publicKey == + _activeAccountInfo.identityPublicKey; final accountRecordKey = _activeAccountInfo.accountRecordKey; return (await pool.openRecordWrite(_contactRequestInboxKey, _writer, @@ -48,24 +48,23 @@ class ValidContactInvitation { final conversation = ConversationCubit( activeAccountInfo: _activeAccountInfo, remoteIdentityPublicKey: - _contactIdentityMaster.identityPublicTypedKey()); + _contactSuperIdentity.currentInstance.typedPublicKey); return conversation.initLocalConversation( profile: _account.profile, callback: (localConversation) async { final contactResponse = proto.ContactResponse() ..accept = true ..remoteConversationRecordKey = localConversation.key.toProto() - ..identityMasterRecordKey = _activeAccountInfo - .localAccount.identityMaster.masterRecordKey - .toProto(); + ..superIdentityRecordKey = + _activeAccountInfo.superIdentityRecordKey.toProto(); final contactResponseBytes = contactResponse.writeToBuffer(); final cs = await pool.veilid .getCryptoSystem(_contactRequestInboxKey.kind); final identitySignature = await cs.sign( - _activeAccountInfo.conversationWriter.key, - _activeAccountInfo.conversationWriter.secret, + _activeAccountInfo.identityWriter.key, + _activeAccountInfo.identityWriter.secret, contactResponseBytes); final signedContactResponse = proto.SignedContactResponse() @@ -78,7 +77,7 @@ class ValidContactInvitation { return AcceptedContact( remoteProfile: _contactRequestPrivate.profile, - remoteIdentity: _contactIdentityMaster, + remoteIdentity: _contactSuperIdentity, remoteConversationRecordKey: _contactRequestPrivate.chatRecordKey.toVeilid(), localConversationRecordKey: localConversation.key, @@ -95,10 +94,9 @@ class ValidContactInvitation { 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; - final accountRecordKey = - _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + final isSelf = _contactSuperIdentity.currentInstance.publicKey == + _activeAccountInfo.identityPublicKey; + final accountRecordKey = _activeAccountInfo.accountRecordKey; return (await pool.openRecordWrite(_contactRequestInboxKey, _writer, debugName: 'ValidContactInvitation::reject::' @@ -110,14 +108,13 @@ class ValidContactInvitation { final contactResponse = proto.ContactResponse() ..accept = false - ..identityMasterRecordKey = _activeAccountInfo - .localAccount.identityMaster.masterRecordKey - .toProto(); + ..superIdentityRecordKey = + _activeAccountInfo.superIdentityRecordKey.toProto(); final contactResponseBytes = contactResponse.writeToBuffer(); final identitySignature = await cs.sign( - _activeAccountInfo.conversationWriter.key, - _activeAccountInfo.conversationWriter.secret, + _activeAccountInfo.identityWriter.key, + _activeAccountInfo.identityWriter.secret, contactResponseBytes); final signedContactResponse = proto.SignedContactResponse() @@ -135,7 +132,7 @@ class ValidContactInvitation { final ActiveAccountInfo _activeAccountInfo; final proto.Account _account; final TypedKey _contactRequestInboxKey; - final IdentityMaster _contactIdentityMaster; + final SuperIdentity _contactSuperIdentity; final KeyPair _writer; final proto.ContactRequestPrivate _contactRequestPrivate; } diff --git a/lib/contact_invitation/views/invitation_dialog.dart b/lib/contact_invitation/views/invitation_dialog.dart index 2f1bd1c..f97869b 100644 --- a/lib/contact_invitation/views/invitation_dialog.dart +++ b/lib/contact_invitation/views/invitation_dialog.dart @@ -86,13 +86,12 @@ class InvitationDialogState extends State { if (acceptedContact != null) { // initiator when accept is received will create // contact in the case of a 'note to self' - final isSelf = - activeAccountInfo.localAccount.identityMaster.identityPublicKey == - acceptedContact.remoteIdentity.identityPublicKey; + final isSelf = activeAccountInfo.identityPublicKey == + acceptedContact.remoteIdentity.currentInstance.publicKey; if (!isSelf) { await contactList.createContact( remoteProfile: acceptedContact.remoteProfile, - remoteIdentity: acceptedContact.remoteIdentity, + remoteSuperIdentity: acceptedContact.remoteIdentity, remoteConversationRecordKey: acceptedContact.remoteConversationRecordKey, localConversationRecordKey: diff --git a/lib/contacts/cubits/contact_list_cubit.dart b/lib/contacts/cubits/contact_list_cubit.dart index 71669fc..c985392 100644 --- a/lib/contacts/cubits/contact_list_cubit.dart +++ b/lib/contacts/cubits/contact_list_cubit.dart @@ -36,7 +36,7 @@ class ContactListCubit extends DHTShortArrayCubit { Future createContact({ required proto.Profile remoteProfile, - required IdentityMaster remoteIdentity, + required SuperIdentity remoteSuperIdentity, required TypedKey remoteConversationRecordKey, required TypedKey localConversationRecordKey, }) async { @@ -44,11 +44,9 @@ class ContactListCubit extends DHTShortArrayCubit { final contact = proto.Contact() ..editedProfile = remoteProfile ..remoteProfile = remoteProfile - ..identityMasterJson = jsonEncode(remoteIdentity.toJson()) - ..identityPublicKey = TypedKey( - kind: remoteIdentity.identityRecordKey.kind, - value: remoteIdentity.identityPublicKey) - .toProto() + ..superIdentityJson = jsonEncode(remoteSuperIdentity.toJson()) + ..identityPublicKey = + remoteSuperIdentity.currentInstance.typedPublicKey.toProto() ..remoteConversationRecordKey = remoteConversationRecordKey.toProto() ..localConversationRecordKey = localConversationRecordKey.toProto() ..showAvailability = false; diff --git a/lib/contacts/cubits/conversation_cubit.dart b/lib/contacts/cubits/conversation_cubit.dart index ef7e6ec..115ec84 100644 --- a/lib/contacts/cubits/conversation_cubit.dart +++ b/lib/contacts/cubits/conversation_cubit.dart @@ -47,7 +47,7 @@ class ConversationCubit extends Cubit> { // Open local record key if it is specified final pool = DHTRecordPool.instance; final crypto = await _cachedConversationCrypto(); - final writer = _activeAccountInfo.conversationWriter; + final writer = _activeAccountInfo.identityWriter; final record = await pool.openRecordWrite( _localConversationRecordKey!, writer, debugName: 'ConversationCubit::LocalConversation', @@ -221,7 +221,7 @@ class ConversationCubit extends Cubit> { _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; final crypto = await _cachedConversationCrypto(); - final writer = _activeAccountInfo.conversationWriter; + final writer = _activeAccountInfo.identityWriter; // Open with SMPL scheme for identity writer late final DHTRecord localConversationRecord; @@ -254,8 +254,8 @@ class ConversationCubit extends Cubit> { // Create initial local conversation key contents final conversation = proto.Conversation() ..profile = profile - ..identityMasterJson = jsonEncode( - _activeAccountInfo.localAccount.identityMaster.toJson()) + ..superIdentityJson = jsonEncode( + _activeAccountInfo.localAccount.superIdentity.toJson()) ..messages = messages.recordKey.toProto(); // Write initial conversation to record @@ -289,7 +289,7 @@ class ConversationCubit extends Cubit> { }) async { final crypto = await activeAccountInfo.makeConversationCrypto(remoteIdentityPublicKey); - final writer = activeAccountInfo.conversationWriter; + final writer = activeAccountInfo.identityWriter; return (await DHTLog.create( debugName: 'ConversationCubit::initLocalMessages::LocalMessages', 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 bf98bba..c41185b 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 @@ -91,7 +91,7 @@ class HomeAccountReadyShellState extends State { // Accept await contactListCubit.createContact( remoteProfile: acceptedContact.remoteProfile, - remoteIdentity: acceptedContact.remoteIdentity, + remoteSuperIdentity: acceptedContact.remoteIdentity, remoteConversationRecordKey: acceptedContact.remoteConversationRecordKey, localConversationRecordKey: diff --git a/lib/proto/veilidchat.pb.dart b/lib/proto/veilidchat.pb.dart index 7770d73..1e0395b 100644 --- a/lib/proto/veilidchat.pb.dart +++ b/lib/proto/veilidchat.pb.dart @@ -1026,7 +1026,7 @@ class Conversation extends $pb.GeneratedMessage { 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') + ..aOS(2, _omitFieldNames ? '' : 'superIdentityJson') ..aOM<$0.TypedKey>(3, _omitFieldNames ? '' : 'messages', subBuilder: $0.TypedKey.create) ..hasRequiredFields = false ; @@ -1064,13 +1064,13 @@ class Conversation extends $pb.GeneratedMessage { Profile ensureProfile() => $_ensure(0); @$pb.TagNumber(2) - $core.String get identityMasterJson => $_getSZ(1); + $core.String get superIdentityJson => $_getSZ(1); @$pb.TagNumber(2) - set identityMasterJson($core.String v) { $_setString(1, v); } + set superIdentityJson($core.String v) { $_setString(1, v); } @$pb.TagNumber(2) - $core.bool hasIdentityMasterJson() => $_has(1); + $core.bool hasSuperIdentityJson() => $_has(1); @$pb.TagNumber(2) - void clearIdentityMasterJson() => clearField(2); + void clearSuperIdentityJson() => clearField(2); @$pb.TagNumber(3) $0.TypedKey get messages => $_getN(2); @@ -1427,7 +1427,7 @@ class Contact extends $pb.GeneratedMessage { 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') + ..aOS(3, _omitFieldNames ? '' : 'superIdentityJson') ..aOM<$0.TypedKey>(4, _omitFieldNames ? '' : 'identityPublicKey', subBuilder: $0.TypedKey.create) ..aOM<$0.TypedKey>(5, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create) ..aOM<$0.TypedKey>(6, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create) @@ -1479,13 +1479,13 @@ class Contact extends $pb.GeneratedMessage { Profile ensureRemoteProfile() => $_ensure(1); @$pb.TagNumber(3) - $core.String get identityMasterJson => $_getSZ(2); + $core.String get superIdentityJson => $_getSZ(2); @$pb.TagNumber(3) - set identityMasterJson($core.String v) { $_setString(2, v); } + set superIdentityJson($core.String v) { $_setString(2, v); } @$pb.TagNumber(3) - $core.bool hasIdentityMasterJson() => $_has(2); + $core.bool hasSuperIdentityJson() => $_has(2); @$pb.TagNumber(3) - void clearIdentityMasterJson() => clearField(3); + void clearSuperIdentityJson() => clearField(3); @$pb.TagNumber(4) $0.TypedKey get identityPublicKey => $_getN(3); @@ -1699,7 +1699,7 @@ class ContactRequestPrivate extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactRequestPrivate', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) ..aOM<$0.CryptoKey>(1, _omitFieldNames ? '' : 'writerKey', subBuilder: $0.CryptoKey.create) ..aOM(2, _omitFieldNames ? '' : 'profile', subBuilder: Profile.create) - ..aOM<$0.TypedKey>(3, _omitFieldNames ? '' : 'identityMasterRecordKey', subBuilder: $0.TypedKey.create) + ..aOM<$0.TypedKey>(3, _omitFieldNames ? '' : 'superIdentityRecordKey', subBuilder: $0.TypedKey.create) ..aOM<$0.TypedKey>(4, _omitFieldNames ? '' : 'chatRecordKey', subBuilder: $0.TypedKey.create) ..a<$fixnum.Int64>(5, _omitFieldNames ? '' : 'expiration', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) ..hasRequiredFields = false @@ -1749,15 +1749,15 @@ class ContactRequestPrivate extends $pb.GeneratedMessage { Profile ensureProfile() => $_ensure(1); @$pb.TagNumber(3) - $0.TypedKey get identityMasterRecordKey => $_getN(2); + $0.TypedKey get superIdentityRecordKey => $_getN(2); @$pb.TagNumber(3) - set identityMasterRecordKey($0.TypedKey v) { setField(3, v); } + set superIdentityRecordKey($0.TypedKey v) { setField(3, v); } @$pb.TagNumber(3) - $core.bool hasIdentityMasterRecordKey() => $_has(2); + $core.bool hasSuperIdentityRecordKey() => $_has(2); @$pb.TagNumber(3) - void clearIdentityMasterRecordKey() => clearField(3); + void clearSuperIdentityRecordKey() => clearField(3); @$pb.TagNumber(3) - $0.TypedKey ensureIdentityMasterRecordKey() => $_ensure(2); + $0.TypedKey ensureSuperIdentityRecordKey() => $_ensure(2); @$pb.TagNumber(4) $0.TypedKey get chatRecordKey => $_getN(3); @@ -1788,7 +1788,7 @@ class ContactResponse extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) ..aOB(1, _omitFieldNames ? '' : 'accept') - ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'identityMasterRecordKey', subBuilder: $0.TypedKey.create) + ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'superIdentityRecordKey', subBuilder: $0.TypedKey.create) ..aOM<$0.TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create) ..hasRequiredFields = false ; @@ -1824,15 +1824,15 @@ class ContactResponse extends $pb.GeneratedMessage { void clearAccept() => clearField(1); @$pb.TagNumber(2) - $0.TypedKey get identityMasterRecordKey => $_getN(1); + $0.TypedKey get superIdentityRecordKey => $_getN(1); @$pb.TagNumber(2) - set identityMasterRecordKey($0.TypedKey v) { setField(2, v); } + set superIdentityRecordKey($0.TypedKey v) { setField(2, v); } @$pb.TagNumber(2) - $core.bool hasIdentityMasterRecordKey() => $_has(1); + $core.bool hasSuperIdentityRecordKey() => $_has(1); @$pb.TagNumber(2) - void clearIdentityMasterRecordKey() => clearField(2); + void clearSuperIdentityRecordKey() => clearField(2); @$pb.TagNumber(2) - $0.TypedKey ensureIdentityMasterRecordKey() => $_ensure(1); + $0.TypedKey ensureSuperIdentityRecordKey() => $_ensure(1); @$pb.TagNumber(3) $0.TypedKey get remoteConversationRecordKey => $_getN(2); diff --git a/lib/proto/veilidchat.pbjson.dart b/lib/proto/veilidchat.pbjson.dart index 56ebbe6..ed0bda4 100644 --- a/lib/proto/veilidchat.pbjson.dart +++ b/lib/proto/veilidchat.pbjson.dart @@ -309,7 +309,7 @@ const Conversation$json = { '1': 'Conversation', '2': [ {'1': 'profile', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'profile'}, - {'1': 'identity_master_json', '3': 2, '4': 1, '5': 9, '10': 'identityMasterJson'}, + {'1': 'super_identity_json', '3': 2, '4': 1, '5': 9, '10': 'superIdentityJson'}, {'1': 'messages', '3': 3, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'messages'}, ], }; @@ -317,8 +317,8 @@ const Conversation$json = { /// Descriptor for `Conversation`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List conversationDescriptor = $convert.base64Decode( 'CgxDb252ZXJzYXRpb24SLQoHcHJvZmlsZRgBIAEoCzITLnZlaWxpZGNoYXQuUHJvZmlsZVIHcH' - 'JvZmlsZRIwChRpZGVudGl0eV9tYXN0ZXJfanNvbhgCIAEoCVISaWRlbnRpdHlNYXN0ZXJKc29u' - 'EiwKCG1lc3NhZ2VzGAMgASgLMhAudmVpbGlkLlR5cGVkS2V5UghtZXNzYWdlcw=='); + 'JvZmlsZRIuChNzdXBlcl9pZGVudGl0eV9qc29uGAIgASgJUhFzdXBlcklkZW50aXR5SnNvbhIs' + 'CghtZXNzYWdlcxgDIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIIbWVzc2FnZXM='); @$core.Deprecated('Use chatDescriptor instead') const Chat$json = { @@ -411,7 +411,7 @@ const Contact$json = { '2': [ {'1': 'edited_profile', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'editedProfile'}, {'1': 'remote_profile', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'remoteProfile'}, - {'1': 'identity_master_json', '3': 3, '4': 1, '5': 9, '10': 'identityMasterJson'}, + {'1': 'super_identity_json', '3': 3, '4': 1, '5': 9, '10': 'superIdentityJson'}, {'1': 'identity_public_key', '3': 4, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'identityPublicKey'}, {'1': 'remote_conversation_record_key', '3': 5, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'}, {'1': 'local_conversation_record_key', '3': 6, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'}, @@ -423,13 +423,13 @@ const Contact$json = { final $typed_data.Uint8List contactDescriptor = $convert.base64Decode( 'CgdDb250YWN0EjoKDmVkaXRlZF9wcm9maWxlGAEgASgLMhMudmVpbGlkY2hhdC5Qcm9maWxlUg' '1lZGl0ZWRQcm9maWxlEjoKDnJlbW90ZV9wcm9maWxlGAIgASgLMhMudmVpbGlkY2hhdC5Qcm9m' - 'aWxlUg1yZW1vdGVQcm9maWxlEjAKFGlkZW50aXR5X21hc3Rlcl9qc29uGAMgASgJUhJpZGVudG' - 'l0eU1hc3Rlckpzb24SQAoTaWRlbnRpdHlfcHVibGljX2tleRgEIAEoCzIQLnZlaWxpZC5UeXBl' - 'ZEtleVIRaWRlbnRpdHlQdWJsaWNLZXkSVQoecmVtb3RlX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2' - 'V5GAUgASgLMhAudmVpbGlkLlR5cGVkS2V5UhtyZW1vdGVDb252ZXJzYXRpb25SZWNvcmRLZXkS' - 'UwodbG9jYWxfY29udmVyc2F0aW9uX3JlY29yZF9rZXkYBiABKAsyEC52ZWlsaWQuVHlwZWRLZX' - 'lSGmxvY2FsQ29udmVyc2F0aW9uUmVjb3JkS2V5EisKEXNob3dfYXZhaWxhYmlsaXR5GAcgASgI' - 'UhBzaG93QXZhaWxhYmlsaXR5'); + 'aWxlUg1yZW1vdGVQcm9maWxlEi4KE3N1cGVyX2lkZW50aXR5X2pzb24YAyABKAlSEXN1cGVySW' + 'RlbnRpdHlKc29uEkAKE2lkZW50aXR5X3B1YmxpY19rZXkYBCABKAsyEC52ZWlsaWQuVHlwZWRL' + 'ZXlSEWlkZW50aXR5UHVibGljS2V5ElUKHnJlbW90ZV9jb252ZXJzYXRpb25fcmVjb3JkX2tleR' + 'gFIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIbcmVtb3RlQ29udmVyc2F0aW9uUmVjb3JkS2V5ElMK' + 'HWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAYgASgLMhAudmVpbGlkLlR5cGVkS2V5Uh' + 'psb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRIrChFzaG93X2F2YWlsYWJpbGl0eRgHIAEoCFIQ' + 'c2hvd0F2YWlsYWJpbGl0eQ=='); @$core.Deprecated('Use contactInvitationDescriptor instead') const ContactInvitation$json = { @@ -482,7 +482,7 @@ const ContactRequestPrivate$json = { '2': [ {'1': 'writer_key', '3': 1, '4': 1, '5': 11, '6': '.veilid.CryptoKey', '10': 'writerKey'}, {'1': 'profile', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'profile'}, - {'1': 'identity_master_record_key', '3': 3, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'identityMasterRecordKey'}, + {'1': 'super_identity_record_key', '3': 3, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'superIdentityRecordKey'}, {'1': 'chat_record_key', '3': 4, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'chatRecordKey'}, {'1': 'expiration', '3': 5, '4': 1, '5': 4, '10': 'expiration'}, ], @@ -492,27 +492,27 @@ const ContactRequestPrivate$json = { final $typed_data.Uint8List contactRequestPrivateDescriptor = $convert.base64Decode( 'ChVDb250YWN0UmVxdWVzdFByaXZhdGUSMAoKd3JpdGVyX2tleRgBIAEoCzIRLnZlaWxpZC5Dcn' 'lwdG9LZXlSCXdyaXRlcktleRItCgdwcm9maWxlGAIgASgLMhMudmVpbGlkY2hhdC5Qcm9maWxl' - 'Ugdwcm9maWxlEk0KGmlkZW50aXR5X21hc3Rlcl9yZWNvcmRfa2V5GAMgASgLMhAudmVpbGlkLl' - 'R5cGVkS2V5UhdpZGVudGl0eU1hc3RlclJlY29yZEtleRI4Cg9jaGF0X3JlY29yZF9rZXkYBCAB' - 'KAsyEC52ZWlsaWQuVHlwZWRLZXlSDWNoYXRSZWNvcmRLZXkSHgoKZXhwaXJhdGlvbhgFIAEoBF' - 'IKZXhwaXJhdGlvbg=='); + 'Ugdwcm9maWxlEksKGXN1cGVyX2lkZW50aXR5X3JlY29yZF9rZXkYAyABKAsyEC52ZWlsaWQuVH' + 'lwZWRLZXlSFnN1cGVySWRlbnRpdHlSZWNvcmRLZXkSOAoPY2hhdF9yZWNvcmRfa2V5GAQgASgL' + 'MhAudmVpbGlkLlR5cGVkS2V5Ug1jaGF0UmVjb3JkS2V5Eh4KCmV4cGlyYXRpb24YBSABKARSCm' + 'V4cGlyYXRpb24='); @$core.Deprecated('Use contactResponseDescriptor instead') const ContactResponse$json = { '1': 'ContactResponse', '2': [ {'1': 'accept', '3': 1, '4': 1, '5': 8, '10': 'accept'}, - {'1': 'identity_master_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'identityMasterRecordKey'}, + {'1': 'super_identity_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'superIdentityRecordKey'}, {'1': 'remote_conversation_record_key', '3': 3, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'}, ], }; /// Descriptor for `ContactResponse`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List contactResponseDescriptor = $convert.base64Decode( - 'Cg9Db250YWN0UmVzcG9uc2USFgoGYWNjZXB0GAEgASgIUgZhY2NlcHQSTQoaaWRlbnRpdHlfbW' - 'FzdGVyX3JlY29yZF9rZXkYAiABKAsyEC52ZWlsaWQuVHlwZWRLZXlSF2lkZW50aXR5TWFzdGVy' - 'UmVjb3JkS2V5ElUKHnJlbW90ZV9jb252ZXJzYXRpb25fcmVjb3JkX2tleRgDIAEoCzIQLnZlaW' - 'xpZC5UeXBlZEtleVIbcmVtb3RlQ29udmVyc2F0aW9uUmVjb3JkS2V5'); + 'Cg9Db250YWN0UmVzcG9uc2USFgoGYWNjZXB0GAEgASgIUgZhY2NlcHQSSwoZc3VwZXJfaWRlbn' + 'RpdHlfcmVjb3JkX2tleRgCIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIWc3VwZXJJZGVudGl0eVJl' + 'Y29yZEtleRJVCh5yZW1vdGVfY29udmVyc2F0aW9uX3JlY29yZF9rZXkYAyABKAsyEC52ZWlsaW' + 'QuVHlwZWRLZXlSG3JlbW90ZUNvbnZlcnNhdGlvblJlY29yZEtleQ=='); @$core.Deprecated('Use signedContactResponseDescriptor instead') const SignedContactResponse$json = { diff --git a/lib/proto/veilidchat.proto b/lib/proto/veilidchat.proto index fa701fb..dd2de0b 100644 --- a/lib/proto/veilidchat.proto +++ b/lib/proto/veilidchat.proto @@ -237,8 +237,8 @@ message ReconciledMessage { message Conversation { // Profile to publish to friend Profile profile = 1; - // Identity master (JSON) to publish to friend or chat room - string identity_master_json = 2; + // SuperIdentity (JSON) to publish to friend or chat room + string super_identity_json = 2; // Messages DHTLog veilid.TypedKey messages = 3; } @@ -327,8 +327,8 @@ message Contact { Profile edited_profile = 1; // Copy of friend's profile from remote conversation Profile remote_profile = 2; - // Copy of friend's IdentityMaster in JSON from remote conversation - string identity_master_json = 3; + // Copy of friend's SuperIdentity in JSON from remote conversation + string super_identity_json = 3; // Copy of friend's most recent identity public key from their identityMaster veilid.TypedKey identity_public_key = 4; // Remote conversation key to sync from friend @@ -378,8 +378,8 @@ message ContactRequestPrivate { veilid.CryptoKey writer_key = 1; // Snapshot of profile Profile profile = 2; - // Identity master DHT record key - veilid.TypedKey identity_master_record_key = 3; + // SuperIdentity DHT record key + veilid.TypedKey super_identity_record_key = 3; // Local chat DHT record key veilid.TypedKey chat_record_key = 4; // Expiration timestamp @@ -390,8 +390,8 @@ message ContactRequestPrivate { message ContactResponse { // Accept or reject bool accept = 1; - // Remote identity master DHT record key - veilid.TypedKey identity_master_record_key = 2; + // Remote SuperIdentity DHT record key + veilid.TypedKey super_identity_record_key = 2; // Remote chat DHT record key if accepted veilid.TypedKey remote_conversation_record_key = 3; } diff --git a/lib/veilid_processor/models/processor_connection_state.dart b/lib/veilid_processor/models/processor_connection_state.dart index c5220fb..e92ebdc 100644 --- a/lib/veilid_processor/models/processor_connection_state.dart +++ b/lib/veilid_processor/models/processor_connection_state.dart @@ -15,5 +15,7 @@ class ProcessorConnectionState with _$ProcessorConnectionState { attachment.state == AttachmentState.detaching || attachment.state == AttachmentState.attaching); + bool get isDetached => attachment.state == AttachmentState.detached; + bool get isPublicInternetReady => attachment.publicInternetReady; } diff --git a/packages/veilid_support/lib/identity_support/account_record_info.dart b/packages/veilid_support/lib/identity_support/account_record_info.dart new file mode 100644 index 0000000..60accf9 --- /dev/null +++ b/packages/veilid_support/lib/identity_support/account_record_info.dart @@ -0,0 +1,19 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../veilid_support.dart'; + +part 'account_record_info.freezed.dart'; +part 'account_record_info.g.dart'; + +/// AccountRecordInfo is the key and owner info for the account dht record that +/// is stored in the identity instance record +@freezed +class AccountRecordInfo with _$AccountRecordInfo { + const factory AccountRecordInfo({ + // Top level account keys and secrets + required OwnedDHTRecordPointer accountRecord, + }) = _AccountRecordInfo; + + factory AccountRecordInfo.fromJson(dynamic json) => + _$AccountRecordInfoFromJson(json as Map); +} diff --git a/packages/veilid_support/lib/identity_support/account_record_info.freezed.dart b/packages/veilid_support/lib/identity_support/account_record_info.freezed.dart new file mode 100644 index 0000000..0d5b327 --- /dev/null +++ b/packages/veilid_support/lib/identity_support/account_record_info.freezed.dart @@ -0,0 +1,170 @@ +// 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 'account_record_info.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'); + +AccountRecordInfo _$AccountRecordInfoFromJson(Map json) { + return _AccountRecordInfo.fromJson(json); +} + +/// @nodoc +mixin _$AccountRecordInfo { +// Top level account keys and secrets + OwnedDHTRecordPointer get accountRecord => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $AccountRecordInfoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AccountRecordInfoCopyWith<$Res> { + factory $AccountRecordInfoCopyWith( + AccountRecordInfo value, $Res Function(AccountRecordInfo) then) = + _$AccountRecordInfoCopyWithImpl<$Res, AccountRecordInfo>; + @useResult + $Res call({OwnedDHTRecordPointer accountRecord}); + + $OwnedDHTRecordPointerCopyWith<$Res> get accountRecord; +} + +/// @nodoc +class _$AccountRecordInfoCopyWithImpl<$Res, $Val extends AccountRecordInfo> + implements $AccountRecordInfoCopyWith<$Res> { + _$AccountRecordInfoCopyWithImpl(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? accountRecord = null, + }) { + return _then(_value.copyWith( + accountRecord: null == accountRecord + ? _value.accountRecord + : accountRecord // ignore: cast_nullable_to_non_nullable + as OwnedDHTRecordPointer, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $OwnedDHTRecordPointerCopyWith<$Res> get accountRecord { + return $OwnedDHTRecordPointerCopyWith<$Res>(_value.accountRecord, (value) { + return _then(_value.copyWith(accountRecord: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$AccountRecordInfoImplCopyWith<$Res> + implements $AccountRecordInfoCopyWith<$Res> { + factory _$$AccountRecordInfoImplCopyWith(_$AccountRecordInfoImpl value, + $Res Function(_$AccountRecordInfoImpl) then) = + __$$AccountRecordInfoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({OwnedDHTRecordPointer accountRecord}); + + @override + $OwnedDHTRecordPointerCopyWith<$Res> get accountRecord; +} + +/// @nodoc +class __$$AccountRecordInfoImplCopyWithImpl<$Res> + extends _$AccountRecordInfoCopyWithImpl<$Res, _$AccountRecordInfoImpl> + implements _$$AccountRecordInfoImplCopyWith<$Res> { + __$$AccountRecordInfoImplCopyWithImpl(_$AccountRecordInfoImpl _value, + $Res Function(_$AccountRecordInfoImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? accountRecord = null, + }) { + return _then(_$AccountRecordInfoImpl( + accountRecord: null == accountRecord + ? _value.accountRecord + : accountRecord // ignore: cast_nullable_to_non_nullable + as OwnedDHTRecordPointer, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$AccountRecordInfoImpl implements _AccountRecordInfo { + const _$AccountRecordInfoImpl({required this.accountRecord}); + + factory _$AccountRecordInfoImpl.fromJson(Map json) => + _$$AccountRecordInfoImplFromJson(json); + +// Top level account keys and secrets + @override + final OwnedDHTRecordPointer accountRecord; + + @override + String toString() { + return 'AccountRecordInfo(accountRecord: $accountRecord)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AccountRecordInfoImpl && + (identical(other.accountRecord, accountRecord) || + other.accountRecord == accountRecord)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, accountRecord); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$AccountRecordInfoImplCopyWith<_$AccountRecordInfoImpl> get copyWith => + __$$AccountRecordInfoImplCopyWithImpl<_$AccountRecordInfoImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$AccountRecordInfoImplToJson( + this, + ); + } +} + +abstract class _AccountRecordInfo implements AccountRecordInfo { + const factory _AccountRecordInfo( + {required final OwnedDHTRecordPointer accountRecord}) = + _$AccountRecordInfoImpl; + + factory _AccountRecordInfo.fromJson(Map json) = + _$AccountRecordInfoImpl.fromJson; + + @override // Top level account keys and secrets + OwnedDHTRecordPointer get accountRecord; + @override + @JsonKey(ignore: true) + _$$AccountRecordInfoImplCopyWith<_$AccountRecordInfoImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/packages/veilid_support/lib/identity_support/account_record_info.g.dart b/packages/veilid_support/lib/identity_support/account_record_info.g.dart new file mode 100644 index 0000000..ad9318c --- /dev/null +++ b/packages/veilid_support/lib/identity_support/account_record_info.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'account_record_info.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$AccountRecordInfoImpl _$$AccountRecordInfoImplFromJson( + Map json) => + _$AccountRecordInfoImpl( + accountRecord: OwnedDHTRecordPointer.fromJson(json['account_record']), + ); + +Map _$$AccountRecordInfoImplToJson( + _$AccountRecordInfoImpl instance) => + { + 'account_record': instance.accountRecord.toJson(), + }; diff --git a/packages/veilid_support/lib/identity_support/exceptions.dart b/packages/veilid_support/lib/identity_support/exceptions.dart new file mode 100644 index 0000000..f0774b4 --- /dev/null +++ b/packages/veilid_support/lib/identity_support/exceptions.dart @@ -0,0 +1,13 @@ +/// 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'; +} diff --git a/packages/veilid_support/lib/identity_support/identity.dart b/packages/veilid_support/lib/identity_support/identity.dart new file mode 100644 index 0000000..ea9c38c --- /dev/null +++ b/packages/veilid_support/lib/identity_support/identity.dart @@ -0,0 +1,25 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import 'account_record_info.dart'; + +part 'identity.freezed.dart'; +part 'identity.g.dart'; + +/// Identity points to accounts associated with this IdentityInstance +/// accountRecords field has a map of bundle id or uuid to account key pairs +/// DHT Schema: DFLT(1) +/// DHT Key (Private): IdentityInstance.recordKey +/// DHT Owner Key: IdentityInstance.publicKey +/// DHT Secret: IdentityInstance Secret Key (stored encrypted with unlock code +/// in local table store) +@freezed +class Identity with _$Identity { + const factory Identity({ + // Top level account keys and secrets + required IMap> accountRecords, + }) = _Identity; + + factory Identity.fromJson(dynamic json) => + _$IdentityFromJson(json as Map); +} diff --git a/packages/veilid_support/lib/identity_support/identity.freezed.dart b/packages/veilid_support/lib/identity_support/identity.freezed.dart new file mode 100644 index 0000000..5977a26 --- /dev/null +++ b/packages/veilid_support/lib/identity_support/identity.freezed.dart @@ -0,0 +1,156 @@ +// 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 'identity.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'); + +Identity _$IdentityFromJson(Map json) { + return _Identity.fromJson(json); +} + +/// @nodoc +mixin _$Identity { +// Top level account keys and secrets + IMap> get accountRecords => + throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $IdentityCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $IdentityCopyWith<$Res> { + factory $IdentityCopyWith(Identity value, $Res Function(Identity) then) = + _$IdentityCopyWithImpl<$Res, Identity>; + @useResult + $Res call({IMap> accountRecords}); +} + +/// @nodoc +class _$IdentityCopyWithImpl<$Res, $Val extends Identity> + implements $IdentityCopyWith<$Res> { + _$IdentityCopyWithImpl(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? accountRecords = null, + }) { + return _then(_value.copyWith( + accountRecords: null == accountRecords + ? _value.accountRecords + : accountRecords // ignore: cast_nullable_to_non_nullable + as IMap>, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$IdentityImplCopyWith<$Res> + implements $IdentityCopyWith<$Res> { + factory _$$IdentityImplCopyWith( + _$IdentityImpl value, $Res Function(_$IdentityImpl) then) = + __$$IdentityImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({IMap> accountRecords}); +} + +/// @nodoc +class __$$IdentityImplCopyWithImpl<$Res> + extends _$IdentityCopyWithImpl<$Res, _$IdentityImpl> + implements _$$IdentityImplCopyWith<$Res> { + __$$IdentityImplCopyWithImpl( + _$IdentityImpl _value, $Res Function(_$IdentityImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? accountRecords = null, + }) { + return _then(_$IdentityImpl( + accountRecords: null == accountRecords + ? _value.accountRecords + : accountRecords // ignore: cast_nullable_to_non_nullable + as IMap>, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$IdentityImpl implements _Identity { + const _$IdentityImpl({required this.accountRecords}); + + factory _$IdentityImpl.fromJson(Map json) => + _$$IdentityImplFromJson(json); + +// Top level account keys and secrets + @override + final IMap> accountRecords; + + @override + String toString() { + return 'Identity(accountRecords: $accountRecords)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$IdentityImpl && + (identical(other.accountRecords, accountRecords) || + other.accountRecords == accountRecords)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, accountRecords); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$IdentityImplCopyWith<_$IdentityImpl> get copyWith => + __$$IdentityImplCopyWithImpl<_$IdentityImpl>(this, _$identity); + + @override + Map toJson() { + return _$$IdentityImplToJson( + this, + ); + } +} + +abstract class _Identity implements Identity { + const factory _Identity( + {required final IMap> + accountRecords}) = _$IdentityImpl; + + factory _Identity.fromJson(Map json) = + _$IdentityImpl.fromJson; + + @override // Top level account keys and secrets + IMap> get accountRecords; + @override + @JsonKey(ignore: true) + _$$IdentityImplCopyWith<_$IdentityImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/packages/veilid_support/lib/identity_support/identity.g.dart b/packages/veilid_support/lib/identity_support/identity.g.dart new file mode 100644 index 0000000..afc9088 --- /dev/null +++ b/packages/veilid_support/lib/identity_support/identity.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'identity.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$IdentityImpl _$$IdentityImplFromJson(Map json) => + _$IdentityImpl( + accountRecords: IMap>.fromJson( + json['account_records'] as Map, + (value) => value as String, + (value) => ISet.fromJson( + value, (value) => AccountRecordInfo.fromJson(value))), + ); + +Map _$$IdentityImplToJson(_$IdentityImpl instance) => + { + 'account_records': instance.accountRecords.toJson( + (value) => value, + (value) => value.toJson( + (value) => value.toJson(), + ), + ), + }; diff --git a/packages/veilid_support/lib/identity_support/identity_instance.dart b/packages/veilid_support/lib/identity_support/identity_instance.dart new file mode 100644 index 0000000..b11e223 --- /dev/null +++ b/packages/veilid_support/lib/identity_support/identity_instance.dart @@ -0,0 +1,274 @@ +import 'dart:typed_data'; + +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../src/veilid_log.dart'; +import '../veilid_support.dart'; +import 'exceptions.dart'; + +part 'identity_instance.freezed.dart'; +part 'identity_instance.g.dart'; + +@freezed +class IdentityInstance with _$IdentityInstance { + const factory IdentityInstance({ + // Private DHT record storing identity account mapping + required TypedKey recordKey, + + // Public key of identity instance + required PublicKey publicKey, + + // Secret key of identity instance + // Encrypted with DH(publicKey, SuperIdentity.secret) with appended salt + // Used to recover accounts without generating a new instance + @Uint8ListJsonConverter() required Uint8List encryptedSecretKey, + + // Signature of SuperInstance recordKey and SuperInstance publicKey + // by publicKey + required Signature superSignature, + + // Signature of recordKey, publicKey, encryptedSecretKey, and superSignature + // by SuperIdentity publicKey + required Signature signature, + }) = _IdentityInstance; + + factory IdentityInstance.fromJson(dynamic json) => + _$IdentityInstanceFromJson(json as Map); + + const IdentityInstance._(); + + //////////////////////////////////////////////////////////////////////////// + // Public interface + + /// Delete this identity instance record + /// Only deletes from the local machine not the DHT + Future delete() async { + final pool = DHTRecordPool.instance; + await pool.deleteRecord(recordKey); + } + + Future get cryptoSystem => + Veilid.instance.getCryptoSystem(recordKey.kind); + + Future getPrivateCrypto(SecretKey secretKey) async => + DHTRecordPool.privateCryptoFromTypedSecret( + TypedKey(kind: recordKey.kind, value: secretKey)); + + KeyPair writer(SecretKey secret) => KeyPair(key: publicKey, secret: secret); + + TypedKey get typedPublicKey => + TypedKey(kind: recordKey.kind, value: publicKey); + + Future validateIdentitySecret(SecretKey secretKey) async { + final cs = await cryptoSystem; + final keyOk = await cs.validateKeyPair(publicKey, secretKey); + if (!keyOk) { + throw IdentityException.invalid; + } + return cs; + } + + /// Read the account record info for a specific accountKey from the identity + /// instance record using the identity instance secret key to decrypt + Future> readAccount( + {required TypedKey superRecordKey, + required SecretKey secretKey, + required String accountKey}) async { + // Read the identity key to get the account keys + final pool = DHTRecordPool.instance; + + final identityRecordCrypto = await getPrivateCrypto(secretKey); + + late final List accountRecordInfo; + await (await pool.openRecordRead(recordKey, + debugName: 'IdentityInstance::readAccounts::IdentityRecord', + parent: superRecordKey, + 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 IdentityException.readError; + } + final accountRecords = IMapOfSets.from(identity.accountRecords); + final vcAccounts = accountRecords.get(accountKey); + + accountRecordInfo = vcAccounts.toList(); + }); + + return accountRecordInfo; + } + + /// Creates a new Account associated with super identity and store it in the + /// identity instance record. + Future addAccount({ + required TypedKey superRecordKey, + required SecretKey secretKey, + required String accountKey, + required Future Function(TypedKey parent) createAccountCallback, + int maxAccounts = 1, + }) async { + final pool = DHTRecordPool.instance; + + /////// Add account with profile to DHT + + // Open identity key for writing + veilidLoggy.debug('Opening identity record'); + return (await pool.openRecordWrite(recordKey, writer(secretKey), + debugName: 'IdentityInstance::addAccount::IdentityRecord', + parent: superRecordKey)) + .scope((identityRec) async { + // Create new account to insert into identity + veilidLoggy.debug('Creating new account'); + return (await pool.createRecord( + debugName: + 'IdentityInstance::addAccount::IdentityRecord::AccountRecord', + parent: identityRec.key)) + .deleteScope((accountRec) async { + final account = await createAccountCallback(accountRec.key); + // Write account key + veilidLoggy.debug('Writing account record'); + await accountRec.eventualWriteBytes(account); + + // Update identity key to include account + final newAccountRecordInfo = AccountRecordInfo( + accountRecord: OwnedDHTRecordPointer( + recordKey: accountRec.key, owner: accountRec.ownerKeyPair!)); + + 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); + }); + + return newAccountRecordInfo; + }); + }); + } + + //////////////////////////////////////////////////////////////////////////// + // Internal implementation + + Future validateIdentityInstance( + {required TypedKey superRecordKey, + required PublicKey superPublicKey}) async { + final sigValid = await IdentityInstance.validateIdentitySignature( + recordKey: recordKey, + publicKey: publicKey, + encryptedSecretKey: encryptedSecretKey, + superSignature: superSignature, + superPublicKey: superPublicKey, + signature: signature); + if (!sigValid) { + return false; + } + + final superSigValid = await IdentityInstance.validateSuperSignature( + superRecordKey: superRecordKey, + superPublicKey: superPublicKey, + publicKey: publicKey, + superSignature: superSignature); + if (!superSigValid) { + return false; + } + + return true; + } + + static Uint8List signatureBytes({ + required TypedKey recordKey, + required PublicKey publicKey, + required Uint8List encryptedSecretKey, + required Signature superSignature, + }) { + final sigBuf = BytesBuilder() + ..add(recordKey.decode()) + ..add(publicKey.decode()) + ..add(encryptedSecretKey) + ..add(superSignature.decode()); + return sigBuf.toBytes(); + } + + static Future validateIdentitySignature({ + required TypedKey recordKey, + required PublicKey publicKey, + required Uint8List encryptedSecretKey, + required Signature superSignature, + required PublicKey superPublicKey, + required Signature signature, + }) async { + final cs = await Veilid.instance.getCryptoSystem(recordKey.kind); + final identitySigBytes = IdentityInstance.signatureBytes( + recordKey: recordKey, + publicKey: publicKey, + encryptedSecretKey: encryptedSecretKey, + superSignature: superSignature); + return cs.verify(superPublicKey, identitySigBytes, signature); + } + + static Future createIdentitySignature({ + required TypedKey recordKey, + required PublicKey publicKey, + required Uint8List encryptedSecretKey, + required Signature superSignature, + required PublicKey superPublicKey, + required SecretKey superSecret, + }) async { + final cs = await Veilid.instance.getCryptoSystem(recordKey.kind); + final identitySigBytes = IdentityInstance.signatureBytes( + recordKey: recordKey, + publicKey: publicKey, + encryptedSecretKey: encryptedSecretKey, + superSignature: superSignature); + return cs.sign(superPublicKey, superSecret, identitySigBytes); + } + + static Uint8List superSignatureBytes({ + required TypedKey superRecordKey, + required PublicKey superPublicKey, + }) { + final superSigBuf = BytesBuilder() + ..add(superRecordKey.decode()) + ..add(superPublicKey.decode()); + return superSigBuf.toBytes(); + } + + static Future validateSuperSignature({ + required TypedKey superRecordKey, + required PublicKey superPublicKey, + required PublicKey publicKey, + required Signature superSignature, + }) async { + final cs = await Veilid.instance.getCryptoSystem(superRecordKey.kind); + final superSigBytes = IdentityInstance.superSignatureBytes( + superRecordKey: superRecordKey, + superPublicKey: superPublicKey, + ); + return cs.verify(publicKey, superSigBytes, superSignature); + } + + static Future createSuperSignature({ + required TypedKey superRecordKey, + required PublicKey superPublicKey, + required PublicKey publicKey, + required SecretKey secretKey, + }) async { + final cs = await Veilid.instance.getCryptoSystem(superRecordKey.kind); + final superSigBytes = IdentityInstance.superSignatureBytes( + superRecordKey: superRecordKey, + superPublicKey: superPublicKey, + ); + return cs.sign(publicKey, secretKey, superSigBytes); + } +} diff --git a/packages/veilid_support/lib/identity_support/identity_instance.freezed.dart b/packages/veilid_support/lib/identity_support/identity_instance.freezed.dart new file mode 100644 index 0000000..4d6c4ad --- /dev/null +++ b/packages/veilid_support/lib/identity_support/identity_instance.freezed.dart @@ -0,0 +1,274 @@ +// 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 'identity_instance.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'); + +IdentityInstance _$IdentityInstanceFromJson(Map json) { + return _IdentityInstance.fromJson(json); +} + +/// @nodoc +mixin _$IdentityInstance { +// Private DHT record storing identity account mapping + Typed get recordKey => + throw _privateConstructorUsedError; // Public key of identity instance + FixedEncodedString43 get publicKey => + throw _privateConstructorUsedError; // Secret key of identity instance +// Encrypted with DH(publicKey, SuperIdentity.secret) with appended salt +// Used to recover accounts without generating a new instance + @Uint8ListJsonConverter() + Uint8List get encryptedSecretKey => + throw _privateConstructorUsedError; // Signature of SuperInstance recordKey and SuperInstance publicKey +// by publicKey + FixedEncodedString86 get superSignature => + throw _privateConstructorUsedError; // Signature of recordKey, publicKey, encryptedSecretKey, and superSignature +// by SuperIdentity publicKey + FixedEncodedString86 get signature => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $IdentityInstanceCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $IdentityInstanceCopyWith<$Res> { + factory $IdentityInstanceCopyWith( + IdentityInstance value, $Res Function(IdentityInstance) then) = + _$IdentityInstanceCopyWithImpl<$Res, IdentityInstance>; + @useResult + $Res call( + {Typed recordKey, + FixedEncodedString43 publicKey, + @Uint8ListJsonConverter() Uint8List encryptedSecretKey, + FixedEncodedString86 superSignature, + FixedEncodedString86 signature}); +} + +/// @nodoc +class _$IdentityInstanceCopyWithImpl<$Res, $Val extends IdentityInstance> + implements $IdentityInstanceCopyWith<$Res> { + _$IdentityInstanceCopyWithImpl(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? recordKey = null, + Object? publicKey = null, + Object? encryptedSecretKey = null, + Object? superSignature = null, + Object? signature = null, + }) { + return _then(_value.copyWith( + recordKey: null == recordKey + ? _value.recordKey + : recordKey // ignore: cast_nullable_to_non_nullable + as Typed, + publicKey: null == publicKey + ? _value.publicKey + : publicKey // ignore: cast_nullable_to_non_nullable + as FixedEncodedString43, + encryptedSecretKey: null == encryptedSecretKey + ? _value.encryptedSecretKey + : encryptedSecretKey // ignore: cast_nullable_to_non_nullable + as Uint8List, + superSignature: null == superSignature + ? _value.superSignature + : superSignature // ignore: cast_nullable_to_non_nullable + as FixedEncodedString86, + signature: null == signature + ? _value.signature + : signature // ignore: cast_nullable_to_non_nullable + as FixedEncodedString86, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$IdentityInstanceImplCopyWith<$Res> + implements $IdentityInstanceCopyWith<$Res> { + factory _$$IdentityInstanceImplCopyWith(_$IdentityInstanceImpl value, + $Res Function(_$IdentityInstanceImpl) then) = + __$$IdentityInstanceImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {Typed recordKey, + FixedEncodedString43 publicKey, + @Uint8ListJsonConverter() Uint8List encryptedSecretKey, + FixedEncodedString86 superSignature, + FixedEncodedString86 signature}); +} + +/// @nodoc +class __$$IdentityInstanceImplCopyWithImpl<$Res> + extends _$IdentityInstanceCopyWithImpl<$Res, _$IdentityInstanceImpl> + implements _$$IdentityInstanceImplCopyWith<$Res> { + __$$IdentityInstanceImplCopyWithImpl(_$IdentityInstanceImpl _value, + $Res Function(_$IdentityInstanceImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? recordKey = null, + Object? publicKey = null, + Object? encryptedSecretKey = null, + Object? superSignature = null, + Object? signature = null, + }) { + return _then(_$IdentityInstanceImpl( + recordKey: null == recordKey + ? _value.recordKey + : recordKey // ignore: cast_nullable_to_non_nullable + as Typed, + publicKey: null == publicKey + ? _value.publicKey + : publicKey // ignore: cast_nullable_to_non_nullable + as FixedEncodedString43, + encryptedSecretKey: null == encryptedSecretKey + ? _value.encryptedSecretKey + : encryptedSecretKey // ignore: cast_nullable_to_non_nullable + as Uint8List, + superSignature: null == superSignature + ? _value.superSignature + : superSignature // ignore: cast_nullable_to_non_nullable + as FixedEncodedString86, + signature: null == signature + ? _value.signature + : signature // ignore: cast_nullable_to_non_nullable + as FixedEncodedString86, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$IdentityInstanceImpl extends _IdentityInstance { + const _$IdentityInstanceImpl( + {required this.recordKey, + required this.publicKey, + @Uint8ListJsonConverter() required this.encryptedSecretKey, + required this.superSignature, + required this.signature}) + : super._(); + + factory _$IdentityInstanceImpl.fromJson(Map json) => + _$$IdentityInstanceImplFromJson(json); + +// Private DHT record storing identity account mapping + @override + final Typed recordKey; +// Public key of identity instance + @override + final FixedEncodedString43 publicKey; +// Secret key of identity instance +// Encrypted with DH(publicKey, SuperIdentity.secret) with appended salt +// Used to recover accounts without generating a new instance + @override + @Uint8ListJsonConverter() + final Uint8List encryptedSecretKey; +// Signature of SuperInstance recordKey and SuperInstance publicKey +// by publicKey + @override + final FixedEncodedString86 superSignature; +// Signature of recordKey, publicKey, encryptedSecretKey, and superSignature +// by SuperIdentity publicKey + @override + final FixedEncodedString86 signature; + + @override + String toString() { + return 'IdentityInstance(recordKey: $recordKey, publicKey: $publicKey, encryptedSecretKey: $encryptedSecretKey, superSignature: $superSignature, signature: $signature)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$IdentityInstanceImpl && + (identical(other.recordKey, recordKey) || + other.recordKey == recordKey) && + (identical(other.publicKey, publicKey) || + other.publicKey == publicKey) && + const DeepCollectionEquality() + .equals(other.encryptedSecretKey, encryptedSecretKey) && + (identical(other.superSignature, superSignature) || + other.superSignature == superSignature) && + (identical(other.signature, signature) || + other.signature == signature)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, + recordKey, + publicKey, + const DeepCollectionEquality().hash(encryptedSecretKey), + superSignature, + signature); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$IdentityInstanceImplCopyWith<_$IdentityInstanceImpl> get copyWith => + __$$IdentityInstanceImplCopyWithImpl<_$IdentityInstanceImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$IdentityInstanceImplToJson( + this, + ); + } +} + +abstract class _IdentityInstance extends IdentityInstance { + const factory _IdentityInstance( + {required final Typed recordKey, + required final FixedEncodedString43 publicKey, + @Uint8ListJsonConverter() required final Uint8List encryptedSecretKey, + required final FixedEncodedString86 superSignature, + required final FixedEncodedString86 signature}) = _$IdentityInstanceImpl; + const _IdentityInstance._() : super._(); + + factory _IdentityInstance.fromJson(Map json) = + _$IdentityInstanceImpl.fromJson; + + @override // Private DHT record storing identity account mapping + Typed get recordKey; + @override // Public key of identity instance + FixedEncodedString43 get publicKey; + @override // Secret key of identity instance +// Encrypted with DH(publicKey, SuperIdentity.secret) with appended salt +// Used to recover accounts without generating a new instance + @Uint8ListJsonConverter() + Uint8List get encryptedSecretKey; + @override // Signature of SuperInstance recordKey and SuperInstance publicKey +// by publicKey + FixedEncodedString86 get superSignature; + @override // Signature of recordKey, publicKey, encryptedSecretKey, and superSignature +// by SuperIdentity publicKey + FixedEncodedString86 get signature; + @override + @JsonKey(ignore: true) + _$$IdentityInstanceImplCopyWith<_$IdentityInstanceImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/packages/veilid_support/lib/identity_support/identity_instance.g.dart b/packages/veilid_support/lib/identity_support/identity_instance.g.dart new file mode 100644 index 0000000..cb228e6 --- /dev/null +++ b/packages/veilid_support/lib/identity_support/identity_instance.g.dart @@ -0,0 +1,29 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'identity_instance.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$IdentityInstanceImpl _$$IdentityInstanceImplFromJson( + Map json) => + _$IdentityInstanceImpl( + recordKey: Typed.fromJson(json['record_key']), + publicKey: FixedEncodedString43.fromJson(json['public_key']), + encryptedSecretKey: + const Uint8ListJsonConverter().fromJson(json['encrypted_secret_key']), + superSignature: FixedEncodedString86.fromJson(json['super_signature']), + signature: FixedEncodedString86.fromJson(json['signature']), + ); + +Map _$$IdentityInstanceImplToJson( + _$IdentityInstanceImpl instance) => + { + 'record_key': instance.recordKey.toJson(), + 'public_key': instance.publicKey.toJson(), + 'encrypted_secret_key': + const Uint8ListJsonConverter().toJson(instance.encryptedSecretKey), + 'super_signature': instance.superSignature.toJson(), + 'signature': instance.signature.toJson(), + }; diff --git a/packages/veilid_support/lib/identity_support/identity_support.dart b/packages/veilid_support/lib/identity_support/identity_support.dart new file mode 100644 index 0000000..463be9a --- /dev/null +++ b/packages/veilid_support/lib/identity_support/identity_support.dart @@ -0,0 +1,6 @@ +export 'account_record_info.dart'; +export 'exceptions.dart'; +export 'identity.dart'; +export 'identity_instance.dart'; +export 'super_identity.dart'; +export 'writable_super_identity.dart'; diff --git a/packages/veilid_support/lib/identity_support/super_identity.dart b/packages/veilid_support/lib/identity_support/super_identity.dart new file mode 100644 index 0000000..e4ec8fc --- /dev/null +++ b/packages/veilid_support/lib/identity_support/super_identity.dart @@ -0,0 +1,174 @@ +import 'dart:typed_data'; + +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../veilid_support.dart'; + +part 'super_identity.freezed.dart'; +part 'super_identity.g.dart'; + +/// SuperIdentity key structure for created account +/// +/// SuperIdentity key allows for regeneration of identity DHT record +/// Bidirectional Super<->Instance signature allows for +/// chain of identity ownership for account recovery process +/// +/// Backed by a DHT key at superRecordKey, the secret is kept +/// completely offline and only written to upon account recovery +/// +/// DHT Schema: DFLT(1) +/// DHT Record Key (Public): SuperIdentity.recordKey +/// DHT Owner Key: SuperIdentity.publicKey +/// DHT Owner Secret: SuperIdentity Secret Key (kept offline) +/// Encryption: None +@freezed +class SuperIdentity with _$SuperIdentity { + const factory SuperIdentity({ + /// Public DHT record storing this structure for account recovery + /// changing this can migrate/forward the SuperIdentity to a new DHT record + /// Instances should not hash this recordKey, rather the actual record + /// key used to store the superIdentity, as this may change. + required TypedKey recordKey, + + /// Public key of the SuperIdentity used to sign identity keys for recovery + /// This must match the owner of the superRecord DHT record and can not be + /// changed without changing the record + required PublicKey publicKey, + + /// Current identity instance + /// The most recently generated identity instance for this SuperIdentity + required IdentityInstance currentInstance, + + /// Deprecated identity instances + /// These may be compromised and should not be considered valid for + /// new signatures, but may be used to validate old signatures + required List deprecatedInstances, + + /// Deprecated superRecords + /// These may be compromised and should not be considered valid for + /// new signatures, but may be used to validate old signatures + required List deprecatedSuperRecordKeys, + + /// Signature of recordKey, currentInstance signature, + /// signatures of deprecatedInstances, and deprecatedSuperRecordKeys + /// by publicKey + required Signature signature, + }) = _SuperIdentity; + + //////////////////////////////////////////////////////////////////////////// + // Constructors + + factory SuperIdentity.fromJson(dynamic json) => + _$SuperIdentityFromJson(json as Map); + + const SuperIdentity._(); + + /// Opens an existing super identity and validates it + static Future open({required TypedKey superRecordKey}) async { + final pool = DHTRecordPool.instance; + + // SuperIdentity DHT record is public/unencrypted + return (await pool.openRecordRead(superRecordKey, + debugName: 'SuperIdentity::openSuperIdentity::SuperIdentityRecord')) + .deleteScope((superRec) async { + final superIdentity = (await superRec.getJson(SuperIdentity.fromJson, + refreshMode: DHTRecordRefreshMode.network))!; + + // Validate current IdentityInstance + if (!await superIdentity.currentInstance.validateIdentityInstance( + superRecordKey: superRecordKey, + superPublicKey: superIdentity.publicKey)) { + // Invalid current IdentityInstance signature(s) + throw IdentityException.invalid; + } + + // Validate deprecated IdentityInstances + for (final deprecatedInstance in superIdentity.deprecatedInstances) { + if (!await deprecatedInstance.validateIdentityInstance( + superRecordKey: superRecordKey, + superPublicKey: superIdentity.publicKey)) { + // Invalid deprecated IdentityInstance signature(s) + throw IdentityException.invalid; + } + } + + // Validate SuperIdentity + final deprecatedInstancesSignatures = + superIdentity.deprecatedInstances.map((x) => x.signature).toList(); + if (!await _validateSuperIdentitySignature( + recordKey: superIdentity.recordKey, + currentInstanceSignature: superIdentity.currentInstance.signature, + deprecatedInstancesSignatures: deprecatedInstancesSignatures, + deprecatedSuperRecordKeys: superIdentity.deprecatedSuperRecordKeys, + publicKey: superIdentity.publicKey, + signature: superIdentity.signature)) { + // Invalid SuperIdentity signature + throw IdentityException.invalid; + } + + return superIdentity; + }); + } + + //////////////////////////////////////////////////////////////////////////// + // Public Interface + + /// Deletes a super identity and the identity instance records under it + /// Only deletes from the local machine not the DHT + Future delete() async { + final pool = DHTRecordPool.instance; + await pool.deleteRecord(recordKey); + } + + Future get cryptoSystem => + Veilid.instance.getCryptoSystem(recordKey.kind); + + KeyPair writer(SecretKey secretKey) => + KeyPair(key: publicKey, secret: secretKey); + + TypedKey get typedPublicKey => + TypedKey(kind: recordKey.kind, value: publicKey); + + Future validateSecret(SecretKey secretKey) async { + final cs = await cryptoSystem; + final keyOk = await cs.validateKeyPair(publicKey, secretKey); + if (!keyOk) { + throw IdentityException.invalid; + } + return cs; + } + + //////////////////////////////////////////////////////////////////////////// + // Internal implementation + + static Uint8List signatureBytes({ + required TypedKey recordKey, + required Signature currentInstanceSignature, + required List deprecatedInstancesSignatures, + required List deprecatedSuperRecordKeys, + }) { + final sigBuf = BytesBuilder() + ..add(recordKey.decode()) + ..add(currentInstanceSignature.decode()) + ..add(deprecatedInstancesSignatures.expand((s) => s.decode()).toList()) + ..add(deprecatedSuperRecordKeys.expand((s) => s.decode()).toList()); + return sigBuf.toBytes(); + } + + static Future _validateSuperIdentitySignature({ + required TypedKey recordKey, + required Signature currentInstanceSignature, + required List deprecatedInstancesSignatures, + required List deprecatedSuperRecordKeys, + required PublicKey publicKey, + required Signature signature, + }) async { + final cs = await Veilid.instance.getCryptoSystem(recordKey.kind); + final sigBytes = SuperIdentity.signatureBytes( + recordKey: recordKey, + currentInstanceSignature: currentInstanceSignature, + deprecatedInstancesSignatures: deprecatedInstancesSignatures, + deprecatedSuperRecordKeys: deprecatedSuperRecordKeys); + return cs.verify(publicKey, sigBytes, signature); + } +} diff --git a/packages/veilid_support/lib/identity_support/super_identity.freezed.dart b/packages/veilid_support/lib/identity_support/super_identity.freezed.dart new file mode 100644 index 0000000..dc1c69a --- /dev/null +++ b/packages/veilid_support/lib/identity_support/super_identity.freezed.dart @@ -0,0 +1,380 @@ +// 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 'super_identity.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'); + +SuperIdentity _$SuperIdentityFromJson(Map json) { + return _SuperIdentity.fromJson(json); +} + +/// @nodoc +mixin _$SuperIdentity { + /// Public DHT record storing this structure for account recovery + /// changing this can migrate/forward the SuperIdentity to a new DHT record + /// Instances should not hash this recordKey, rather the actual record + /// key used to store the superIdentity, as this may change. + Typed get recordKey => + throw _privateConstructorUsedError; + + /// Public key of the SuperIdentity used to sign identity keys for recovery + /// This must match the owner of the superRecord DHT record and can not be + /// changed without changing the record + FixedEncodedString43 get publicKey => throw _privateConstructorUsedError; + + /// Current identity instance + /// The most recently generated identity instance for this SuperIdentity + IdentityInstance get currentInstance => throw _privateConstructorUsedError; + + /// Deprecated identity instances + /// These may be compromised and should not be considered valid for + /// new signatures, but may be used to validate old signatures + List get deprecatedInstances => + throw _privateConstructorUsedError; + + /// Deprecated superRecords + /// These may be compromised and should not be considered valid for + /// new signatures, but may be used to validate old signatures + List> get deprecatedSuperRecordKeys => + throw _privateConstructorUsedError; + + /// Signature of recordKey, currentInstance signature, + /// signatures of deprecatedInstances, and deprecatedSuperRecordKeys + /// by publicKey + FixedEncodedString86 get signature => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $SuperIdentityCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SuperIdentityCopyWith<$Res> { + factory $SuperIdentityCopyWith( + SuperIdentity value, $Res Function(SuperIdentity) then) = + _$SuperIdentityCopyWithImpl<$Res, SuperIdentity>; + @useResult + $Res call( + {Typed recordKey, + FixedEncodedString43 publicKey, + IdentityInstance currentInstance, + List deprecatedInstances, + List> deprecatedSuperRecordKeys, + FixedEncodedString86 signature}); + + $IdentityInstanceCopyWith<$Res> get currentInstance; +} + +/// @nodoc +class _$SuperIdentityCopyWithImpl<$Res, $Val extends SuperIdentity> + implements $SuperIdentityCopyWith<$Res> { + _$SuperIdentityCopyWithImpl(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? recordKey = null, + Object? publicKey = null, + Object? currentInstance = null, + Object? deprecatedInstances = null, + Object? deprecatedSuperRecordKeys = null, + Object? signature = null, + }) { + return _then(_value.copyWith( + recordKey: null == recordKey + ? _value.recordKey + : recordKey // ignore: cast_nullable_to_non_nullable + as Typed, + publicKey: null == publicKey + ? _value.publicKey + : publicKey // ignore: cast_nullable_to_non_nullable + as FixedEncodedString43, + currentInstance: null == currentInstance + ? _value.currentInstance + : currentInstance // ignore: cast_nullable_to_non_nullable + as IdentityInstance, + deprecatedInstances: null == deprecatedInstances + ? _value.deprecatedInstances + : deprecatedInstances // ignore: cast_nullable_to_non_nullable + as List, + deprecatedSuperRecordKeys: null == deprecatedSuperRecordKeys + ? _value.deprecatedSuperRecordKeys + : deprecatedSuperRecordKeys // ignore: cast_nullable_to_non_nullable + as List>, + signature: null == signature + ? _value.signature + : signature // ignore: cast_nullable_to_non_nullable + as FixedEncodedString86, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $IdentityInstanceCopyWith<$Res> get currentInstance { + return $IdentityInstanceCopyWith<$Res>(_value.currentInstance, (value) { + return _then(_value.copyWith(currentInstance: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$SuperIdentityImplCopyWith<$Res> + implements $SuperIdentityCopyWith<$Res> { + factory _$$SuperIdentityImplCopyWith( + _$SuperIdentityImpl value, $Res Function(_$SuperIdentityImpl) then) = + __$$SuperIdentityImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {Typed recordKey, + FixedEncodedString43 publicKey, + IdentityInstance currentInstance, + List deprecatedInstances, + List> deprecatedSuperRecordKeys, + FixedEncodedString86 signature}); + + @override + $IdentityInstanceCopyWith<$Res> get currentInstance; +} + +/// @nodoc +class __$$SuperIdentityImplCopyWithImpl<$Res> + extends _$SuperIdentityCopyWithImpl<$Res, _$SuperIdentityImpl> + implements _$$SuperIdentityImplCopyWith<$Res> { + __$$SuperIdentityImplCopyWithImpl( + _$SuperIdentityImpl _value, $Res Function(_$SuperIdentityImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? recordKey = null, + Object? publicKey = null, + Object? currentInstance = null, + Object? deprecatedInstances = null, + Object? deprecatedSuperRecordKeys = null, + Object? signature = null, + }) { + return _then(_$SuperIdentityImpl( + recordKey: null == recordKey + ? _value.recordKey + : recordKey // ignore: cast_nullable_to_non_nullable + as Typed, + publicKey: null == publicKey + ? _value.publicKey + : publicKey // ignore: cast_nullable_to_non_nullable + as FixedEncodedString43, + currentInstance: null == currentInstance + ? _value.currentInstance + : currentInstance // ignore: cast_nullable_to_non_nullable + as IdentityInstance, + deprecatedInstances: null == deprecatedInstances + ? _value._deprecatedInstances + : deprecatedInstances // ignore: cast_nullable_to_non_nullable + as List, + deprecatedSuperRecordKeys: null == deprecatedSuperRecordKeys + ? _value._deprecatedSuperRecordKeys + : deprecatedSuperRecordKeys // ignore: cast_nullable_to_non_nullable + as List>, + signature: null == signature + ? _value.signature + : signature // ignore: cast_nullable_to_non_nullable + as FixedEncodedString86, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$SuperIdentityImpl extends _SuperIdentity { + const _$SuperIdentityImpl( + {required this.recordKey, + required this.publicKey, + required this.currentInstance, + required final List deprecatedInstances, + required final List> + deprecatedSuperRecordKeys, + required this.signature}) + : _deprecatedInstances = deprecatedInstances, + _deprecatedSuperRecordKeys = deprecatedSuperRecordKeys, + super._(); + + factory _$SuperIdentityImpl.fromJson(Map json) => + _$$SuperIdentityImplFromJson(json); + + /// Public DHT record storing this structure for account recovery + /// changing this can migrate/forward the SuperIdentity to a new DHT record + /// Instances should not hash this recordKey, rather the actual record + /// key used to store the superIdentity, as this may change. + @override + final Typed recordKey; + + /// Public key of the SuperIdentity used to sign identity keys for recovery + /// This must match the owner of the superRecord DHT record and can not be + /// changed without changing the record + @override + final FixedEncodedString43 publicKey; + + /// Current identity instance + /// The most recently generated identity instance for this SuperIdentity + @override + final IdentityInstance currentInstance; + + /// Deprecated identity instances + /// These may be compromised and should not be considered valid for + /// new signatures, but may be used to validate old signatures + final List _deprecatedInstances; + + /// Deprecated identity instances + /// These may be compromised and should not be considered valid for + /// new signatures, but may be used to validate old signatures + @override + List get deprecatedInstances { + if (_deprecatedInstances is EqualUnmodifiableListView) + return _deprecatedInstances; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_deprecatedInstances); + } + + /// Deprecated superRecords + /// These may be compromised and should not be considered valid for + /// new signatures, but may be used to validate old signatures + final List> _deprecatedSuperRecordKeys; + + /// Deprecated superRecords + /// These may be compromised and should not be considered valid for + /// new signatures, but may be used to validate old signatures + @override + List> get deprecatedSuperRecordKeys { + if (_deprecatedSuperRecordKeys is EqualUnmodifiableListView) + return _deprecatedSuperRecordKeys; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_deprecatedSuperRecordKeys); + } + + /// Signature of recordKey, currentInstance signature, + /// signatures of deprecatedInstances, and deprecatedSuperRecordKeys + /// by publicKey + @override + final FixedEncodedString86 signature; + + @override + String toString() { + return 'SuperIdentity(recordKey: $recordKey, publicKey: $publicKey, currentInstance: $currentInstance, deprecatedInstances: $deprecatedInstances, deprecatedSuperRecordKeys: $deprecatedSuperRecordKeys, signature: $signature)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SuperIdentityImpl && + (identical(other.recordKey, recordKey) || + other.recordKey == recordKey) && + (identical(other.publicKey, publicKey) || + other.publicKey == publicKey) && + (identical(other.currentInstance, currentInstance) || + other.currentInstance == currentInstance) && + const DeepCollectionEquality() + .equals(other._deprecatedInstances, _deprecatedInstances) && + const DeepCollectionEquality().equals( + other._deprecatedSuperRecordKeys, _deprecatedSuperRecordKeys) && + (identical(other.signature, signature) || + other.signature == signature)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, + recordKey, + publicKey, + currentInstance, + const DeepCollectionEquality().hash(_deprecatedInstances), + const DeepCollectionEquality().hash(_deprecatedSuperRecordKeys), + signature); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$SuperIdentityImplCopyWith<_$SuperIdentityImpl> get copyWith => + __$$SuperIdentityImplCopyWithImpl<_$SuperIdentityImpl>(this, _$identity); + + @override + Map toJson() { + return _$$SuperIdentityImplToJson( + this, + ); + } +} + +abstract class _SuperIdentity extends SuperIdentity { + const factory _SuperIdentity( + {required final Typed recordKey, + required final FixedEncodedString43 publicKey, + required final IdentityInstance currentInstance, + required final List deprecatedInstances, + required final List> + deprecatedSuperRecordKeys, + required final FixedEncodedString86 signature}) = _$SuperIdentityImpl; + const _SuperIdentity._() : super._(); + + factory _SuperIdentity.fromJson(Map json) = + _$SuperIdentityImpl.fromJson; + + @override + + /// Public DHT record storing this structure for account recovery + /// changing this can migrate/forward the SuperIdentity to a new DHT record + /// Instances should not hash this recordKey, rather the actual record + /// key used to store the superIdentity, as this may change. + Typed get recordKey; + @override + + /// Public key of the SuperIdentity used to sign identity keys for recovery + /// This must match the owner of the superRecord DHT record and can not be + /// changed without changing the record + FixedEncodedString43 get publicKey; + @override + + /// Current identity instance + /// The most recently generated identity instance for this SuperIdentity + IdentityInstance get currentInstance; + @override + + /// Deprecated identity instances + /// These may be compromised and should not be considered valid for + /// new signatures, but may be used to validate old signatures + List get deprecatedInstances; + @override + + /// Deprecated superRecords + /// These may be compromised and should not be considered valid for + /// new signatures, but may be used to validate old signatures + List> get deprecatedSuperRecordKeys; + @override + + /// Signature of recordKey, currentInstance signature, + /// signatures of deprecatedInstances, and deprecatedSuperRecordKeys + /// by publicKey + FixedEncodedString86 get signature; + @override + @JsonKey(ignore: true) + _$$SuperIdentityImplCopyWith<_$SuperIdentityImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/packages/veilid_support/lib/identity_support/super_identity.g.dart b/packages/veilid_support/lib/identity_support/super_identity.g.dart new file mode 100644 index 0000000..4c4f4f3 --- /dev/null +++ b/packages/veilid_support/lib/identity_support/super_identity.g.dart @@ -0,0 +1,34 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'super_identity.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$SuperIdentityImpl _$$SuperIdentityImplFromJson(Map json) => + _$SuperIdentityImpl( + recordKey: Typed.fromJson(json['record_key']), + publicKey: FixedEncodedString43.fromJson(json['public_key']), + currentInstance: IdentityInstance.fromJson(json['current_instance']), + deprecatedInstances: (json['deprecated_instances'] as List) + .map(IdentityInstance.fromJson) + .toList(), + deprecatedSuperRecordKeys: + (json['deprecated_super_record_keys'] as List) + .map(Typed.fromJson) + .toList(), + signature: FixedEncodedString86.fromJson(json['signature']), + ); + +Map _$$SuperIdentityImplToJson(_$SuperIdentityImpl instance) => + { + 'record_key': instance.recordKey.toJson(), + 'public_key': instance.publicKey.toJson(), + 'current_instance': instance.currentInstance.toJson(), + 'deprecated_instances': + instance.deprecatedInstances.map((e) => e.toJson()).toList(), + 'deprecated_super_record_keys': + instance.deprecatedSuperRecordKeys.map((e) => e.toJson()).toList(), + 'signature': instance.signature.toJson(), + }; diff --git a/packages/veilid_support/lib/identity_support/writable_super_identity.dart b/packages/veilid_support/lib/identity_support/writable_super_identity.dart new file mode 100644 index 0000000..3d88742 --- /dev/null +++ b/packages/veilid_support/lib/identity_support/writable_super_identity.dart @@ -0,0 +1,158 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; + +import '../src/veilid_log.dart'; +import '../veilid_support.dart'; + +Uint8List identityCryptoDomain = utf8.encode('identity'); + +/// SuperIdentity creator with secret +/// Not freezed because we never persist this class in its entirety. +class WritableSuperIdentity { + WritableSuperIdentity._({ + required this.superIdentity, + required this.superSecret, + required this.identitySecret, + }); + + static Future create() async { + final pool = DHTRecordPool.instance; + + // SuperIdentity DHT record is public/unencrypted + veilidLoggy.debug('Creating super identity record'); + return (await pool.createRecord( + debugName: 'WritableSuperIdentity::create::SuperIdentityRecord', + crypto: const VeilidCryptoPublic())) + .deleteScope((superRec) async { + final superRecordKey = superRec.key; + final superPublicKey = superRec.ownerKeyPair!.key; + final superSecret = superRec.ownerKeyPair!.secret; + + return _createIdentityInstance( + superRecordKey: superRecordKey, + superPublicKey: superPublicKey, + superSecret: superSecret, + closure: (identityInstance, identitySecret) async { + final signature = await _createSuperIdentitySignature( + recordKey: superRecordKey, + publicKey: superPublicKey, + secretKey: superSecret, + currentInstanceSignature: identityInstance.signature, + deprecatedInstancesSignatures: [], + deprecatedSuperRecordKeys: [], + ); + + final superIdentity = SuperIdentity( + recordKey: superRecordKey, + publicKey: superPublicKey, + currentInstance: identityInstance, + deprecatedInstances: [], + deprecatedSuperRecordKeys: [], + signature: signature); + + // Write superidentity to dht record + await superRec.eventualWriteJson(superIdentity); + + return WritableSuperIdentity._( + superIdentity: superIdentity, + superSecret: superSecret, + identitySecret: identitySecret); + }); + }); + } + + //////////////////////////////////////////////////////////////////////////// + // Public Interface + + /// Delete a super identity with secrets + Future delete() async => superIdentity.delete(); + + /// xxx: migration support, new identities, reveal identity secret etc + + //////////////////////////////////////////////////////////////////////////// + /// Private Implementation + + static Future _createSuperIdentitySignature({ + required TypedKey recordKey, + required Signature currentInstanceSignature, + required List deprecatedInstancesSignatures, + required List deprecatedSuperRecordKeys, + required PublicKey publicKey, + required SecretKey secretKey, + }) async { + final cs = await Veilid.instance.getCryptoSystem(recordKey.kind); + final sigBytes = SuperIdentity.signatureBytes( + recordKey: recordKey, + currentInstanceSignature: currentInstanceSignature, + deprecatedInstancesSignatures: deprecatedInstancesSignatures, + deprecatedSuperRecordKeys: deprecatedSuperRecordKeys); + return cs.sign(publicKey, secretKey, sigBytes); + } + + static Future _createIdentityInstance({ + required TypedKey superRecordKey, + required PublicKey superPublicKey, + required SecretKey superSecret, + required Future Function(IdentityInstance, SecretKey) closure, + }) async { + final pool = DHTRecordPool.instance; + veilidLoggy.debug('Creating identity instance record'); + // Identity record is private + return (await pool.createRecord( + debugName: 'SuperIdentityWithSecrets::create::IdentityRecord', + parent: superRecordKey)) + .deleteScope((identityRec) async { + final identityRecordKey = identityRec.key; + assert(superRecordKey.kind == identityRecordKey.kind, + 'new super and identity should have same cryptosystem'); + final identityPublicKey = identityRec.ownerKeyPair!.key; + final identitySecretKey = identityRec.ownerKeyPair!.secret; + + // Make encrypted secret key + final cs = await Veilid.instance.getCryptoSystem(identityRecordKey.kind); + + final encryptionKey = await cs.generateSharedSecret( + identityPublicKey, superSecret, identityCryptoDomain); + final encryptedSecretKey = await cs.encryptNoAuthWithNonce( + identitySecretKey.decode(), encryptionKey); + + // Make supersignature + final superSigBuf = BytesBuilder() + ..add(superRecordKey.decode()) + ..add(superPublicKey.decode()); + + final superSignature = await cs.signWithKeyPair( + identityRec.ownerKeyPair!, superSigBuf.toBytes()); + + // Make signature + final signature = await IdentityInstance.createIdentitySignature( + recordKey: identityRecordKey, + publicKey: identityPublicKey, + encryptedSecretKey: encryptedSecretKey, + superSignature: superSignature, + superPublicKey: superPublicKey, + superSecret: superSecret); + + // Make empty identity + const identity = Identity(accountRecords: IMapConst({})); + + // Write empty identity to identity dht key + await identityRec.eventualWriteJson(identity); + + final identityInstance = IdentityInstance( + recordKey: identityRecordKey, + publicKey: identityPublicKey, + encryptedSecretKey: encryptedSecretKey, + superSignature: superSignature, + signature: signature); + + return closure(identityInstance, identitySecretKey); + }); + } + + SuperIdentity superIdentity; + SecretKey superSecret; + SecretKey identitySecret; +} diff --git a/packages/veilid_support/lib/src/identity.dart b/packages/veilid_support/lib/src/identity.dart deleted file mode 100644 index 4666487..0000000 --- a/packages/veilid_support/lib/src/identity.dart +++ /dev/null @@ -1,333 +0,0 @@ -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 '../veilid_support.dart'; -import 'veilid_log.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 -class AccountRecordInfo with _$AccountRecordInfo { - const factory AccountRecordInfo({ - // Top level account keys and secrets - required OwnedDHTRecordPointer accountRecord, - }) = _AccountRecordInfo; - - factory AccountRecordInfo.fromJson(dynamic json) => - _$AccountRecordInfoFromJson(json as Map); -} - -// Identity Key points to accounts associated with this identity -// accounts field has a map of bundle id or uuid to account key pairs -// DHT Schema: DFLT(1) -// DHT Key (Private): identityRecordKey -// DHT Owner Key: identityPublicKey -// DHT Secret: identitySecretKey (stored encrypted -// with unlock code in local table store) -@freezed -class Identity with _$Identity { - const factory Identity({ - // Top level account keys and secrets - required IMap> accountRecords, - }) = _Identity; - - factory Identity.fromJson(dynamic json) => - _$IdentityFromJson(json as Map); -} - -// Identity Master key structure for created account -// Master key allows for regeneration of identity DHT record -// Bidirectional Master<->Identity signature allows for -// chain of identity ownership for account recovery process -// -// Backed by a DHT key at masterRecordKey, the secret is kept -// completely offline and only written to upon account recovery -// -// DHT Schema: DFLT(1) -// DHT Record Key (Public): masterRecordKey -// DHT Owner Key: masterPublicKey -// DHT Owner Secret: masterSecretKey (kept offline) -// Encryption: None -@freezed -class IdentityMaster with _$IdentityMaster { - const factory IdentityMaster( - { - // Private DHT record storing identity account mapping - required TypedKey identityRecordKey, - // Public key of identity - required PublicKey identityPublicKey, - // Public DHT record storing this structure for account recovery - required TypedKey masterRecordKey, - // Public key of master identity used to sign identity keys for recovery - required PublicKey masterPublicKey, - // Signature of identityRecordKey and identityPublicKey by masterPublicKey - required Signature identitySignature, - // Signature of masterRecordKey and masterPublicKey by identityPublicKey - required Signature masterSignature}) = _IdentityMaster; - - factory IdentityMaster.fromJson(dynamic json) => - _$IdentityMasterFromJson(json as Map); -} - -extension IdentityMasterExtension on IdentityMaster { - /// Deletes a master identity and the identity record under it - Future delete() async { - final pool = DHTRecordPool.instance; - await pool.deleteRecord(masterRecordKey); - } - - 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); - - KeyPair masterWriter(SecretKey secret) => - KeyPair(key: masterPublicKey, secret: secret); - - TypedKey identityPublicTypedKey() => - TypedKey(kind: identityRecordKey.kind, value: identityPublicKey); - - TypedKey masterPublicTypedKey() => - TypedKey(kind: identityRecordKey.kind, value: masterPublicKey); - - 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 SecretKey identitySecret, required String accountKey}) async { - // Read the identity key to get the account keys - final pool = DHTRecordPool.instance; - - final identityRecordCrypto = - await DHTRecordPool.privateCryptoFromTypedSecret( - TypedKey(kind: identityRecordKey.kind, value: identitySecret), - ); - - late final List accountRecordInfo; - await (await pool.openRecordRead(identityRecordKey, - debugName: - 'IdentityMaster::readAccountsFromIdentity::IdentityRecord', - 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 IdentityException.readError; - } - final accountRecords = IMapOfSets.from(identity.accountRecords); - final vcAccounts = accountRecords.get(accountKey); - - accountRecordInfo = vcAccounts.toList(); - }); - - return accountRecordInfo; - } - - /// Creates a new Account associated with master identity and store it in the - /// identity key. - Future addAccountToIdentity({ - required SecretKey identitySecret, - required String accountKey, - required Future Function(TypedKey parent) createAccountCallback, - int maxAccounts = 1, - }) async { - final pool = DHTRecordPool.instance; - - /////// Add account with profile to DHT - - // Open identity key for writing - veilidLoggy.debug('Opening identity record'); - return (await pool.openRecordWrite( - identityRecordKey, identityWriter(identitySecret), - debugName: 'IdentityMaster::addAccountToIdentity::IdentityRecord', - parent: masterRecordKey)) - .scope((identityRec) async { - // Create new account to insert into identity - veilidLoggy.debug('Creating new account'); - return (await pool.createRecord( - debugName: 'IdentityMaster::addAccountToIdentity::AccountRecord', - 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!)); - - 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); - }); - - return newAccountRecordInfo; - }); - }); - } -} - -// Identity Master with secrets -// Not freezed because we never persist this class in its entirety -class IdentityMasterWithSecrets { - IdentityMasterWithSecrets._( - {required this.identityMaster, - required this.masterSecret, - required this.identitySecret}); - IdentityMaster identityMaster; - SecretKey masterSecret; - SecretKey identitySecret; - - /// Delete a master identity with secrets - Future delete() async => identityMaster.delete(); - - /// Creates a new master identity and returns it with its secrets - static Future create() async { - final pool = DHTRecordPool.instance; - - // IdentityMaster DHT record is public/unencrypted - veilidLoggy.debug('Creating master identity record'); - return (await pool.createRecord( - debugName: - 'IdentityMasterWithSecrets::create::IdentityMasterRecord', - crypto: const VeilidCryptoPublic())) - .deleteScope((masterRec) async { - veilidLoggy.debug('Creating identity record'); - // Identity record is private - return (await pool.createRecord( - debugName: 'IdentityMasterWithSecrets::create::IdentityRecord', - 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()); - - 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 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); - - // Make empty identity - const identity = Identity(accountRecords: IMapConst({})); - - // Write empty identity to identity dht key - await identityRec.eventualWriteJson(identity); - - return IdentityMasterWithSecrets._( - identityMaster: identityMaster, - masterSecret: masterOwner.secret, - identitySecret: identityOwner.secret); - }); - }); - } -} - -/// Opens an existing master identity and validates it -Future openIdentityMaster( - {required TypedKey identityMasterRecordKey}) async { - final pool = DHTRecordPool.instance; - - // IdentityMaster DHT record is public/unencrypted - return (await pool.openRecordRead(identityMasterRecordKey, - debugName: - 'IdentityMaster::openIdentityMaster::IdentityMasterRecord')) - .deleteScope((masterRec) async { - final identityMaster = (await masterRec.getJson(IdentityMaster.fromJson, - refreshMode: DHTRecordRefreshMode.network))!; - - // Validate IdentityMaster - final masterRecordKey = masterRec.key; - final masterOwnerKey = masterRec.owner; - final masterSigBuf = BytesBuilder() - ..add(masterRecordKey.decode()) - ..add(masterOwnerKey.decode()); - final masterSignature = identityMaster.masterSignature; - - final identityRecordKey = identityMaster.identityRecordKey; - final identityOwnerKey = identityMaster.identityPublicKey; - final identitySigBuf = BytesBuilder() - ..add(identityRecordKey.decode()) - ..add(identityOwnerKey.decode()); - final identitySignature = identityMaster.identitySignature; - - assert(masterRecordKey.kind == identityRecordKey.kind, - 'new master and identity should have same cryptosystem'); - final crypto = await pool.veilid.getCryptoSystem(masterRecordKey.kind); - - await crypto.verify( - masterOwnerKey, identitySigBuf.toBytes(), identitySignature); - await crypto.verify( - identityOwnerKey, masterSigBuf.toBytes(), masterSignature); - - return identityMaster; - }); -} diff --git a/packages/veilid_support/lib/src/identity.freezed.dart b/packages/veilid_support/lib/src/identity.freezed.dart deleted file mode 100644 index 27f34ea..0000000 --- a/packages/veilid_support/lib/src/identity.freezed.dart +++ /dev/null @@ -1,579 +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 'identity.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'); - -AccountRecordInfo _$AccountRecordInfoFromJson(Map json) { - return _AccountRecordInfo.fromJson(json); -} - -/// @nodoc -mixin _$AccountRecordInfo { -// Top level account keys and secrets - OwnedDHTRecordPointer get accountRecord => throw _privateConstructorUsedError; - - Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) - $AccountRecordInfoCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $AccountRecordInfoCopyWith<$Res> { - factory $AccountRecordInfoCopyWith( - AccountRecordInfo value, $Res Function(AccountRecordInfo) then) = - _$AccountRecordInfoCopyWithImpl<$Res, AccountRecordInfo>; - @useResult - $Res call({OwnedDHTRecordPointer accountRecord}); - - $OwnedDHTRecordPointerCopyWith<$Res> get accountRecord; -} - -/// @nodoc -class _$AccountRecordInfoCopyWithImpl<$Res, $Val extends AccountRecordInfo> - implements $AccountRecordInfoCopyWith<$Res> { - _$AccountRecordInfoCopyWithImpl(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? accountRecord = null, - }) { - return _then(_value.copyWith( - accountRecord: null == accountRecord - ? _value.accountRecord - : accountRecord // ignore: cast_nullable_to_non_nullable - as OwnedDHTRecordPointer, - ) as $Val); - } - - @override - @pragma('vm:prefer-inline') - $OwnedDHTRecordPointerCopyWith<$Res> get accountRecord { - return $OwnedDHTRecordPointerCopyWith<$Res>(_value.accountRecord, (value) { - return _then(_value.copyWith(accountRecord: value) as $Val); - }); - } -} - -/// @nodoc -abstract class _$$AccountRecordInfoImplCopyWith<$Res> - implements $AccountRecordInfoCopyWith<$Res> { - factory _$$AccountRecordInfoImplCopyWith(_$AccountRecordInfoImpl value, - $Res Function(_$AccountRecordInfoImpl) then) = - __$$AccountRecordInfoImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({OwnedDHTRecordPointer accountRecord}); - - @override - $OwnedDHTRecordPointerCopyWith<$Res> get accountRecord; -} - -/// @nodoc -class __$$AccountRecordInfoImplCopyWithImpl<$Res> - extends _$AccountRecordInfoCopyWithImpl<$Res, _$AccountRecordInfoImpl> - implements _$$AccountRecordInfoImplCopyWith<$Res> { - __$$AccountRecordInfoImplCopyWithImpl(_$AccountRecordInfoImpl _value, - $Res Function(_$AccountRecordInfoImpl) _then) - : super(_value, _then); - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? accountRecord = null, - }) { - return _then(_$AccountRecordInfoImpl( - accountRecord: null == accountRecord - ? _value.accountRecord - : accountRecord // ignore: cast_nullable_to_non_nullable - as OwnedDHTRecordPointer, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$AccountRecordInfoImpl implements _AccountRecordInfo { - const _$AccountRecordInfoImpl({required this.accountRecord}); - - factory _$AccountRecordInfoImpl.fromJson(Map json) => - _$$AccountRecordInfoImplFromJson(json); - -// Top level account keys and secrets - @override - final OwnedDHTRecordPointer accountRecord; - - @override - String toString() { - return 'AccountRecordInfo(accountRecord: $accountRecord)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$AccountRecordInfoImpl && - (identical(other.accountRecord, accountRecord) || - other.accountRecord == accountRecord)); - } - - @JsonKey(ignore: true) - @override - int get hashCode => Object.hash(runtimeType, accountRecord); - - @JsonKey(ignore: true) - @override - @pragma('vm:prefer-inline') - _$$AccountRecordInfoImplCopyWith<_$AccountRecordInfoImpl> get copyWith => - __$$AccountRecordInfoImplCopyWithImpl<_$AccountRecordInfoImpl>( - this, _$identity); - - @override - Map toJson() { - return _$$AccountRecordInfoImplToJson( - this, - ); - } -} - -abstract class _AccountRecordInfo implements AccountRecordInfo { - const factory _AccountRecordInfo( - {required final OwnedDHTRecordPointer accountRecord}) = - _$AccountRecordInfoImpl; - - factory _AccountRecordInfo.fromJson(Map json) = - _$AccountRecordInfoImpl.fromJson; - - @override // Top level account keys and secrets - OwnedDHTRecordPointer get accountRecord; - @override - @JsonKey(ignore: true) - _$$AccountRecordInfoImplCopyWith<_$AccountRecordInfoImpl> get copyWith => - throw _privateConstructorUsedError; -} - -Identity _$IdentityFromJson(Map json) { - return _Identity.fromJson(json); -} - -/// @nodoc -mixin _$Identity { -// Top level account keys and secrets - IMap> get accountRecords => - throw _privateConstructorUsedError; - - Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) - $IdentityCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $IdentityCopyWith<$Res> { - factory $IdentityCopyWith(Identity value, $Res Function(Identity) then) = - _$IdentityCopyWithImpl<$Res, Identity>; - @useResult - $Res call({IMap> accountRecords}); -} - -/// @nodoc -class _$IdentityCopyWithImpl<$Res, $Val extends Identity> - implements $IdentityCopyWith<$Res> { - _$IdentityCopyWithImpl(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? accountRecords = null, - }) { - return _then(_value.copyWith( - accountRecords: null == accountRecords - ? _value.accountRecords - : accountRecords // ignore: cast_nullable_to_non_nullable - as IMap>, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$IdentityImplCopyWith<$Res> - implements $IdentityCopyWith<$Res> { - factory _$$IdentityImplCopyWith( - _$IdentityImpl value, $Res Function(_$IdentityImpl) then) = - __$$IdentityImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({IMap> accountRecords}); -} - -/// @nodoc -class __$$IdentityImplCopyWithImpl<$Res> - extends _$IdentityCopyWithImpl<$Res, _$IdentityImpl> - implements _$$IdentityImplCopyWith<$Res> { - __$$IdentityImplCopyWithImpl( - _$IdentityImpl _value, $Res Function(_$IdentityImpl) _then) - : super(_value, _then); - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? accountRecords = null, - }) { - return _then(_$IdentityImpl( - accountRecords: null == accountRecords - ? _value.accountRecords - : accountRecords // ignore: cast_nullable_to_non_nullable - as IMap>, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$IdentityImpl implements _Identity { - const _$IdentityImpl({required this.accountRecords}); - - factory _$IdentityImpl.fromJson(Map json) => - _$$IdentityImplFromJson(json); - -// Top level account keys and secrets - @override - final IMap> accountRecords; - - @override - String toString() { - return 'Identity(accountRecords: $accountRecords)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$IdentityImpl && - (identical(other.accountRecords, accountRecords) || - other.accountRecords == accountRecords)); - } - - @JsonKey(ignore: true) - @override - int get hashCode => Object.hash(runtimeType, accountRecords); - - @JsonKey(ignore: true) - @override - @pragma('vm:prefer-inline') - _$$IdentityImplCopyWith<_$IdentityImpl> get copyWith => - __$$IdentityImplCopyWithImpl<_$IdentityImpl>(this, _$identity); - - @override - Map toJson() { - return _$$IdentityImplToJson( - this, - ); - } -} - -abstract class _Identity implements Identity { - const factory _Identity( - {required final IMap> - accountRecords}) = _$IdentityImpl; - - factory _Identity.fromJson(Map json) = - _$IdentityImpl.fromJson; - - @override // Top level account keys and secrets - IMap> get accountRecords; - @override - @JsonKey(ignore: true) - _$$IdentityImplCopyWith<_$IdentityImpl> get copyWith => - throw _privateConstructorUsedError; -} - -IdentityMaster _$IdentityMasterFromJson(Map json) { - return _IdentityMaster.fromJson(json); -} - -/// @nodoc -mixin _$IdentityMaster { -// Private DHT record storing identity account mapping - Typed get identityRecordKey => - throw _privateConstructorUsedError; // Public key of identity - FixedEncodedString43 get identityPublicKey => - throw _privateConstructorUsedError; // Public DHT record storing this structure for account recovery - Typed get masterRecordKey => - throw _privateConstructorUsedError; // Public key of master identity used to sign identity keys for recovery - FixedEncodedString43 get masterPublicKey => - throw _privateConstructorUsedError; // Signature of identityRecordKey and identityPublicKey by masterPublicKey - FixedEncodedString86 get identitySignature => - throw _privateConstructorUsedError; // Signature of masterRecordKey and masterPublicKey by identityPublicKey - FixedEncodedString86 get masterSignature => - throw _privateConstructorUsedError; - - Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) - $IdentityMasterCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $IdentityMasterCopyWith<$Res> { - factory $IdentityMasterCopyWith( - IdentityMaster value, $Res Function(IdentityMaster) then) = - _$IdentityMasterCopyWithImpl<$Res, IdentityMaster>; - @useResult - $Res call( - {Typed identityRecordKey, - FixedEncodedString43 identityPublicKey, - Typed masterRecordKey, - FixedEncodedString43 masterPublicKey, - FixedEncodedString86 identitySignature, - FixedEncodedString86 masterSignature}); -} - -/// @nodoc -class _$IdentityMasterCopyWithImpl<$Res, $Val extends IdentityMaster> - implements $IdentityMasterCopyWith<$Res> { - _$IdentityMasterCopyWithImpl(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? identityRecordKey = null, - Object? identityPublicKey = null, - Object? masterRecordKey = null, - Object? masterPublicKey = null, - Object? identitySignature = null, - Object? masterSignature = null, - }) { - return _then(_value.copyWith( - identityRecordKey: null == identityRecordKey - ? _value.identityRecordKey - : identityRecordKey // ignore: cast_nullable_to_non_nullable - as Typed, - identityPublicKey: null == identityPublicKey - ? _value.identityPublicKey - : identityPublicKey // ignore: cast_nullable_to_non_nullable - as FixedEncodedString43, - masterRecordKey: null == masterRecordKey - ? _value.masterRecordKey - : masterRecordKey // ignore: cast_nullable_to_non_nullable - as Typed, - masterPublicKey: null == masterPublicKey - ? _value.masterPublicKey - : masterPublicKey // ignore: cast_nullable_to_non_nullable - as FixedEncodedString43, - identitySignature: null == identitySignature - ? _value.identitySignature - : identitySignature // ignore: cast_nullable_to_non_nullable - as FixedEncodedString86, - masterSignature: null == masterSignature - ? _value.masterSignature - : masterSignature // ignore: cast_nullable_to_non_nullable - as FixedEncodedString86, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$IdentityMasterImplCopyWith<$Res> - implements $IdentityMasterCopyWith<$Res> { - factory _$$IdentityMasterImplCopyWith(_$IdentityMasterImpl value, - $Res Function(_$IdentityMasterImpl) then) = - __$$IdentityMasterImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {Typed identityRecordKey, - FixedEncodedString43 identityPublicKey, - Typed masterRecordKey, - FixedEncodedString43 masterPublicKey, - FixedEncodedString86 identitySignature, - FixedEncodedString86 masterSignature}); -} - -/// @nodoc -class __$$IdentityMasterImplCopyWithImpl<$Res> - extends _$IdentityMasterCopyWithImpl<$Res, _$IdentityMasterImpl> - implements _$$IdentityMasterImplCopyWith<$Res> { - __$$IdentityMasterImplCopyWithImpl( - _$IdentityMasterImpl _value, $Res Function(_$IdentityMasterImpl) _then) - : super(_value, _then); - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? identityRecordKey = null, - Object? identityPublicKey = null, - Object? masterRecordKey = null, - Object? masterPublicKey = null, - Object? identitySignature = null, - Object? masterSignature = null, - }) { - return _then(_$IdentityMasterImpl( - identityRecordKey: null == identityRecordKey - ? _value.identityRecordKey - : identityRecordKey // ignore: cast_nullable_to_non_nullable - as Typed, - identityPublicKey: null == identityPublicKey - ? _value.identityPublicKey - : identityPublicKey // ignore: cast_nullable_to_non_nullable - as FixedEncodedString43, - masterRecordKey: null == masterRecordKey - ? _value.masterRecordKey - : masterRecordKey // ignore: cast_nullable_to_non_nullable - as Typed, - masterPublicKey: null == masterPublicKey - ? _value.masterPublicKey - : masterPublicKey // ignore: cast_nullable_to_non_nullable - as FixedEncodedString43, - identitySignature: null == identitySignature - ? _value.identitySignature - : identitySignature // ignore: cast_nullable_to_non_nullable - as FixedEncodedString86, - masterSignature: null == masterSignature - ? _value.masterSignature - : masterSignature // ignore: cast_nullable_to_non_nullable - as FixedEncodedString86, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$IdentityMasterImpl implements _IdentityMaster { - const _$IdentityMasterImpl( - {required this.identityRecordKey, - required this.identityPublicKey, - required this.masterRecordKey, - required this.masterPublicKey, - required this.identitySignature, - required this.masterSignature}); - - factory _$IdentityMasterImpl.fromJson(Map json) => - _$$IdentityMasterImplFromJson(json); - -// Private DHT record storing identity account mapping - @override - final Typed identityRecordKey; -// Public key of identity - @override - final FixedEncodedString43 identityPublicKey; -// Public DHT record storing this structure for account recovery - @override - final Typed masterRecordKey; -// Public key of master identity used to sign identity keys for recovery - @override - final FixedEncodedString43 masterPublicKey; -// Signature of identityRecordKey and identityPublicKey by masterPublicKey - @override - final FixedEncodedString86 identitySignature; -// Signature of masterRecordKey and masterPublicKey by identityPublicKey - @override - final FixedEncodedString86 masterSignature; - - @override - String toString() { - return 'IdentityMaster(identityRecordKey: $identityRecordKey, identityPublicKey: $identityPublicKey, masterRecordKey: $masterRecordKey, masterPublicKey: $masterPublicKey, identitySignature: $identitySignature, masterSignature: $masterSignature)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$IdentityMasterImpl && - (identical(other.identityRecordKey, identityRecordKey) || - other.identityRecordKey == identityRecordKey) && - (identical(other.identityPublicKey, identityPublicKey) || - other.identityPublicKey == identityPublicKey) && - (identical(other.masterRecordKey, masterRecordKey) || - other.masterRecordKey == masterRecordKey) && - (identical(other.masterPublicKey, masterPublicKey) || - other.masterPublicKey == masterPublicKey) && - (identical(other.identitySignature, identitySignature) || - other.identitySignature == identitySignature) && - (identical(other.masterSignature, masterSignature) || - other.masterSignature == masterSignature)); - } - - @JsonKey(ignore: true) - @override - int get hashCode => Object.hash( - runtimeType, - identityRecordKey, - identityPublicKey, - masterRecordKey, - masterPublicKey, - identitySignature, - masterSignature); - - @JsonKey(ignore: true) - @override - @pragma('vm:prefer-inline') - _$$IdentityMasterImplCopyWith<_$IdentityMasterImpl> get copyWith => - __$$IdentityMasterImplCopyWithImpl<_$IdentityMasterImpl>( - this, _$identity); - - @override - Map toJson() { - return _$$IdentityMasterImplToJson( - this, - ); - } -} - -abstract class _IdentityMaster implements IdentityMaster { - const factory _IdentityMaster( - {required final Typed identityRecordKey, - required final FixedEncodedString43 identityPublicKey, - required final Typed masterRecordKey, - required final FixedEncodedString43 masterPublicKey, - required final FixedEncodedString86 identitySignature, - required final FixedEncodedString86 masterSignature}) = - _$IdentityMasterImpl; - - factory _IdentityMaster.fromJson(Map json) = - _$IdentityMasterImpl.fromJson; - - @override // Private DHT record storing identity account mapping - Typed get identityRecordKey; - @override // Public key of identity - FixedEncodedString43 get identityPublicKey; - @override // Public DHT record storing this structure for account recovery - Typed get masterRecordKey; - @override // Public key of master identity used to sign identity keys for recovery - FixedEncodedString43 get masterPublicKey; - @override // Signature of identityRecordKey and identityPublicKey by masterPublicKey - FixedEncodedString86 get identitySignature; - @override // Signature of masterRecordKey and masterPublicKey by identityPublicKey - FixedEncodedString86 get masterSignature; - @override - @JsonKey(ignore: true) - _$$IdentityMasterImplCopyWith<_$IdentityMasterImpl> get copyWith => - throw _privateConstructorUsedError; -} diff --git a/packages/veilid_support/lib/src/identity.g.dart b/packages/veilid_support/lib/src/identity.g.dart deleted file mode 100644 index 616477a..0000000 --- a/packages/veilid_support/lib/src/identity.g.dart +++ /dev/null @@ -1,63 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'identity.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -_$AccountRecordInfoImpl _$$AccountRecordInfoImplFromJson( - Map json) => - _$AccountRecordInfoImpl( - accountRecord: OwnedDHTRecordPointer.fromJson(json['account_record']), - ); - -Map _$$AccountRecordInfoImplToJson( - _$AccountRecordInfoImpl instance) => - { - 'account_record': instance.accountRecord.toJson(), - }; - -_$IdentityImpl _$$IdentityImplFromJson(Map json) => - _$IdentityImpl( - accountRecords: IMap>.fromJson( - json['account_records'] as Map, - (value) => value as String, - (value) => ISet.fromJson( - value, (value) => AccountRecordInfo.fromJson(value))), - ); - -Map _$$IdentityImplToJson(_$IdentityImpl instance) => - { - 'account_records': instance.accountRecords.toJson( - (value) => value, - (value) => value.toJson( - (value) => value.toJson(), - ), - ), - }; - -_$IdentityMasterImpl _$$IdentityMasterImplFromJson(Map json) => - _$IdentityMasterImpl( - identityRecordKey: - Typed.fromJson(json['identity_record_key']), - identityPublicKey: - FixedEncodedString43.fromJson(json['identity_public_key']), - masterRecordKey: - Typed.fromJson(json['master_record_key']), - masterPublicKey: FixedEncodedString43.fromJson(json['master_public_key']), - identitySignature: - FixedEncodedString86.fromJson(json['identity_signature']), - masterSignature: FixedEncodedString86.fromJson(json['master_signature']), - ); - -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(), - }; diff --git a/packages/veilid_support/lib/veilid_support.dart b/packages/veilid_support/lib/veilid_support.dart index 6d10049..f48376f 100644 --- a/packages/veilid_support/lib/veilid_support.dart +++ b/packages/veilid_support/lib/veilid_support.dart @@ -6,8 +6,8 @@ library veilid_support; export 'package:veilid/veilid.dart'; export 'dht_support/dht_support.dart'; +export 'identity_support/identity_support.dart'; export 'src/config.dart'; -export 'src/identity.dart'; export 'src/json_tools.dart'; export 'src/memory_tools.dart'; export 'src/online_element_state.dart'; From 68aad4a94e88bc776fb252f11aed375b3d0936a1 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 7 Jun 2024 21:38:19 -0400 Subject: [PATCH 131/270] spine update --- .../test_dht_short_array.dart | 37 +++----- .../src/dht_log/dht_log_spine.dart | 90 ++++++++++++------- .../src/interfaces/exceptions.dart | 5 ++ 3 files changed, 75 insertions(+), 57 deletions(-) diff --git a/packages/veilid_support/example/integration_test/test_dht_short_array.dart b/packages/veilid_support/example/integration_test/test_dht_short_array.dart index 244b3d5..497b5fc 100644 --- a/packages/veilid_support/example/integration_test/test_dht_short_array.dart +++ b/packages/veilid_support/example/integration_test/test_dht_short_array.dart @@ -61,48 +61,39 @@ Future Function() makeTestDHTShortArrayAdd({required int stride}) => print('adding singles\n'); { - final res = await arr.operateWrite((w) async { - for (var n = 4; n < 8; n++) { + for (var n = 4; n < 8; n++) { + await arr.operateWriteEventual((w) async { print('$n '); - final success = await w.tryAdd(dataset[n]); - expect(success, isTrue); - } - }); - expect(res, isNull); + return w.tryAdd(dataset[n]); + }); + } } print('adding batch\n'); { - final res = await arr.operateWrite((w) async { + await arr.operateWriteEventual((w) async { print('${dataset.length ~/ 2}-${dataset.length}'); - final success = await w + return w .tryAddAll(dataset.sublist(dataset.length ~/ 2, dataset.length)); - expect(success, isTrue); }); - expect(res, isNull); } print('inserting singles\n'); { - final res = await arr.operateWrite((w) async { - for (var n = 0; n < 4; n++) { + for (var n = 0; n < 4; n++) { + await arr.operateWriteEventual((w) async { print('$n '); - final success = await w.tryInsert(n, dataset[n]); - expect(success, isTrue); - } - }); - expect(res, isNull); + return w.tryInsert(n, dataset[n]); + }); + } } print('inserting batch\n'); { - final res = await arr.operateWrite((w) async { + await arr.operateWriteEventual((w) async { print('8-${dataset.length ~/ 2}'); - final success = - await w.tryInsertAll(8, dataset.sublist(8, dataset.length ~/ 2)); - expect(success, isTrue); + return w.tryInsertAll(8, dataset.sublist(8, dataset.length ~/ 2)); }); - expect(res, isNull); } //print('get all\n'); diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index bad7f80..70155e8 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -234,7 +234,8 @@ class _DHTLogSpine { final existingData = await _spineRecord.tryWriteBytes(headBuffer); if (existingData != null) { // Head write failed, incorporate update - await _updateHead(proto.DHTLog.fromBuffer(existingData)); + final existingHead = proto.DHTLog.fromBuffer(existingData); + _updateHead(existingHead.head, existingHead.tail, old: old); if (old != null) { sendUpdate(old.$1, old.$2); } @@ -258,11 +259,22 @@ class _DHTLogSpine { } /// Validate a new spine head subkey that has come in from the network - Future _updateHead(proto.DHTLog spineHead) async { + void _updateHead(int newHead, int newTail, {(int, int)? old}) { assert(_spineMutex.isLocked, 'should be in mutex here'); - _head = spineHead.head; - _tail = spineHead.tail; + if (old != null) { + final oldHead = old.$1; + final oldTail = old.$2; + + final headDelta = _ringDistance(newHead, oldHead); + final tailDelta = _ringDistance(newTail, oldTail); + if (headDelta > _positionLimit ~/ 2 || tailDelta > _positionLimit ~/ 2) { + throw DHTExceptionInvalidData(); + } + } + + _head = newHead; + _tail = newTail; } ///////////////////////////////////////////////////////////////////////////// @@ -456,7 +468,8 @@ class _DHTLogSpine { // API for public interfaces Future<_DHTLogPosition?> lookupPositionBySegmentNumber( - int segmentNumber, int segmentPos) async => + int segmentNumber, int segmentPos, + {bool onlyOpened = false}) async => _spineCacheMutex.protect(() async { // Get the segment shortArray final openedSegment = _openedSegments[segmentNumber]; @@ -465,6 +478,10 @@ class _DHTLogSpine { openedSegment.openCount++; shortArray = openedSegment.shortArray; } else { + if (onlyOpened) { + return null; + } + final newShortArray = (_spineRecord.writer == null) ? await _openSegment(segmentNumber) : await _openOrCreateSegment(segmentNumber); @@ -665,39 +682,42 @@ class _DHTLogSpine { final headData = proto.DHTLog.fromBuffer(data); // Then update the head record - await _spineMutex.protect(() async { - final oldHead = _head; - final oldTail = _tail; - await _updateHead(headData); + _spineChangeProcessor.updateState(headData, (headData) async { + await _spineMutex.protect(() async { + final oldHead = _head; + final oldTail = _tail; - // Lookup tail position segments that have changed - // and force their short arrays to refresh their heads - final segmentsToRefresh = <_DHTLogPosition>[]; - int? lastSegmentNumber; - for (var curTail = oldTail; - curTail != _tail; - curTail = (curTail + 1) % _positionLimit) { - final segmentNumber = curTail ~/ DHTShortArray.maxElements; - final segmentPos = curTail % DHTShortArray.maxElements; - if (segmentNumber == lastSegmentNumber) { - continue; + _updateHead(headData.head, headData.tail, old: (oldHead, oldTail)); + + // Lookup tail position segments that have changed + // and force their short arrays to refresh their heads if + // they are opened + final segmentsToRefresh = <_DHTLogPosition>[]; + for (var curTail = oldTail; + curTail != _tail; + curTail = (curTail + + (DHTShortArray.maxElements - + (curTail % DHTShortArray.maxElements))) % + _positionLimit) { + final segmentNumber = curTail ~/ DHTShortArray.maxElements; + final segmentPos = curTail % DHTShortArray.maxElements; + final dhtLogPosition = await lookupPositionBySegmentNumber( + segmentNumber, segmentPos, + onlyOpened: true); + if (dhtLogPosition == null) { + continue; + } + segmentsToRefresh.add(dhtLogPosition); } - lastSegmentNumber = segmentNumber; - final dhtLogPosition = - await lookupPositionBySegmentNumber(segmentNumber, segmentPos); - if (dhtLogPosition == null) { - throw Exception('missing segment in dht log'); - } - segmentsToRefresh.add(dhtLogPosition); - } - // Refresh the segments that have probably changed - await segmentsToRefresh.map((p) async { - await p.shortArray.refresh(); - await p.close(); - }).wait; + // Refresh the segments that have probably changed + await segmentsToRefresh.map((p) async { + await p.shortArray.refresh(); + await p.close(); + }).wait; - sendUpdate(oldHead, oldTail); + sendUpdate(oldHead, oldTail); + }); }); } @@ -723,6 +743,8 @@ class _DHTLogSpine { StreamSubscription? _subscription; // Notify closure for external spine head changes void Function(DHTLogUpdate)? onUpdatedSpine; + // Single state processor for spine updates + final _spineChangeProcessor = SingleStateProcessor(); // Spine DHT record final DHTRecord _spineRecord; diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/exceptions.dart b/packages/veilid_support/lib/dht_support/src/interfaces/exceptions.dart index 2b95033..529c308 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/exceptions.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/exceptions.dart @@ -3,3 +3,8 @@ class DHTExceptionTryAgain implements Exception { [this.cause = 'operation failed due to newer dht value']); String cause; } + +class DHTExceptionInvalidData implements Exception { + DHTExceptionInvalidData([this.cause = 'dht data structure is corrupt']); + String cause; +} From 0b9835b23d9fd7eaa9bbf3bb7d3034046e16d23d Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 8 Jun 2024 12:59:56 -0400 Subject: [PATCH 132/270] simplify reference counting --- .../example/integration_test/app_test.dart | 208 +++++++++--------- .../lib/dht_support/src/dht_log/dht_log.dart | 11 +- .../src/dht_log/dht_log_spine.dart | 168 +++++--------- .../src/dht_record/dht_record.dart | 10 +- .../src/dht_short_array/dht_short_array.dart | 10 +- .../src/interfaces/dht_closeable.dart | 16 +- 6 files changed, 186 insertions(+), 237 deletions(-) diff --git a/packages/veilid_support/example/integration_test/app_test.dart b/packages/veilid_support/example/integration_test/app_test.dart index 6912fd3..a0f3b7f 100644 --- a/packages/veilid_support/example/integration_test/app_test.dart +++ b/packages/veilid_support/example/integration_test/app_test.dart @@ -36,116 +36,116 @@ void main() { setUpAll(veilidFixture.attach); tearDownAll(veilidFixture.detach); - group('TableDB Tests', () { - group('TableDBArray Tests', () { - // test('create/delete TableDBArray', testTableDBArrayCreateDelete); + // group('TableDB Tests', () { + // group('TableDBArray Tests', () { + // // test('create/delete TableDBArray', testTableDBArrayCreateDelete); - group('TableDBArray Add/Get Tests', () { - for (final params in [ - // - (99, 3, 15), - (100, 4, 16), - (101, 5, 17), - // - (511, 3, 127), - (512, 4, 128), - (513, 5, 129), - // - (4095, 3, 1023), - (4096, 4, 1024), - (4097, 5, 1025), - // - (65535, 3, 16383), - (65536, 4, 16384), - (65537, 5, 16385), - ]) { - final count = params.$1; - final singles = params.$2; - final batchSize = params.$3; + // group('TableDBArray Add/Get Tests', () { + // for (final params in [ + // // + // (99, 3, 15), + // (100, 4, 16), + // (101, 5, 17), + // // + // (511, 3, 127), + // (512, 4, 128), + // (513, 5, 129), + // // + // (4095, 3, 1023), + // (4096, 4, 1024), + // (4097, 5, 1025), + // // + // (65535, 3, 16383), + // (65536, 4, 16384), + // (65537, 5, 16385), + // ]) { + // final count = params.$1; + // final singles = params.$2; + // final batchSize = params.$3; - test( - timeout: const Timeout(Duration(seconds: 480)), - 'add/remove TableDBArray count = $count batchSize=$batchSize', - makeTestTableDBArrayAddGetClear( - count: count, - singles: singles, - batchSize: batchSize, - crypto: const VeilidCryptoPublic()), - ); - } - }); + // test( + // timeout: const Timeout(Duration(seconds: 480)), + // 'add/remove TableDBArray count = $count batchSize=$batchSize', + // makeTestTableDBArrayAddGetClear( + // count: count, + // singles: singles, + // batchSize: batchSize, + // crypto: const VeilidCryptoPublic()), + // ); + // } + // }); - group('TableDBArray Insert Tests', () { - for (final params in [ - // - (99, 3, 15), - (100, 4, 16), - (101, 5, 17), - // - (511, 3, 127), - (512, 4, 128), - (513, 5, 129), - // - (4095, 3, 1023), - (4096, 4, 1024), - (4097, 5, 1025), - // - (65535, 3, 16383), - (65536, 4, 16384), - (65537, 5, 16385), - ]) { - final count = params.$1; - final singles = params.$2; - final batchSize = params.$3; + // group('TableDBArray Insert Tests', () { + // for (final params in [ + // // + // (99, 3, 15), + // (100, 4, 16), + // (101, 5, 17), + // // + // (511, 3, 127), + // (512, 4, 128), + // (513, 5, 129), + // // + // (4095, 3, 1023), + // (4096, 4, 1024), + // (4097, 5, 1025), + // // + // (65535, 3, 16383), + // (65536, 4, 16384), + // (65537, 5, 16385), + // ]) { + // final count = params.$1; + // final singles = params.$2; + // final batchSize = params.$3; - test( - timeout: const Timeout(Duration(seconds: 480)), - 'insert TableDBArray count=$count singles=$singles batchSize=$batchSize', - makeTestTableDBArrayInsert( - count: count, - singles: singles, - batchSize: batchSize, - crypto: const VeilidCryptoPublic()), - ); - } - }); + // test( + // timeout: const Timeout(Duration(seconds: 480)), + // 'insert TableDBArray count=$count singles=$singles batchSize=$batchSize', + // makeTestTableDBArrayInsert( + // count: count, + // singles: singles, + // batchSize: batchSize, + // crypto: const VeilidCryptoPublic()), + // ); + // } + // }); - group('TableDBArray Remove Tests', () { - for (final params in [ - // - (99, 3, 15), - (100, 4, 16), - (101, 5, 17), - // - (511, 3, 127), - (512, 4, 128), - (513, 5, 129), - // - (4095, 3, 1023), - (4096, 4, 1024), - (4097, 5, 1025), - // - (16383, 3, 4095), - (16384, 4, 4096), - (16385, 5, 4097), - ]) { - final count = params.$1; - final singles = params.$2; - final batchSize = params.$3; + // group('TableDBArray Remove Tests', () { + // for (final params in [ + // // + // (99, 3, 15), + // (100, 4, 16), + // (101, 5, 17), + // // + // (511, 3, 127), + // (512, 4, 128), + // (513, 5, 129), + // // + // (4095, 3, 1023), + // (4096, 4, 1024), + // (4097, 5, 1025), + // // + // (16383, 3, 4095), + // (16384, 4, 4096), + // (16385, 5, 4097), + // ]) { + // final count = params.$1; + // final singles = params.$2; + // final batchSize = params.$3; - test( - timeout: const Timeout(Duration(seconds: 480)), - 'remove TableDBArray count=$count singles=$singles batchSize=$batchSize', - makeTestTableDBArrayRemove( - count: count, - singles: singles, - batchSize: batchSize, - crypto: const VeilidCryptoPublic()), - ); - } - }); - }); - }); + // test( + // timeout: const Timeout(Duration(seconds: 480)), + // 'remove TableDBArray count=$count singles=$singles batchSize=$batchSize', + // makeTestTableDBArrayRemove( + // count: count, + // singles: singles, + // batchSize: batchSize, + // crypto: const VeilidCryptoPublic()), + // ); + // } + // }); + // }); + // }); group('DHT Support Tests', () { setUpAll(updateProcessorFixture.setUp); diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart index 985b11f..d226b68 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart @@ -9,7 +9,6 @@ import 'package:meta/meta.dart'; import '../../../veilid_support.dart'; import '../../proto/proto.dart' as proto; -import '../interfaces/dht_add.dart'; part 'dht_log_spine.dart'; part 'dht_log_read.dart'; @@ -42,7 +41,7 @@ class DHTLogUpdate extends Equatable { /// * The head and tail position of the log /// - subkeyIdx = pos / recordsPerSubkey /// - recordIdx = pos % recordsPerSubkey -class DHTLog implements DHTDeleteable { +class DHTLog implements DHTDeleteable { //////////////////////////////////////////////////////////////// // Constructors @@ -172,24 +171,24 @@ class DHTLog implements DHTDeleteable { /// Add a reference to this log @override - Future ref() async => _mutex.protect(() async { + Future ref() async => _mutex.protect(() async { _openCount++; - return this; }); /// Free all resources for the DHTLog @override - Future close() async => _mutex.protect(() async { + Future close() async => _mutex.protect(() async { if (_openCount == 0) { throw StateError('already closed'); } _openCount--; if (_openCount != 0) { - return; + return false; } await _watchController?.close(); _watchController = null; await _spine.close(); + return true; }); /// Free all resources for the DHTLog and delete it from the DHT diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index 70155e8..0950c76 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -1,6 +1,6 @@ part of 'dht_log.dart'; -class _DHTLogPosition extends DHTCloseable<_DHTLogPosition, DHTShortArray> { +class _DHTLogPosition extends DHTCloseable { _DHTLogPosition._({ required _DHTLogSpine dhtLogSpine, required this.shortArray, @@ -12,13 +12,11 @@ class _DHTLogPosition extends DHTCloseable<_DHTLogPosition, DHTShortArray> { final _DHTLogSpine _dhtLogSpine; final DHTShortArray shortArray; - var _openCount = 1; final int _segmentNumber; - final Mutex _mutex = Mutex(); /// Check if the DHTLogPosition is open @override - bool get isOpen => _openCount > 0; + bool get isOpen => shortArray.isOpen; /// The type of the openable scope @override @@ -26,32 +24,13 @@ class _DHTLogPosition extends DHTCloseable<_DHTLogPosition, DHTShortArray> { /// Add a reference to this log @override - Future<_DHTLogPosition> ref() async => _mutex.protect(() async { - _openCount++; - return this; - }); + Future ref() async { + await shortArray.ref(); + } /// Free all resources for the DHTLogPosition @override - Future close() async => _mutex.protect(() async { - if (_openCount == 0) { - throw StateError('already closed'); - } - _openCount--; - if (_openCount != 0) { - return; - } - await _dhtLogSpine._segmentClosed(_segmentNumber); - }); -} - -class _OpenedSegment { - _OpenedSegment._({ - required this.shortArray, - }); - - final DHTShortArray shortArray; - int openCount = 1; + Future close() async => _dhtLogSpine._segmentClosed(_segmentNumber); } class _DHTLogSegmentLookup extends Equatable { @@ -81,7 +60,7 @@ class _DHTLogSpine { _tail = tail, _segmentStride = stride, _openedSegments = {}, - _spineCache = []; + _openCache = []; // Create a new spine record and push it to the network static Future<_DHTLogSpine> create( @@ -130,8 +109,8 @@ class _DHTLogSpine { return; } final futures = >[_spineRecord.close()]; - for (final (_, sc) in _spineCache) { - futures.add(sc.close()); + for (final seg in _openCache.toList()) { + futures.add(_segmentClosed(seg)); } await Future.wait(futures); @@ -308,7 +287,7 @@ class _DHTLogSpine { segmentKeyBytes); } - Future _openOrCreateSegmentInner(int segmentNumber) async { + Future _openOrCreateSegment(int segmentNumber) async { assert(_spineMutex.isLocked, 'should be in mutex here'); assert(_spineRecord.writer != null, 'should be writable'); @@ -366,7 +345,7 @@ class _DHTLogSpine { } } - Future _openSegmentInner(int segmentNumber) async { + Future _openSegment(int segmentNumber) async { assert(_spineMutex.isLocked, 'should be in mutex here'); // Lookup what subkey and segment subrange has this position's segment @@ -395,59 +374,6 @@ class _DHTLogSpine { return segmentRec; } - Future _openOrCreateSegment(int segmentNumber) async { - assert(_spineMutex.isLocked, 'should be in mutex here'); - - // See if we already have this in the cache - for (var i = 0; i < _spineCache.length; i++) { - if (_spineCache[i].$1 == segmentNumber) { - // Touch the element - final x = _spineCache.removeAt(i); - _spineCache.add(x); - // Return the shortarray for this position - return x.$2.ref(); - } - } - - // If we don't have it in the cache, get/create it and then cache a ref - final segment = await _openOrCreateSegmentInner(segmentNumber); - _spineCache.add((segmentNumber, await segment.ref())); - if (_spineCache.length > _spineCacheLength) { - // Trim the LRU cache - final (_, sa) = _spineCache.removeAt(0); - await sa.close(); - } - return segment; - } - - Future _openSegment(int segmentNumber) async { - assert(_spineMutex.isLocked, 'should be in mutex here'); - - // See if we already have this in the cache - for (var i = 0; i < _spineCache.length; i++) { - if (_spineCache[i].$1 == segmentNumber) { - // Touch the element - final x = _spineCache.removeAt(i); - _spineCache.add(x); - // Return the shortarray for this position - return x.$2.ref(); - } - } - - // If we don't have it in the cache, get it and then cache it - final segment = await _openSegmentInner(segmentNumber); - if (segment == null) { - return null; - } - _spineCache.add((segmentNumber, await segment.ref())); - if (_spineCache.length > _spineCacheLength) { - // Trim the LRU cache - final (_, sa) = _spineCache.removeAt(0); - await sa.close(); - } - return segment; - } - _DHTLogSegmentLookup _lookupSegment(int segmentNumber) { assert(_spineMutex.isLocked, 'should be in mutex here'); @@ -471,13 +397,15 @@ class _DHTLogSpine { int segmentNumber, int segmentPos, {bool onlyOpened = false}) async => _spineCacheMutex.protect(() async { - // Get the segment shortArray + // See if we have this segment opened already final openedSegment = _openedSegments[segmentNumber]; - late final DHTShortArray shortArray; + late DHTShortArray shortArray; if (openedSegment != null) { - openedSegment.openCount++; - shortArray = openedSegment.shortArray; + // If so, return a ref + await openedSegment.ref(); + shortArray = openedSegment; } else { + // Otherwise open a segment if (onlyOpened) { return null; } @@ -488,13 +416,26 @@ class _DHTLogSpine { if (newShortArray == null) { return null; } - - _openedSegments[segmentNumber] = - _OpenedSegment._(shortArray: newShortArray); - + // Keep in the opened segments table + _openedSegments[segmentNumber] = newShortArray; shortArray = newShortArray; } + // LRU cache the segment number + if (!_openCache.remove(segmentNumber)) { + // If this is new to the cache ref it when it goes in + await shortArray.ref(); + } + _openCache.add(segmentNumber); + if (_openCache.length > _openCacheSize) { + // Trim the LRU cache + final lruseg = _openCache.removeAt(0); + final lrusa = _openedSegments[lruseg]!; + if (await lrusa.close()) { + _openedSegments.remove(lruseg); + } + } + return _DHTLogPosition._( dhtLogSpine: this, shortArray: shortArray, @@ -521,15 +462,15 @@ class _DHTLogSpine { return lookupPositionBySegmentNumber(segmentNumber, segmentPos); } - Future _segmentClosed(int segmentNumber) async { + Future _segmentClosed(int segmentNumber) async { assert(_spineMutex.isLocked, 'should be locked'); - await _spineCacheMutex.protect(() async { - final os = _openedSegments[segmentNumber]!; - os.openCount--; - if (os.openCount == 0) { + return _spineCacheMutex.protect(() async { + final sa = _openedSegments[segmentNumber]!; + if (await sa.close()) { _openedSegments.remove(segmentNumber); - await os.shortArray.close(); + return true; } + return false; }); } @@ -693,21 +634,26 @@ class _DHTLogSpine { // and force their short arrays to refresh their heads if // they are opened final segmentsToRefresh = <_DHTLogPosition>[]; - for (var curTail = oldTail; - curTail != _tail; - curTail = (curTail + - (DHTShortArray.maxElements - - (curTail % DHTShortArray.maxElements))) % - _positionLimit) { + var curTail = oldTail; + final endSegmentNumber = _tail ~/ DHTShortArray.maxElements; + while (true) { final segmentNumber = curTail ~/ DHTShortArray.maxElements; final segmentPos = curTail % DHTShortArray.maxElements; final dhtLogPosition = await lookupPositionBySegmentNumber( segmentNumber, segmentPos, onlyOpened: true); - if (dhtLogPosition == null) { - continue; + if (dhtLogPosition != null) { + segmentsToRefresh.add(dhtLogPosition); } - segmentsToRefresh.add(dhtLogPosition); + + if (segmentNumber == endSegmentNumber) { + break; + } + + curTail = (curTail + + (DHTShortArray.maxElements - + (curTail % DHTShortArray.maxElements))) % + _positionLimit; } // Refresh the segments that have probably changed @@ -759,7 +705,7 @@ class _DHTLogSpine { // LRU cache of DHT spine elements accessed recently // Pair of position and associated shortarray segment final Mutex _spineCacheMutex = Mutex(); - final List<(int, DHTShortArray)> _spineCache; - final Map _openedSegments; - static const int _spineCacheLength = 3; + final List _openCache; + final Map _openedSegments; + static const int _openCacheSize = 3; } 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 521bf1f..4bd0ee6 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 @@ -36,7 +36,7 @@ enum DHTRecordRefreshMode { ///////////////////////////////////////////////// -class DHTRecord implements DHTDeleteable { +class DHTRecord implements DHTDeleteable { DHTRecord._( {required VeilidRoutingContext routingContext, required SharedDHTRecordData sharedDHTRecordData, @@ -64,25 +64,25 @@ class DHTRecord implements DHTDeleteable { /// Add a reference to this DHTRecord @override - Future ref() async => _mutex.protect(() async { + Future ref() async => _mutex.protect(() async { _openCount++; - return this; }); /// Free all resources for the DHTRecord @override - Future close() async => _mutex.protect(() async { + Future close() async => _mutex.protect(() async { if (_openCount == 0) { throw StateError('already closed'); } _openCount--; if (_openCount != 0) { - return; + return false; } await _watchController?.close(); _watchController = null; await DHTRecordPool.instance._recordClosed(this); + return true; }); /// Free all resources for the DHTRecord and delete it from the DHT 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 a84f02d..10ddf01 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 @@ -13,7 +13,7 @@ part 'dht_short_array_write.dart'; /////////////////////////////////////////////////////////////////////// -class DHTShortArray implements DHTDeleteable { +class DHTShortArray implements DHTDeleteable { //////////////////////////////////////////////////////////////// // Constructors @@ -148,25 +148,25 @@ class DHTShortArray implements DHTDeleteable { /// Add a reference to this shortarray @override - Future ref() async => _mutex.protect(() async { + Future ref() async => _mutex.protect(() async { _openCount++; - return this; }); /// Free all resources for the DHTShortArray @override - Future close() async => _mutex.protect(() async { + Future close() async => _mutex.protect(() async { if (_openCount == 0) { throw StateError('already closed'); } _openCount--; if (_openCount != 0) { - return; + return false; } await _watchController?.close(); _watchController = null; await _head.close(); + return true; }); /// Free all resources for the DHTShortArray and delete it from the DHT diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_closeable.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_closeable.dart index 65e9db1..c913340 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_closeable.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_closeable.dart @@ -2,19 +2,23 @@ import 'dart:async'; import 'package:meta/meta.dart'; -abstract class DHTCloseable { +abstract class DHTCloseable { + // Public interface + Future ref(); + Future close(); + + // Internal implementation + @protected bool get isOpen; @protected FutureOr scoped(); - Future ref(); - Future close(); } -abstract class DHTDeleteable extends DHTCloseable { +abstract class DHTDeleteable extends DHTCloseable { Future delete(); } -extension DHTCloseableExt on DHTCloseable { +extension DHTCloseableExt on DHTCloseable { /// Runs a closure that guarantees the DHTCloseable /// will be closed upon exit, even if an uncaught exception is thrown Future scope(Future Function(D) scopeFunction) async { @@ -29,7 +33,7 @@ extension DHTCloseableExt on DHTCloseable { } } -extension DHTDeletableExt on DHTDeleteable { +extension DHTDeletableExt on DHTDeleteable { /// Runs a closure that guarantees the DHTCloseable /// will be closed upon exit, and deleted if an an /// uncaught exception is thrown From 2c3d4dce93e3624ed2e9fdfe3b245e1a89e53cfb Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 8 Jun 2024 23:18:54 -0400 Subject: [PATCH 133/270] better handling of subkeys for spine --- .../example/integration_test/app_test.dart | 294 +++++++++--------- .../lib/dht_support/src/dht_log/dht_log.dart | 3 + .../src/dht_log/dht_log_cubit.dart | 13 +- .../dht_support/src/dht_log/dht_log_read.dart | 14 +- .../src/dht_log/dht_log_spine.dart | 21 +- .../src/dht_record/dht_record.dart | 2 +- .../src/dht_short_array/dht_short_array.dart | 3 + .../dht_short_array_cubit.dart | 13 +- .../src/interfaces/dht_random_read.dart | 2 +- 9 files changed, 203 insertions(+), 162 deletions(-) diff --git a/packages/veilid_support/example/integration_test/app_test.dart b/packages/veilid_support/example/integration_test/app_test.dart index a0f3b7f..e703ff6 100644 --- a/packages/veilid_support/example/integration_test/app_test.dart +++ b/packages/veilid_support/example/integration_test/app_test.dart @@ -36,161 +36,161 @@ void main() { setUpAll(veilidFixture.attach); tearDownAll(veilidFixture.detach); - // group('TableDB Tests', () { - // group('TableDBArray Tests', () { - // // test('create/delete TableDBArray', testTableDBArrayCreateDelete); + group('TableDB Tests', () { + group('TableDBArray Tests', () { + // test('create/delete TableDBArray', testTableDBArrayCreateDelete); - // group('TableDBArray Add/Get Tests', () { - // for (final params in [ - // // - // (99, 3, 15), - // (100, 4, 16), - // (101, 5, 17), - // // - // (511, 3, 127), - // (512, 4, 128), - // (513, 5, 129), - // // - // (4095, 3, 1023), - // (4096, 4, 1024), - // (4097, 5, 1025), - // // - // (65535, 3, 16383), - // (65536, 4, 16384), - // (65537, 5, 16385), - // ]) { - // final count = params.$1; - // final singles = params.$2; - // final batchSize = params.$3; + group('TableDBArray Add/Get Tests', () { + for (final params in [ + // + (99, 3, 15), + (100, 4, 16), + (101, 5, 17), + // + (511, 3, 127), + (512, 4, 128), + (513, 5, 129), + // + (4095, 3, 1023), + (4096, 4, 1024), + (4097, 5, 1025), + // + (65535, 3, 16383), + (65536, 4, 16384), + (65537, 5, 16385), + ]) { + final count = params.$1; + final singles = params.$2; + final batchSize = params.$3; - // test( - // timeout: const Timeout(Duration(seconds: 480)), - // 'add/remove TableDBArray count = $count batchSize=$batchSize', - // makeTestTableDBArrayAddGetClear( - // count: count, - // singles: singles, - // batchSize: batchSize, - // crypto: const VeilidCryptoPublic()), - // ); - // } - // }); + test( + timeout: const Timeout(Duration(seconds: 480)), + 'add/remove TableDBArray count = $count batchSize=$batchSize', + makeTestTableDBArrayAddGetClear( + count: count, + singles: singles, + batchSize: batchSize, + crypto: const VeilidCryptoPublic()), + ); + } + }); - // group('TableDBArray Insert Tests', () { - // for (final params in [ - // // - // (99, 3, 15), - // (100, 4, 16), - // (101, 5, 17), - // // - // (511, 3, 127), - // (512, 4, 128), - // (513, 5, 129), - // // - // (4095, 3, 1023), - // (4096, 4, 1024), - // (4097, 5, 1025), - // // - // (65535, 3, 16383), - // (65536, 4, 16384), - // (65537, 5, 16385), - // ]) { - // final count = params.$1; - // final singles = params.$2; - // final batchSize = params.$3; + group('TableDBArray Insert Tests', () { + for (final params in [ + // + (99, 3, 15), + (100, 4, 16), + (101, 5, 17), + // + (511, 3, 127), + (512, 4, 128), + (513, 5, 129), + // + (4095, 3, 1023), + (4096, 4, 1024), + (4097, 5, 1025), + // + (65535, 3, 16383), + (65536, 4, 16384), + (65537, 5, 16385), + ]) { + final count = params.$1; + final singles = params.$2; + final batchSize = params.$3; - // test( - // timeout: const Timeout(Duration(seconds: 480)), - // 'insert TableDBArray count=$count singles=$singles batchSize=$batchSize', - // makeTestTableDBArrayInsert( - // count: count, - // singles: singles, - // batchSize: batchSize, - // crypto: const VeilidCryptoPublic()), - // ); - // } - // }); + test( + timeout: const Timeout(Duration(seconds: 480)), + 'insert TableDBArray count=$count singles=$singles batchSize=$batchSize', + makeTestTableDBArrayInsert( + count: count, + singles: singles, + batchSize: batchSize, + crypto: const VeilidCryptoPublic()), + ); + } + }); - // group('TableDBArray Remove Tests', () { - // for (final params in [ - // // - // (99, 3, 15), - // (100, 4, 16), - // (101, 5, 17), - // // - // (511, 3, 127), - // (512, 4, 128), - // (513, 5, 129), - // // - // (4095, 3, 1023), - // (4096, 4, 1024), - // (4097, 5, 1025), - // // - // (16383, 3, 4095), - // (16384, 4, 4096), - // (16385, 5, 4097), - // ]) { - // final count = params.$1; - // final singles = params.$2; - // final batchSize = params.$3; + group('TableDBArray Remove Tests', () { + for (final params in [ + // + (99, 3, 15), + (100, 4, 16), + (101, 5, 17), + // + (511, 3, 127), + (512, 4, 128), + (513, 5, 129), + // + (4095, 3, 1023), + (4096, 4, 1024), + (4097, 5, 1025), + // + (16383, 3, 4095), + (16384, 4, 4096), + (16385, 5, 4097), + ]) { + final count = params.$1; + final singles = params.$2; + final batchSize = params.$3; - // test( - // timeout: const Timeout(Duration(seconds: 480)), - // 'remove TableDBArray count=$count singles=$singles batchSize=$batchSize', - // makeTestTableDBArrayRemove( - // count: count, - // singles: singles, - // batchSize: batchSize, - // crypto: const VeilidCryptoPublic()), - // ); - // } - // }); - // }); - // }); - - group('DHT Support Tests', () { - setUpAll(updateProcessorFixture.setUp); - setUpAll(tickerFixture.setUp); - tearDownAll(tickerFixture.tearDown); - tearDownAll(updateProcessorFixture.tearDown); - - test('create pool', testDHTRecordPoolCreate); - - group('DHTRecordPool Tests', () { - setUpAll(dhtRecordPoolFixture.setUp); - tearDownAll(dhtRecordPoolFixture.tearDown); - - test('create/delete record', testDHTRecordCreateDelete); - test('record scopes', testDHTRecordScopes); - test('create/delete deep record', testDHTRecordDeepCreateDelete); - }); - - group('DHTShortArray Tests', () { - setUpAll(dhtRecordPoolFixture.setUp); - tearDownAll(dhtRecordPoolFixture.tearDown); - - for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { - test('create shortarray stride=$stride', - makeTestDHTShortArrayCreateDelete(stride: stride)); - test('add shortarray stride=$stride', - makeTestDHTShortArrayAdd(stride: stride)); - } - }); - - group('DHTLog Tests', () { - setUpAll(dhtRecordPoolFixture.setUp); - tearDownAll(dhtRecordPoolFixture.tearDown); - - for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { - test('create log stride=$stride', - makeTestDHTLogCreateDelete(stride: stride)); - test( - timeout: const Timeout(Duration(seconds: 480)), - 'add/truncate log stride=$stride', - makeTestDHTLogAddTruncate(stride: stride), - ); - } + test( + timeout: const Timeout(Duration(seconds: 480)), + 'remove TableDBArray count=$count singles=$singles batchSize=$batchSize', + makeTestTableDBArrayRemove( + count: count, + singles: singles, + batchSize: batchSize, + crypto: const VeilidCryptoPublic()), + ); + } + }); }); }); + + // group('DHT Support Tests', () { + // setUpAll(updateProcessorFixture.setUp); + // setUpAll(tickerFixture.setUp); + // tearDownAll(tickerFixture.tearDown); + // tearDownAll(updateProcessorFixture.tearDown); + + // test('create pool', testDHTRecordPoolCreate); + + // group('DHTRecordPool Tests', () { + // setUpAll(dhtRecordPoolFixture.setUp); + // tearDownAll(dhtRecordPoolFixture.tearDown); + + // test('create/delete record', testDHTRecordCreateDelete); + // test('record scopes', testDHTRecordScopes); + // test('create/delete deep record', testDHTRecordDeepCreateDelete); + // }); + + // group('DHTShortArray Tests', () { + // setUpAll(dhtRecordPoolFixture.setUp); + // tearDownAll(dhtRecordPoolFixture.tearDown); + + // for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { + // test('create shortarray stride=$stride', + // makeTestDHTShortArrayCreateDelete(stride: stride)); + // test('add shortarray stride=$stride', + // makeTestDHTShortArrayAdd(stride: stride)); + // } + // }); + + // group('DHTLog Tests', () { + // setUpAll(dhtRecordPoolFixture.setUp); + // tearDownAll(dhtRecordPoolFixture.tearDown); + + // for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { + // test('create log stride=$stride', + // makeTestDHTLogCreateDelete(stride: stride)); + // test( + // timeout: const Timeout(Duration(seconds: 480)), + // 'add/truncate log stride=$stride', + // makeTestDHTLogAddTruncate(stride: stride), + // ); + // } + // }); + // }); }); }); } diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart index d226b68..6b6f2fe 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart @@ -204,6 +204,9 @@ class DHTLog implements DHTDeleteable { /// Get the record key for this log TypedKey get recordKey => _spine.recordKey; + /// Get the writer for the log + KeyPair? get writer => _spine._spineRecord.writer; + /// Get the record pointer foir this log OwnedDHTRecordPointer get recordPointer => _spine.recordPointer; diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart index b5a728b..effd527 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart @@ -126,13 +126,22 @@ class DHTLogCubit extends Cubit> final end = ((tail - 1) % length) + 1; final start = (count < end) ? end - count : 0; - final offlinePositions = await reader.getOfflinePositions(); + // If this is writeable get the offline positions + Set? offlinePositions; + if (_log.writer != null) { + offlinePositions = await reader.getOfflinePositions(); + if (offlinePositions == null) { + return const AsyncValue.loading(); + } + } + + // Get the items final allItems = (await reader.getRange(start, length: end - start, forceRefresh: forceRefresh)) ?.indexed .map((x) => OnlineElementState( value: _decodeElement(x.$2), - isOffline: offlinePositions.contains(x.$1))) + isOffline: offlinePositions?.contains(x.$1) ?? false)) .toIList(); if (allItems == null) { return const AsyncValue.loading(); diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart index 7f397ac..d7b3541 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart @@ -61,20 +61,23 @@ class _DHTLogRead implements DHTLogReadOperations { } @override - Future> getOfflinePositions() async { + Future?> getOfflinePositions() async { final positionOffline = {}; // Iterate positions backward from most recent for (var pos = _spine.length - 1; pos >= 0; pos--) { final lookup = await _spine.lookupPosition(pos); if (lookup == null) { - throw StateError('Unable to look up position'); + return null; } // Check each segment for offline positions var foundOffline = false; - await lookup.scope((sa) => sa.operate((read) async { + final success = await lookup.scope((sa) => sa.operate((read) async { final segmentOffline = await read.getOfflinePositions(); + if (segmentOffline == null) { + return false; + } // For each shortarray segment go through their segment positions // in reverse order and see if they are offline @@ -88,8 +91,11 @@ class _DHTLogRead implements DHTLogReadOperations { foundOffline = true; } } + return true; })); - + if (!success) { + return null; + } // If we found nothing offline in this segment then we can stop if (!foundOffline) { break; diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index 0950c76..a6b6068 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -354,13 +354,24 @@ class _DHTLogSpine { final subkey = l.subkey; final segment = l.segment; - final subkeyData = await _spineRecord.get(subkey: subkey); - if (subkeyData == null) { - return null; + // See if we have the segment key locally + TypedKey? segmentKey; + var subkeyData = await _spineRecord.get( + subkey: subkey, refreshMode: DHTRecordRefreshMode.local); + if (subkeyData != null) { + segmentKey = _getSegmentKey(subkeyData, segment); } - final segmentKey = _getSegmentKey(subkeyData, segment); if (segmentKey == null) { - return null; + // If not, try from the network + subkeyData = await _spineRecord.get( + subkey: subkey, refreshMode: DHTRecordRefreshMode.network); + if (subkeyData == null) { + return null; + } + segmentKey = _getSegmentKey(subkeyData, segment); + if (segmentKey == null) { + return null; + } } // Open a shortarray segment 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 4bd0ee6..0539f8b 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 @@ -16,7 +16,7 @@ class DHTRecordWatchChange extends Equatable { /// Refresh mode for DHT record 'get' enum DHTRecordRefreshMode { /// Return existing subkey values if they exist locally already - /// And then check the network for a newer value + /// If not, check the network for a value /// This is the default refresh mode cached, 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 10ddf01..562536e 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 @@ -182,6 +182,9 @@ class DHTShortArray implements DHTDeleteable { /// Get the record key for this shortarray TypedKey get recordKey => _head.recordKey; + /// Get the writer for the log + KeyPair? get writer => _head._headRecord.writer; + /// Get the record pointer foir this shortarray OwnedDHTRecordPointer get recordPointer => _head.recordPointer; 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 90fcbad..1355d4c 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 @@ -53,12 +53,21 @@ class DHTShortArrayCubit extends Cubit> {bool forceRefresh = false}) async { try { final newState = await _shortArray.operate((reader) async { - final offlinePositions = await reader.getOfflinePositions(); + // If this is writeable get the offline positions + Set? offlinePositions; + if (_shortArray.writer != null) { + offlinePositions = await reader.getOfflinePositions(); + if (offlinePositions == null) { + return null; + } + } + + // Get the items final allItems = (await reader.getRange(0, forceRefresh: forceRefresh)) ?.indexed .map((x) => DHTShortArrayElementState( value: _decodeElement(x.$2), - isOffline: offlinePositions.contains(x.$1))) + isOffline: offlinePositions?.contains(x.$1) ?? false)) .toIList(); return allItems; }); diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart index 362d688..0547332 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart @@ -26,7 +26,7 @@ abstract class DHTRandomRead { {int? length, bool forceRefresh = false}); /// Get a list of the positions that were written offline and not flushed yet - Future> getOfflinePositions(); + Future?> getOfflinePositions(); } extension DHTRandomReadExt on DHTRandomRead { From b5612e5dd86b9ebdb94769021110fabc8f34ead1 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 9 Jun 2024 14:43:28 -0400 Subject: [PATCH 134/270] fix eventual consistency --- .../cubits/single_contact_messages_cubit.dart | 2 +- lib/chat_list/cubits/chat_list_cubit.dart | 5 +- .../cubits/contact_invitation_list_cubit.dart | 4 +- lib/contacts/cubits/contact_list_cubit.dart | 4 +- .../example/integration_test/app_test.dart | 294 +++++++++--------- .../integration_test/test_dht_log.dart | 3 +- .../test_dht_short_array.dart | 10 +- packages/veilid_support/example/pubspec.lock | 8 +- packages/veilid_support/example/pubspec.yaml | 2 +- .../lib/dht_support/src/dht_log/dht_log.dart | 10 +- .../src/dht_log/dht_log_cubit.dart | 4 +- .../src/dht_log/dht_log_spine.dart | 27 +- .../src/dht_log/dht_log_write.dart | 100 +++--- .../src/dht_record/dht_record.dart | 6 +- .../src/dht_short_array/dht_short_array.dart | 10 +- .../dht_short_array_cubit.dart | 4 +- .../dht_short_array/dht_short_array_head.dart | 33 +- .../dht_short_array_write.dart | 19 +- .../dht_support/src/interfaces/dht_add.dart | 24 +- .../src/interfaces/dht_insert_remove.dart | 16 +- packages/veilid_support/pubspec.yaml | 14 +- pubspec.lock | 4 +- pubspec.yaml | 2 +- 23 files changed, 309 insertions(+), 296 deletions(-) diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index b3c6325..0894ac1 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -292,7 +292,7 @@ class SingleContactMessagesCubit extends Cubit { } await _sentMessagesCubit!.operateAppendEventual((writer) => - writer.tryAddAll(messages.map((m) => m.writeToBuffer()).toList())); + writer.addAll(messages.map((m) => m.writeToBuffer()).toList())); } // Produce a state for this cubit from the input cubits and queues diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index 2ab2993..0c36f52 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -84,10 +84,7 @@ class ChatListCubit extends DHTShortArrayCubit ..remoteConversationRecordKey = remoteConversationRecordKey.toProto(); // Add chat - final added = await writer.tryAdd(chat.writeToBuffer()); - if (!added) { - throw Exception('Failed to add chat'); - } + await writer.add(chat.writeToBuffer()); }); } diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index e278c6d..f2f44e9 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -160,9 +160,7 @@ class ContactInvitationListCubit // Add ContactInvitationRecord to account's list // if this fails, don't keep retrying, user can try again later await operateWrite((writer) async { - if (await writer.tryAdd(cinvrec.writeToBuffer()) == false) { - throw Exception('Failed to add contact invitation record'); - } + await writer.add(cinvrec.writeToBuffer()); }); }); }); diff --git a/lib/contacts/cubits/contact_list_cubit.dart b/lib/contacts/cubits/contact_list_cubit.dart index c985392..5ab14ea 100644 --- a/lib/contacts/cubits/contact_list_cubit.dart +++ b/lib/contacts/cubits/contact_list_cubit.dart @@ -54,9 +54,7 @@ class ContactListCubit extends DHTShortArrayCubit { // Add Contact to account's list // if this fails, don't keep retrying, user can try again later await operateWrite((writer) async { - if (!await writer.tryAdd(contact.writeToBuffer())) { - throw Exception('Failed to add contact record'); - } + await writer.add(contact.writeToBuffer()); }); } diff --git a/packages/veilid_support/example/integration_test/app_test.dart b/packages/veilid_support/example/integration_test/app_test.dart index e703ff6..a0f3b7f 100644 --- a/packages/veilid_support/example/integration_test/app_test.dart +++ b/packages/veilid_support/example/integration_test/app_test.dart @@ -36,161 +36,161 @@ void main() { setUpAll(veilidFixture.attach); tearDownAll(veilidFixture.detach); - group('TableDB Tests', () { - group('TableDBArray Tests', () { - // test('create/delete TableDBArray', testTableDBArrayCreateDelete); + // group('TableDB Tests', () { + // group('TableDBArray Tests', () { + // // test('create/delete TableDBArray', testTableDBArrayCreateDelete); - group('TableDBArray Add/Get Tests', () { - for (final params in [ - // - (99, 3, 15), - (100, 4, 16), - (101, 5, 17), - // - (511, 3, 127), - (512, 4, 128), - (513, 5, 129), - // - (4095, 3, 1023), - (4096, 4, 1024), - (4097, 5, 1025), - // - (65535, 3, 16383), - (65536, 4, 16384), - (65537, 5, 16385), - ]) { - final count = params.$1; - final singles = params.$2; - final batchSize = params.$3; + // group('TableDBArray Add/Get Tests', () { + // for (final params in [ + // // + // (99, 3, 15), + // (100, 4, 16), + // (101, 5, 17), + // // + // (511, 3, 127), + // (512, 4, 128), + // (513, 5, 129), + // // + // (4095, 3, 1023), + // (4096, 4, 1024), + // (4097, 5, 1025), + // // + // (65535, 3, 16383), + // (65536, 4, 16384), + // (65537, 5, 16385), + // ]) { + // final count = params.$1; + // final singles = params.$2; + // final batchSize = params.$3; - test( - timeout: const Timeout(Duration(seconds: 480)), - 'add/remove TableDBArray count = $count batchSize=$batchSize', - makeTestTableDBArrayAddGetClear( - count: count, - singles: singles, - batchSize: batchSize, - crypto: const VeilidCryptoPublic()), - ); - } - }); + // test( + // timeout: const Timeout(Duration(seconds: 480)), + // 'add/remove TableDBArray count = $count batchSize=$batchSize', + // makeTestTableDBArrayAddGetClear( + // count: count, + // singles: singles, + // batchSize: batchSize, + // crypto: const VeilidCryptoPublic()), + // ); + // } + // }); - group('TableDBArray Insert Tests', () { - for (final params in [ - // - (99, 3, 15), - (100, 4, 16), - (101, 5, 17), - // - (511, 3, 127), - (512, 4, 128), - (513, 5, 129), - // - (4095, 3, 1023), - (4096, 4, 1024), - (4097, 5, 1025), - // - (65535, 3, 16383), - (65536, 4, 16384), - (65537, 5, 16385), - ]) { - final count = params.$1; - final singles = params.$2; - final batchSize = params.$3; + // group('TableDBArray Insert Tests', () { + // for (final params in [ + // // + // (99, 3, 15), + // (100, 4, 16), + // (101, 5, 17), + // // + // (511, 3, 127), + // (512, 4, 128), + // (513, 5, 129), + // // + // (4095, 3, 1023), + // (4096, 4, 1024), + // (4097, 5, 1025), + // // + // (65535, 3, 16383), + // (65536, 4, 16384), + // (65537, 5, 16385), + // ]) { + // final count = params.$1; + // final singles = params.$2; + // final batchSize = params.$3; - test( - timeout: const Timeout(Duration(seconds: 480)), - 'insert TableDBArray count=$count singles=$singles batchSize=$batchSize', - makeTestTableDBArrayInsert( - count: count, - singles: singles, - batchSize: batchSize, - crypto: const VeilidCryptoPublic()), - ); - } - }); + // test( + // timeout: const Timeout(Duration(seconds: 480)), + // 'insert TableDBArray count=$count singles=$singles batchSize=$batchSize', + // makeTestTableDBArrayInsert( + // count: count, + // singles: singles, + // batchSize: batchSize, + // crypto: const VeilidCryptoPublic()), + // ); + // } + // }); - group('TableDBArray Remove Tests', () { - for (final params in [ - // - (99, 3, 15), - (100, 4, 16), - (101, 5, 17), - // - (511, 3, 127), - (512, 4, 128), - (513, 5, 129), - // - (4095, 3, 1023), - (4096, 4, 1024), - (4097, 5, 1025), - // - (16383, 3, 4095), - (16384, 4, 4096), - (16385, 5, 4097), - ]) { - final count = params.$1; - final singles = params.$2; - final batchSize = params.$3; + // group('TableDBArray Remove Tests', () { + // for (final params in [ + // // + // (99, 3, 15), + // (100, 4, 16), + // (101, 5, 17), + // // + // (511, 3, 127), + // (512, 4, 128), + // (513, 5, 129), + // // + // (4095, 3, 1023), + // (4096, 4, 1024), + // (4097, 5, 1025), + // // + // (16383, 3, 4095), + // (16384, 4, 4096), + // (16385, 5, 4097), + // ]) { + // final count = params.$1; + // final singles = params.$2; + // final batchSize = params.$3; - test( - timeout: const Timeout(Duration(seconds: 480)), - 'remove TableDBArray count=$count singles=$singles batchSize=$batchSize', - makeTestTableDBArrayRemove( - count: count, - singles: singles, - batchSize: batchSize, - crypto: const VeilidCryptoPublic()), - ); - } - }); - }); - }); - - // group('DHT Support Tests', () { - // setUpAll(updateProcessorFixture.setUp); - // setUpAll(tickerFixture.setUp); - // tearDownAll(tickerFixture.tearDown); - // tearDownAll(updateProcessorFixture.tearDown); - - // test('create pool', testDHTRecordPoolCreate); - - // group('DHTRecordPool Tests', () { - // setUpAll(dhtRecordPoolFixture.setUp); - // tearDownAll(dhtRecordPoolFixture.tearDown); - - // test('create/delete record', testDHTRecordCreateDelete); - // test('record scopes', testDHTRecordScopes); - // test('create/delete deep record', testDHTRecordDeepCreateDelete); - // }); - - // group('DHTShortArray Tests', () { - // setUpAll(dhtRecordPoolFixture.setUp); - // tearDownAll(dhtRecordPoolFixture.tearDown); - - // for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { - // test('create shortarray stride=$stride', - // makeTestDHTShortArrayCreateDelete(stride: stride)); - // test('add shortarray stride=$stride', - // makeTestDHTShortArrayAdd(stride: stride)); - // } - // }); - - // group('DHTLog Tests', () { - // setUpAll(dhtRecordPoolFixture.setUp); - // tearDownAll(dhtRecordPoolFixture.tearDown); - - // for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { - // test('create log stride=$stride', - // makeTestDHTLogCreateDelete(stride: stride)); - // test( - // timeout: const Timeout(Duration(seconds: 480)), - // 'add/truncate log stride=$stride', - // makeTestDHTLogAddTruncate(stride: stride), - // ); - // } + // test( + // timeout: const Timeout(Duration(seconds: 480)), + // 'remove TableDBArray count=$count singles=$singles batchSize=$batchSize', + // makeTestTableDBArrayRemove( + // count: count, + // singles: singles, + // batchSize: batchSize, + // crypto: const VeilidCryptoPublic()), + // ); + // } + // }); // }); // }); + + group('DHT Support Tests', () { + setUpAll(updateProcessorFixture.setUp); + setUpAll(tickerFixture.setUp); + tearDownAll(tickerFixture.tearDown); + tearDownAll(updateProcessorFixture.tearDown); + + test('create pool', testDHTRecordPoolCreate); + + group('DHTRecordPool Tests', () { + setUpAll(dhtRecordPoolFixture.setUp); + tearDownAll(dhtRecordPoolFixture.tearDown); + + test('create/delete record', testDHTRecordCreateDelete); + test('record scopes', testDHTRecordScopes); + test('create/delete deep record', testDHTRecordDeepCreateDelete); + }); + + group('DHTShortArray Tests', () { + setUpAll(dhtRecordPoolFixture.setUp); + tearDownAll(dhtRecordPoolFixture.tearDown); + + for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { + test('create shortarray stride=$stride', + makeTestDHTShortArrayCreateDelete(stride: stride)); + test('add shortarray stride=$stride', + makeTestDHTShortArrayAdd(stride: stride)); + } + }); + + group('DHTLog Tests', () { + setUpAll(dhtRecordPoolFixture.setUp); + tearDownAll(dhtRecordPoolFixture.tearDown); + + for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { + test('create log stride=$stride', + makeTestDHTLogCreateDelete(stride: stride)); + test( + timeout: const Timeout(Duration(seconds: 480)), + 'add/truncate log stride=$stride', + makeTestDHTLogAddTruncate(stride: stride), + ); + } + }); + }); }); }); } diff --git a/packages/veilid_support/example/integration_test/test_dht_log.dart b/packages/veilid_support/example/integration_test/test_dht_log.dart index 0ebdd55..3ca4eb9 100644 --- a/packages/veilid_support/example/integration_test/test_dht_log.dart +++ b/packages/veilid_support/example/integration_test/test_dht_log.dart @@ -64,8 +64,7 @@ Future Function() makeTestDHTLogAddTruncate({required int stride}) => const chunk = 25; for (var n = 0; n < dataset.length; n += chunk) { print('$n-${n + chunk - 1} '); - final success = await w.tryAddAll(dataset.sublist(n, n + chunk)); - expect(success, isTrue); + await w.addAll(dataset.sublist(n, n + chunk)); } }); expect(res, isNull); diff --git a/packages/veilid_support/example/integration_test/test_dht_short_array.dart b/packages/veilid_support/example/integration_test/test_dht_short_array.dart index 497b5fc..52e7942 100644 --- a/packages/veilid_support/example/integration_test/test_dht_short_array.dart +++ b/packages/veilid_support/example/integration_test/test_dht_short_array.dart @@ -64,7 +64,7 @@ Future Function() makeTestDHTShortArrayAdd({required int stride}) => for (var n = 4; n < 8; n++) { await arr.operateWriteEventual((w) async { print('$n '); - return w.tryAdd(dataset[n]); + await w.add(dataset[n]); }); } } @@ -73,8 +73,7 @@ Future Function() makeTestDHTShortArrayAdd({required int stride}) => { await arr.operateWriteEventual((w) async { print('${dataset.length ~/ 2}-${dataset.length}'); - return w - .tryAddAll(dataset.sublist(dataset.length ~/ 2, dataset.length)); + await w.addAll(dataset.sublist(dataset.length ~/ 2, dataset.length)); }); } @@ -83,7 +82,7 @@ Future Function() makeTestDHTShortArrayAdd({required int stride}) => for (var n = 0; n < 4; n++) { await arr.operateWriteEventual((w) async { print('$n '); - return w.tryInsert(n, dataset[n]); + await w.insert(n, dataset[n]); }); } } @@ -92,7 +91,7 @@ Future Function() makeTestDHTShortArrayAdd({required int stride}) => { await arr.operateWriteEventual((w) async { print('8-${dataset.length ~/ 2}'); - return w.tryInsertAll(8, dataset.sublist(8, dataset.length ~/ 2)); + await w.insertAll(8, dataset.sublist(8, dataset.length ~/ 2)); }); } @@ -111,7 +110,6 @@ Future Function() makeTestDHTShortArrayAdd({required int stride}) => { await arr.operateWriteEventual((w) async { await w.clear(); - return true; }); } diff --git a/packages/veilid_support/example/pubspec.lock b/packages/veilid_support/example/pubspec.lock index b7cbcd7..5ff4899 100644 --- a/packages/veilid_support/example/pubspec.lock +++ b/packages/veilid_support/example/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: "direct dev" description: name: async_tools - sha256: e783ac6ed5645c86da34240389bb3a000fc5e3ae6589c6a482eb24ece7217681 + sha256: "72590010ed6c6f5cbd5d40e33834abc08a43da6a73ac3c3645517d53899b8684" url: "https://pub.dev" source: hosted - version: "0.1.1" + version: "0.1.2" bloc: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: bloc_advanced_tools - sha256: "09f8a121d950575f1f2980c8b10df46b2ac6c72c8cbe48cc145871e5882ed430" + sha256: "0cf9b3a73a67addfe22ec3f97a1ac240f6ad53870d6b21a980260f390d7901cd" url: "https://pub.dev" source: hosted - version: "0.1.1" + version: "0.1.2" boolean_selector: dependency: transitive description: diff --git a/packages/veilid_support/example/pubspec.yaml b/packages/veilid_support/example/pubspec.yaml index f353fc9..2599f5f 100644 --- a/packages/veilid_support/example/pubspec.yaml +++ b/packages/veilid_support/example/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: path: ../ dev_dependencies: - async_tools: ^0.1.1 + async_tools: ^0.1.2 integration_test: sdk: flutter lint_hard: ^4.0.0 diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart index 6b6f2fe..71bcfa2 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart @@ -242,11 +242,11 @@ class DHTLog implements DHTDeleteable { /// Runs a closure allowing append/truncate access to the log /// 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 operateAppendEventual( - Future Function(DHTLogWriteOperations) closure, + /// TimeoutException. The closure should return a value if its changes also + /// succeeded, and throw DHTExceptionTryAgain to trigger another + /// eventual consistency pass. + Future operateAppendEventual( + Future Function(DHTLogWriteOperations) closure, {Duration? timeout}) async { if (!isOpen) { throw StateError('log is not open"'); diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart index effd527..570474f 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart @@ -210,8 +210,8 @@ class DHTLogCubit extends Cubit> return _log.operateAppend(closure); } - Future operateAppendEventual( - Future Function(DHTLogWriteOperations) closure, + Future operateAppendEventual( + Future Function(DHTLogWriteOperations) closure, {Duration? timeout}) async { await _initWait(); return _log.operateAppendEventual(closure, timeout: timeout); diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index a6b6068..3105fa8 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -152,17 +152,16 @@ class _DHTLogSpine { } }); - Future operateAppendEventual( - Future Function(_DHTLogSpine) closure, + Future operateAppendEventual(Future Function(_DHTLogSpine) closure, {Duration? timeout}) async { final timeoutTs = timeout == null ? null : Veilid.instance.now().offset(TimestampDuration.fromDuration(timeout)); - await _spineMutex.protect(() async { + return _spineMutex.protect(() async { late int oldHead; late int oldTail; - + late T out; try { // Iterate until we have a successful element and head write do { @@ -180,17 +179,19 @@ class _DHTLogSpine { } } try { - if (await closure(this)) { - break; - } + out = await closure(this); + break; } on DHTExceptionTryAgain { - // + // Failed to write in closure resets state + _head = oldHead; + _tail = oldTail; + } on Exception { + // Failed to write in closure resets state + _head = oldHead; + _tail = oldTail; + rethrow; } - // Failed to write in closure resets state - _head = oldHead; - _tail = oldTail; } - // Try to do the head write } while (!await writeSpineHead(old: (oldHead, oldTail))); } on Exception { @@ -199,6 +200,8 @@ class _DHTLogSpine { _tail = oldTail; rethrow; } + + return out; }); } diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart index a688453..ca47e00 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart @@ -17,12 +17,22 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { } final lookup = await _spine.lookupPosition(pos); if (lookup == null) { - throw StateError("can't lookup position in write to dht log"); + throw DHTExceptionInvalidData(); } // Write item to the segment - return lookup.scope((sa) => sa.operateWrite((write) async => - write.tryWriteItem(lookup.pos, newValue, output: output))); + try { + await lookup.scope((sa) => sa.operateWrite((write) async { + final success = + await write.tryWriteItem(lookup.pos, newValue, output: output); + if (!success) { + throw DHTExceptionTryAgain(); + } + })); + } on DHTExceptionTryAgain { + return false; + } + return true; } @override @@ -35,40 +45,47 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { } final aLookup = await _spine.lookupPosition(aPos); if (aLookup == null) { - throw StateError("can't lookup position a in swap of dht log"); + throw DHTExceptionInvalidData(); } final bLookup = await _spine.lookupPosition(bPos); if (bLookup == null) { await aLookup.close(); - throw StateError("can't lookup position b in swap of dht log"); + throw DHTExceptionInvalidData(); } // Swap items in the segments if (aLookup.shortArray == bLookup.shortArray) { await bLookup.close(); - await aLookup.scope((sa) => sa.operateWriteEventual((aWrite) async { - await aWrite.swap(aLookup.pos, bLookup.pos); - return true; - })); + return aLookup.scope((sa) => sa.operateWriteEventual( + (aWrite) async => aWrite.swap(aLookup.pos, bLookup.pos))); } else { final bItem = Output(); - await aLookup.scope( + return aLookup.scope( (sa) => bLookup.scope((sb) => sa.operateWriteEventual((aWrite) async { if (bItem.value == null) { final aItem = await aWrite.get(aLookup.pos); if (aItem == null) { - throw StateError("can't get item for position a in swap"); + throw DHTExceptionInvalidData(); } - await sb.operateWriteEventual((bWrite) async => - bWrite.tryWriteItem(bLookup.pos, aItem, output: bItem)); + await sb.operateWriteEventual((bWrite) async { + final success = await bWrite + .tryWriteItem(bLookup.pos, aItem, output: bItem); + if (!success) { + throw DHTExceptionTryAgain(); + } + }); + } + final success = + await aWrite.tryWriteItem(aLookup.pos, bItem.value!); + if (!success) { + throw DHTExceptionTryAgain(); } - return aWrite.tryWriteItem(aLookup.pos, bItem.value!); }))); } } @override - Future tryAdd(Uint8List value) async { + Future add(Uint8List value) async { // Allocate empty index at the end of the list final insertPos = _spine.length; _spine.allocateTail(1); @@ -78,26 +95,20 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { } // Write item to the segment - return lookup.scope((sa) async { - try { - return sa.operateWrite((write) async { + return lookup.scope((sa) async => sa.operateWrite((write) async { // If this a new segment, then clear it in case we have wrapped around if (lookup.pos == 0) { await write.clear(); } else if (lookup.pos != write.length) { // We should always be appending at the length - throw StateError('appending should be at the end'); + throw DHTExceptionInvalidData(); } - return write.tryAdd(value); - }); - } on DHTExceptionTryAgain { - return false; - } - }); + return write.add(value); + })); } @override - Future tryAddAll(List values) async { + Future addAll(List values) async { // Allocate empty index at the end of the list final insertPos = _spine.length; _spine.allocateTail(values.length); @@ -111,31 +122,26 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { final lookup = await _spine.lookupPosition(insertPos + valueIdx); if (lookup == null) { - throw StateError("can't write to dht log"); + throw DHTExceptionInvalidData(); } final sacount = min(remaining, DHTShortArray.maxElements - lookup.pos); final sublistValues = values.sublist(valueIdx, valueIdx + sacount); dws.add(() async { - final ok = await lookup.scope((sa) async { - try { - return sa.operateWrite((write) async { - // If this a new segment, then clear it in - // case we have wrapped around - if (lookup.pos == 0) { - await write.clear(); - } else if (lookup.pos != write.length) { - // We should always be appending at the length - throw StateError('appending should be at the end'); - } - return write.tryAddAll(sublistValues); - }); - } on DHTExceptionTryAgain { - return false; - } - }); - if (!ok) { + try { + await lookup.scope((sa) async => sa.operateWrite((write) async { + // If this a new segment, then clear it in + // case we have wrapped around + if (lookup.pos == 0) { + await write.clear(); + } else if (lookup.pos != write.length) { + // We should always be appending at the length + throw DHTExceptionInvalidData(); + } + return write.addAll(sublistValues); + })); + } on DHTExceptionTryAgain { success = false; } }); @@ -145,7 +151,9 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { await dws(); - return success; + if (!success) { + throw DHTExceptionTryAgain(); + } } @override 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 0539f8b..cd6c859 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 @@ -1,5 +1,7 @@ part of 'dht_record_pool.dart'; +const _sfListen = 'listen'; + @immutable class DHTRecordWatchChange extends Equatable { const DHTRecordWatchChange( @@ -79,6 +81,7 @@ class DHTRecord implements DHTDeleteable { return false; } + await serialFuturePause((this, _sfListen)); await _watchController?.close(); _watchController = null; await DHTRecordPool.instance._recordClosed(this); @@ -445,7 +448,8 @@ class DHTRecord implements DHTDeleteable { if (change.local && !localChanges) { return; } - Future.delayed(Duration.zero, () async { + + serialFuture((this, _sfListen), () async { final Uint8List? data; if (change.local) { // local changes are not encrypted 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 562536e..fe291ca 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 @@ -232,11 +232,11 @@ class DHTShortArray implements DHTDeleteable { /// 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(DHTShortArrayWriteOperations) closure, + /// TimeoutException. The closure should return a value if its changes also + /// succeeded, and throw DHTExceptionTryAgain to trigger another + /// eventual consistency pass. + Future operateWriteEventual( + Future Function(DHTShortArrayWriteOperations) closure, {Duration? timeout}) async { if (!isOpen) { throw StateError('short array is not open"'); 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 1355d4c..d9e1e57 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 @@ -111,8 +111,8 @@ class DHTShortArrayCubit extends Cubit> return _shortArray.operateWrite(closure); } - Future operateWriteEventual( - Future Function(DHTShortArrayWriteOperations) closure, + Future operateWriteEventual( + Future Function(DHTShortArrayWriteOperations) closure, {Duration? timeout}) async { await _initWait(); return _shortArray.operateWriteEventual(closure, timeout: timeout); 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 68c2a18..45c4e71 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 @@ -107,19 +107,20 @@ class _DHTShortArrayHead { } }); - Future operateWriteEventual( - Future Function(_DHTShortArrayHead) closure, + Future operateWriteEventual( + Future Function(_DHTShortArrayHead) closure, {Duration? timeout}) async { final timeoutTs = timeout == null ? null : Veilid.instance.now().offset(TimestampDuration.fromDuration(timeout)); - await _headMutex.protect(() async { + return _headMutex.protect(() async { late List oldLinkedRecords; late List oldIndex; late List oldFree; late List oldSeqs; + late T out; try { // Iterate until we have a successful element and head write @@ -140,20 +141,23 @@ class _DHTShortArrayHead { } } try { - if (await closure(this)) { - break; - } + out = await closure(this); + break; } on DHTExceptionTryAgain { - // + // Failed to write in closure resets state + _linkedRecords = List.of(oldLinkedRecords); + _index = List.of(oldIndex); + _free = List.of(oldFree); + _seqs = List.of(oldSeqs); + } on Exception { + // Failed to write in closure resets state + _linkedRecords = List.of(oldLinkedRecords); + _index = List.of(oldIndex); + _free = List.of(oldFree); + _seqs = List.of(oldSeqs); + rethrow; } - - // 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()); @@ -167,6 +171,7 @@ class _DHTShortArrayHead { rethrow; } + return out; }); } 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 index d002e35..665ea00 100644 --- 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 @@ -16,14 +16,14 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead _DHTShortArrayWrite._(super.head) : super._(); @override - Future tryAdd(Uint8List value) => tryInsert(_head.length, value); + Future add(Uint8List value) => insert(_head.length, value); @override - Future tryAddAll(List values) => - tryInsertAll(_head.length, values); + Future addAll(List values) => + insertAll(_head.length, values); @override - Future tryInsert(int pos, Uint8List value) async { + Future insert(int pos, Uint8List value) async { if (pos < 0 || pos > _head.length) { throw IndexError.withLength(pos, _head.length); } @@ -39,11 +39,13 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead _head.freeIndex(pos); } } - return true; + if (!success) { + throw DHTExceptionTryAgain(); + } } @override - Future tryInsertAll(int pos, List values) async { + Future insertAll(int pos, List values) async { if (pos < 0 || pos > _head.length) { throw IndexError.withLength(pos, _head.length); } @@ -94,8 +96,9 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead } } } - - return success; + if (!success) { + throw DHTExceptionTryAgain(); + } } @override diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_add.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_add.dart index e2b5ad7..dc79350 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_add.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_add.dart @@ -8,34 +8,34 @@ import '../../../veilid_support.dart'; // Add abstract class DHTAdd { /// Try to add an item to the DHT container. - /// 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. + /// Return if the element was successfully added, + /// Throws DHTExceptionTryAgain if the state changed before the element could + /// be added or a newer value was found on the network. /// Throws a StateError if the container exceeds its maximum size. - Future tryAdd(Uint8List value); + Future add(Uint8List value); /// Try to add a list of items to the DHT container. - /// Return true if the elements were successfully added, and false if the - /// state changed before the element could be added or a newer value was found - /// on the network. + /// Return if the elements were successfully added. + /// Throws DHTExceptionTryAgain if the state changed before the elements could + /// be added or a newer value was found on the network. /// Throws a StateError if the container exceeds its maximum size. - Future tryAddAll(List values); + Future addAll(List values); } extension DHTAddExt on DHTAdd { /// Convenience function: /// Like tryAddItem but also encodes the input value as JSON and parses the /// returned element as JSON - Future tryAddJson( + Future addJson( T newValue, ) => - tryAdd(jsonEncodeBytes(newValue)); + add(jsonEncodeBytes(newValue)); /// Convenience function: /// Like tryAddItem but also encodes the input value as a protobuf object /// and parses the returned element as a protobuf object - Future tryAddProtobuf( + Future addProtobuf( T newValue, ) => - tryAdd(newValue.writeToBuffer()); + add(newValue.writeToBuffer()); } diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_insert_remove.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_insert_remove.dart index fe44368..55967f7 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_insert_remove.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_insert_remove.dart @@ -8,22 +8,22 @@ import '../../../veilid_support.dart'; // Insert/Remove interface abstract class DHTInsertRemove { /// Try to insert an item as position 'pos' of the DHT container. - /// 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. + /// Return if the element was successfully inserted + /// Throws DHTExceptionTryAgain if the state changed before the element could + /// be inserted or a newer value was found on the network. /// Throws an IndexError if the position removed exceeds the length of /// the container. /// Throws a StateError if the container exceeds its maximum size. - Future tryInsert(int pos, Uint8List value); + Future insert(int pos, Uint8List value); /// Try to insert items at position 'pos' of the DHT container. - /// Return true if the elements were successfully inserted, and false if the - /// state changed before the elements could be inserted or a newer value was - /// found on the network. + /// Return if the elements were successfully inserted + /// Throws DHTExceptionTryAgain if the state changed before the elements could + /// be inserted or a newer value was found on the network. /// Throws an IndexError if the position removed exceeds the length of /// the container. /// Throws a StateError if the container exceeds its maximum size. - Future tryInsertAll(int pos, List values); + Future insertAll(int pos, List values); /// Remove an item at position 'pos' in the DHT container. /// If the remove was successful this returns: diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index eeab762..49c5325 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -7,9 +7,9 @@ environment: sdk: '>=3.2.0 <4.0.0' dependencies: - async_tools: ^0.1.1 + async_tools: ^0.1.2 bloc: ^8.1.4 - bloc_advanced_tools: ^0.1.1 + bloc_advanced_tools: ^0.1.2 charcode: ^1.3.1 collection: ^1.18.0 equatable: ^2.0.5 @@ -24,11 +24,11 @@ dependencies: # veilid: ^0.0.1 path: ../../../veilid/veilid-flutter -dependency_overrides: - async_tools: - path: ../../../dart_async_tools - bloc_advanced_tools: - path: ../../../bloc_advanced_tools +# dependency_overrides: +# async_tools: +# path: ../../../dart_async_tools +# bloc_advanced_tools: +# path: ../../../bloc_advanced_tools dev_dependencies: build_runner: ^2.4.10 diff --git a/pubspec.lock b/pubspec.lock index bfd5be1..03260fe 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -61,10 +61,10 @@ packages: dependency: "direct main" description: name: async_tools - sha256: e783ac6ed5645c86da34240389bb3a000fc5e3ae6589c6a482eb24ece7217681 + sha256: "72590010ed6c6f5cbd5d40e33834abc08a43da6a73ac3c3645517d53899b8684" url: "https://pub.dev" source: hosted - version: "0.1.1" + version: "0.1.2" awesome_extensions: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 56c7685..60c759a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: animated_theme_switcher: ^2.0.10 ansicolor: ^2.0.2 archive: ^3.6.1 - async_tools: ^0.1.1 + async_tools: ^0.1.2 awesome_extensions: ^2.0.16 badges: ^3.1.2 basic_utils: ^5.7.0 From b0d4e35c6fd83c70d603e6a74d65d0989939d961 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 10 Jun 2024 10:04:03 -0400 Subject: [PATCH 135/270] fix slow first message --- assets/i18n/en.json | 11 +++- .../account_repository.dart | 5 +- .../views/new_account_page.dart | 5 +- .../views/show_recovery_key_page.dart | 65 +++++++++++++++++++ .../views/switch_account_widget.dart | 35 ++++++++++ lib/account_manager/views/views.dart | 1 + .../reconciliation/author_input_queue.dart | 6 ++ lib/router/cubit/router_cubit.dart | 12 +++- .../src/dht_log/dht_log_spine.dart | 6 ++ 9 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 lib/account_manager/views/show_recovery_key_page.dart create mode 100644 lib/account_manager/views/switch_account_widget.dart diff --git a/assets/i18n/en.json b/assets/i18n/en.json index d2dcd8c..82f5db0 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -18,7 +18,7 @@ "lock_type_password": "password" }, "new_account_page": { - "titlebar": "Create a new account", + "titlebar": "Create A New Account", "header": "Account Profile", "create": "Create", "instructions": "This information will be shared with the people you invite to connect with you on VeilidChat.", @@ -26,12 +26,21 @@ "name": "Name", "pronouns": "Pronouns" }, + "show_recovery_key_page": { + "titlebar": "Save Recovery Key", + "instructions": "You must save this recovery key somewhere safe. This key is the ONLY way to recover your VeilidChat account in the event of a forgotton password or a lost, stolen, or compromised device.", + "instructions_options": "Here are some options for your recovery key:", + "instructions_print": "Print the recovery key and keep it somewhere safe", + "instructions_write": "View the recovery key and write it down on paper", + "instructions_send": "Send the recovery key to another app to save it" + }, "button": { "ok": "Ok", "cancel": "Cancel", "delete": "Delete", "accept": "Accept", "reject": "Reject", + "finish": "Finish", "waiting_for_network": "Waiting For Network" }, "toast": { diff --git a/lib/account_manager/repository/account_repository/account_repository.dart b/lib/account_manager/repository/account_repository/account_repository.dart index ac29913..5b593a3 100644 --- a/lib/account_manager/repository/account_repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository/account_repository.dart @@ -168,7 +168,8 @@ class AccountRepository { /// Creates a new super identity, an identity instance, an account associated /// with the identity instance, stores the account in the identity key and /// then logs into that account with no password set at this time - Future createWithNewSuperIdentity(NewProfileSpec newProfileSpec) async { + Future createWithNewSuperIdentity( + NewProfileSpec newProfileSpec) async { log.debug('Creating super identity'); final wsi = await WritableSuperIdentity.create(); try { @@ -181,6 +182,8 @@ class AccountRepository { final ok = await login( localAccount.superIdentity.recordKey, EncryptionKeyType.none, ''); assert(ok, 'login with none should never fail'); + + return wsi.superSecret; } on Exception catch (_) { await wsi.delete(); rethrow; diff --git a/lib/account_manager/views/new_account_page.dart b/lib/account_manager/views/new_account_page.dart index 7e15a32..34a17af 100644 --- a/lib/account_manager/views/new_account_page.dart +++ b/lib/account_manager/views/new_account_page.dart @@ -140,8 +140,11 @@ class NewAccountPageState extends State { final newProfileSpec = NewProfileSpec(name: name, pronouns: pronouns); - await AccountRepository.instance + final superSecret = await AccountRepository.instance .createWithNewSuperIdentity(newProfileSpec); + + GoRouterHelper(context).pushReplacement('/new_account/recovery_key', + extra: superSecret); } on Exception catch (e) { if (context.mounted) { await showErrorModal(context, translate('new_account_page.error'), diff --git a/lib/account_manager/views/show_recovery_key_page.dart b/lib/account_manager/views/show_recovery_key_page.dart new file mode 100644 index 0000000..4218d12 --- /dev/null +++ b/lib/account_manager/views/show_recovery_key_page.dart @@ -0,0 +1,65 @@ +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:form_builder_validators/form_builder_validators.dart'; +import 'package:go_router/go_router.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 '../account_manager.dart'; + +class ShowRecoveryKeyPage extends StatefulWidget { + const ShowRecoveryKeyPage({required SecretKey secretKey, super.key}) + : _secretKey = secretKey; + + @override + ShowRecoveryKeyPageState createState() => ShowRecoveryKeyPageState(); + + final SecretKey _secretKey; +} + +class ShowRecoveryKeyPageState extends State { + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + await changeWindowSetup( + TitleBarStyle.normal, OrientationCapability.portraitOnly); + }); + } + + @override + // ignore: prefer_expression_function_bodies + Widget build(BuildContext context) { + final secretKey = widget._secretKey; + + return Scaffold( + // resizeToAvoidBottomInset: false, + appBar: DefaultAppBar( + title: Text(translate('show_recovery_key_page.titlebar')), + actions: [ + const SignalStrengthMeterWidget(), + IconButton( + icon: const Icon(Icons.settings), + tooltip: translate('app_bar.settings_tooltip'), + onPressed: () async { + await GoRouterHelper(context).push('/settings'); + }) + ]), + body: Column(children: [ + Text('ASS: $secretKey'), + ElevatedButton( + onPressed: () { + GoRouterHelper(context).go('/'); + }, + child: Text(translate('button.finish'))) + ]).paddingSymmetric(horizontal: 24, vertical: 8)); + } +} diff --git a/lib/account_manager/views/switch_account_widget.dart b/lib/account_manager/views/switch_account_widget.dart new file mode 100644 index 0000000..82c220d --- /dev/null +++ b/lib/account_manager/views/switch_account_widget.dart @@ -0,0 +1,35 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:flutter/material.dart'; + +import '../../proto/proto.dart' as proto; +import '../../theme/theme.dart'; +import '../account_manager.dart'; + +class SwitchAccountWidget extends StatelessWidget { + const SwitchAccountWidget({ + super.key, + }); + // + + @override + // ignore: prefer_expression_function_bodies + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final textTheme = theme.textTheme; + + final accountRepo = AccountRepository.instance; + final localAccounts = accountRepo.getLocalAccounts(); + for (final la in localAccounts) { + // + } + + return DecoratedBox( + decoration: ShapeDecoration( + color: scale.primaryScale.border, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), + child: Column(children: []), + ); + } +} diff --git a/lib/account_manager/views/views.dart b/lib/account_manager/views/views.dart index 2acc537..4214e05 100644 --- a/lib/account_manager/views/views.dart +++ b/lib/account_manager/views/views.dart @@ -1,2 +1,3 @@ export 'new_account_page.dart'; export 'profile_widget.dart'; +export 'show_recovery_key_page.dart'; diff --git a/lib/chat/cubits/reconciliation/author_input_queue.dart b/lib/chat/cubits/reconciliation/author_input_queue.dart index d7be3eb..b15d92c 100644 --- a/lib/chat/cubits/reconciliation/author_input_queue.dart +++ b/lib/chat/cubits/reconciliation/author_input_queue.dart @@ -3,6 +3,7 @@ import 'package:veilid_support/veilid_support.dart'; import '../../../proto/proto.dart' as proto; +import '../../../tools/tools.dart'; import 'author_input_source.dart'; import 'message_integrity.dart'; import 'output_position.dart'; @@ -84,6 +85,8 @@ class AuthorInputQueue { if (_lastMessage != null) { // Ensure the timestamp is not moving backward if (nextMessage.value.timestamp < _lastMessage!.timestamp) { + log.warning('timestamp backward: ${nextMessage.value.timestamp}' + ' < ${_lastMessage!.timestamp}'); continue; } } @@ -91,11 +94,14 @@ class AuthorInputQueue { // Verify the id chain for the message final matchId = await _messageIntegrity.generateMessageId(_lastMessage); if (matchId.compare(nextMessage.value.idBytes) != 0) { + log.warning( + 'id chain invalid: $matchId != ${nextMessage.value.idBytes}'); continue; } // Verify the signature for the message if (!await _messageIntegrity.verifyMessage(nextMessage.value)) { + log.warning('invalid message signature: ${nextMessage.value}'); continue; } diff --git a/lib/router/cubit/router_cubit.dart b/lib/router/cubit/router_cubit.dart index f30a617..b69037d 100644 --- a/lib/router/cubit/router_cubit.dart +++ b/lib/router/cubit/router_cubit.dart @@ -7,6 +7,7 @@ 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'; +import 'package:veilid_support/veilid_support.dart'; import '../../../account_manager/account_manager.dart'; import '../../layout/layout.dart'; @@ -83,6 +84,11 @@ class RouterCubit extends Cubit { path: '/new_account', builder: (context, state) => const NewAccountPage(), ), + GoRoute( + path: '/new_account/recovery_key', + builder: (context, state) => + ShowRecoveryKeyPage(secretKey: state.extra! as SecretKey), + ), GoRoute( path: '/settings', builder: (context, state) => const SettingsPage(), @@ -98,8 +104,6 @@ class RouterCubit extends Cubit { // No matter where we are, if there's not switch (goRouterState.matchedLocation) { - case '/new_account': - return state.hasAnyAccount ? '/' : null; case '/': if (!state.hasAnyAccount) { return '/new_account'; @@ -130,6 +134,10 @@ class RouterCubit extends Cubit { return '/'; } return null; + case '/new_account': + return null; + case '/new_account/recovery_key': + return null; case '/settings': return null; case '/developer': diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index 3105fa8..5d59190 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -71,6 +71,12 @@ class _DHTLogSpine { // Write new spine head record to the network await spine.operate((spine) async { + // Write first empty subkey + final subkeyData = _makeEmptySubkey(); + final existingSubkeyData = + await spineRecord.tryWriteBytes(subkeyData, subkey: 1); + assert(existingSubkeyData == null, 'Should never conflict on create'); + final success = await spine.writeSpineHead(); assert(success, 'false return should never happen on create'); }); From e40bd717f83b759378f6724d423aa681dd3d921a Mon Sep 17 00:00:00 2001 From: TC Johnson Date: Mon, 10 Jun 2024 20:18:38 -0500 Subject: [PATCH 136/270] =?UTF-8?q?Version=20update:=20v0.2.0=20=E2=86=92?= =?UTF-8?q?=20v0.2.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 85f2f9d..ca34235 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.2.0+10 +current_version = 0.2.1+0 commit = False tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)\+(?P\d+) diff --git a/pubspec.yaml b/pubspec.yaml index 60c759a..a77991a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: veilidchat description: VeilidChat publish_to: 'none' -version: 0.2.0+10 +version: 0.2.1+11 environment: sdk: '>=3.2.0 <4.0.0' From 87bb1657c72c860a9ddc4c73a4728f6cccb6d463 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 11 Jun 2024 21:27:20 -0400 Subject: [PATCH 137/270] add multiple accounts menu --- assets/i18n/en.json | 5 +- .../cubits/account_record_cubit.dart | 4 +- .../account_records_bloc_map_cubit.dart | 36 +++ .../cubits/active_local_account_cubit.dart | 2 +- lib/account_manager/cubits/cubits.dart | 1 + .../cubits/local_accounts_cubit.dart | 2 +- .../cubits/user_logins_cubit.dart | 19 +- .../account_repository.dart | 6 +- .../repository/repository.dart | 2 +- .../views/switch_account_widget.dart | 35 --- lib/app.dart | 6 +- lib/chat_list/cubits/chat_list_cubit.dart | 2 + lib/layout/home/drawer_menu/drawer_menu.dart | 262 ++++++++++++++++++ .../home/drawer_menu/menu_item_widget.dart | 130 +++++++++ lib/layout/home/home.dart | 1 + .../home_account_ready_main.dart | 8 +- lib/layout/home/home_shell.dart | 60 +++- lib/main.dart | 4 + lib/tools/package_info.dart | 14 + lib/tools/tools.dart | 2 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Podfile.lock | 6 + packages/veilid_support/pubspec.lock | 18 +- pubspec.lock | 24 ++ pubspec.yaml | 2 + 25 files changed, 583 insertions(+), 70 deletions(-) create mode 100644 lib/account_manager/cubits/account_records_bloc_map_cubit.dart rename lib/account_manager/repository/{account_repository => }/account_repository.dart (99%) delete mode 100644 lib/account_manager/views/switch_account_widget.dart create mode 100644 lib/layout/home/drawer_menu/drawer_menu.dart create mode 100644 lib/layout/home/drawer_menu/menu_item_widget.dart create mode 100644 lib/tools/package_info.dart diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 82f5db0..5773265 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -2,8 +2,9 @@ "app": { "title": "VeilidChat" }, - "app_bar": { - "settings_tooltip": "Settings" + "menu": { + "settings_tooltip": "Settings", + "add_account_tooltip": "Add Account" }, "pager": { "chats": "Chats", diff --git a/lib/account_manager/cubits/account_record_cubit.dart b/lib/account_manager/cubits/account_record_cubit.dart index 60ddd88..8ea1b5d 100644 --- a/lib/account_manager/cubits/account_record_cubit.dart +++ b/lib/account_manager/cubits/account_record_cubit.dart @@ -4,7 +4,9 @@ import 'package:veilid_support/veilid_support.dart'; import '../../proto/proto.dart' as proto; -class AccountRecordCubit extends DefaultDHTRecordCubit { +typedef AccountRecordState = proto.Account; + +class AccountRecordCubit extends DefaultDHTRecordCubit { AccountRecordCubit({ required super.open, }) : super(decodeState: proto.Account.fromBuffer); diff --git a/lib/account_manager/cubits/account_records_bloc_map_cubit.dart b/lib/account_manager/cubits/account_records_bloc_map_cubit.dart new file mode 100644 index 0000000..be9b581 --- /dev/null +++ b/lib/account_manager/cubits/account_records_bloc_map_cubit.dart @@ -0,0 +1,36 @@ +import 'package:async_tools/async_tools.dart'; +import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../account_manager/account_manager.dart'; + +typedef AccountRecordsBlocMapState + = BlocMapState>; + +// Map of the logged in user accounts to their account information +class AccountRecordsBlocMapCubit extends BlocMapCubit, AccountRecordCubit> + with StateMapFollower { + AccountRecordsBlocMapCubit(AccountRepository accountRepository) + : _accountRepository = accountRepository; + + // Add account record cubit + Future _addAccountRecordCubit({required UserLogin userLogin}) async => + add(() => MapEntry( + userLogin.superIdentityRecordKey, + AccountRecordCubit( + open: () => _accountRepository.openAccountRecord(userLogin)))); + + /// StateFollower ///////////////////////// + + @override + Future removeFromState(TypedKey key) => remove(key); + + @override + Future updateState(TypedKey key, UserLogin value) async { + await _addAccountRecordCubit(userLogin: value); + } + + //////////////////////////////////////////////////////////////////////////// + final AccountRepository _accountRepository; +} diff --git a/lib/account_manager/cubits/active_local_account_cubit.dart b/lib/account_manager/cubits/active_local_account_cubit.dart index 843cc59..58a9cb8 100644 --- a/lib/account_manager/cubits/active_local_account_cubit.dart +++ b/lib/account_manager/cubits/active_local_account_cubit.dart @@ -3,7 +3,7 @@ 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.dart'; class ActiveLocalAccountCubit extends Cubit { ActiveLocalAccountCubit(AccountRepository accountRepository) diff --git a/lib/account_manager/cubits/cubits.dart b/lib/account_manager/cubits/cubits.dart index 6d2875d..ee9d91b 100644 --- a/lib/account_manager/cubits/cubits.dart +++ b/lib/account_manager/cubits/cubits.dart @@ -1,4 +1,5 @@ export 'account_record_cubit.dart'; +export 'account_records_bloc_map_cubit.dart'; export 'active_local_account_cubit.dart'; export 'local_accounts_cubit.dart'; export 'user_logins_cubit.dart'; diff --git a/lib/account_manager/cubits/local_accounts_cubit.dart b/lib/account_manager/cubits/local_accounts_cubit.dart index 484bdbc..a324602 100644 --- a/lib/account_manager/cubits/local_accounts_cubit.dart +++ b/lib/account_manager/cubits/local_accounts_cubit.dart @@ -4,7 +4,7 @@ import 'package:bloc/bloc.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import '../models/models.dart'; -import '../repository/account_repository/account_repository.dart'; +import '../repository/account_repository.dart'; class LocalAccountsCubit extends Cubit> { LocalAccountsCubit(AccountRepository accountRepository) diff --git a/lib/account_manager/cubits/user_logins_cubit.dart b/lib/account_manager/cubits/user_logins_cubit.dart index 9fa6974..75d6ad1 100644 --- a/lib/account_manager/cubits/user_logins_cubit.dart +++ b/lib/account_manager/cubits/user_logins_cubit.dart @@ -1,12 +1,17 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; +import 'package:bloc_advanced_tools/bloc_advanced_tools.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 '../repository/account_repository.dart'; -class UserLoginsCubit extends Cubit> { +typedef UserLoginsState = IList; + +class UserLoginsCubit extends Cubit + with StateMapFollowable { UserLoginsCubit(AccountRepository accountRepository) : _accountRepository = accountRepository, super(accountRepository.getUserLogins()) { @@ -30,6 +35,16 @@ class UserLoginsCubit extends Cubit> { await _accountRepositorySubscription.cancel(); } + /// StateMapFollowable ///////////////////////// + @override + IMap getStateMap(UserLoginsState state) { + final stateValue = state; + return IMap.fromIterable(stateValue, + keyMapper: (e) => e.superIdentityRecordKey, valueMapper: (e) => e); + } + + //////////////////////////////////////////////////////////////////////////// + final AccountRepository _accountRepository; late final StreamSubscription _accountRepositorySubscription; diff --git a/lib/account_manager/repository/account_repository/account_repository.dart b/lib/account_manager/repository/account_repository.dart similarity index 99% rename from lib/account_manager/repository/account_repository/account_repository.dart rename to lib/account_manager/repository/account_repository.dart index 5b593a3..61ac5a3 100644 --- a/lib/account_manager/repository/account_repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository.dart @@ -3,9 +3,9 @@ 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 '../../../proto/proto.dart' as proto; +import '../../tools/tools.dart'; +import '../models/models.dart'; const String veilidChatAccountKey = 'com.veilid.veilidchat'; diff --git a/lib/account_manager/repository/repository.dart b/lib/account_manager/repository/repository.dart index 9d1b9fe..74bf9f8 100644 --- a/lib/account_manager/repository/repository.dart +++ b/lib/account_manager/repository/repository.dart @@ -1 +1 @@ -export 'account_repository/account_repository.dart'; +export 'account_repository.dart'; diff --git a/lib/account_manager/views/switch_account_widget.dart b/lib/account_manager/views/switch_account_widget.dart deleted file mode 100644 index 82c220d..0000000 --- a/lib/account_manager/views/switch_account_widget.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:awesome_extensions/awesome_extensions.dart'; -import 'package:flutter/material.dart'; - -import '../../proto/proto.dart' as proto; -import '../../theme/theme.dart'; -import '../account_manager.dart'; - -class SwitchAccountWidget extends StatelessWidget { - const SwitchAccountWidget({ - super.key, - }); - // - - @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - final theme = Theme.of(context); - final scale = theme.extension()!; - final textTheme = theme.textTheme; - - final accountRepo = AccountRepository.instance; - final localAccounts = accountRepo.getLocalAccounts(); - for (final la in localAccounts) { - // - } - - return DecoratedBox( - decoration: ShapeDecoration( - color: scale.primaryScale.border, - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), - child: Column(children: []), - ); - } -} diff --git a/lib/app.dart b/lib/app.dart index aaa0190..e10ce32 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -129,7 +129,11 @@ class VeilidChatApp extends StatelessWidget { BlocProvider( create: (context) => PreferencesCubit(PreferencesRepository.instance), - ) + ), + BlocProvider( + create: (context) => + AccountRecordsBlocMapCubit(AccountRepository.instance) + ..follow(context.read())), ], child: BackgroundTicker( child: _buildShortcuts( diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index 0c36f52..3ea368b 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -140,5 +140,7 @@ class ChatListCubit extends DHTShortArrayCubit valueMapper: (e) => e.value); } + //////////////////////////////////////////////////////////////////////////// + final ActiveChatCubit activeChatCubit; } diff --git a/lib/layout/home/drawer_menu/drawer_menu.dart b/lib/layout/home/drawer_menu/drawer_menu.dart new file mode 100644 index 0000000..b037c84 --- /dev/null +++ b/lib/layout/home/drawer_menu/drawer_menu.dart @@ -0,0 +1,262 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.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 '../../../theme/theme.dart'; +import '../../../tools/tools.dart'; +import 'menu_item_widget.dart'; + +class DrawerMenu extends StatefulWidget { + const DrawerMenu({super.key}); + + @override + State createState() => _DrawerMenuState(); +} + +class _DrawerMenuState extends State { + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + void _doLoginClick(TypedKey superIdentityRecordKey) { + // + } + + void _doEditClick(TypedKey superIdentityRecordKey) { + // + } + + Widget _wrapInBox({required Widget child, required Color color}) => + DecoratedBox( + decoration: ShapeDecoration( + color: color, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16))), + child: child); + + Widget _makeAccountWidget( + {required String name, + required bool loggedIn, + required void Function() clickHandler}) { + final theme = Theme.of(context); + final scale = theme.extension()!.tertiaryScale; + final abbrev = name.split(' ').map((s) => s.isEmpty ? '' : s[0]).join(); + late final String shortname; + if (abbrev.length >= 3) { + shortname = abbrev[0] + abbrev[1] + abbrev[abbrev.length - 1]; + } else { + shortname = abbrev; + } + + final avatar = AvatarImage( + size: 32, + backgroundColor: loggedIn ? scale.primary : scale.elementBackground, + foregroundColor: loggedIn ? scale.primaryText : scale.subtleText, + child: Text(shortname, style: theme.textTheme.titleLarge)); + + return MenuItemWidget( + title: name, + headerWidget: avatar, + titleStyle: theme.textTheme.titleLarge!, + foregroundColor: scale.primary, + backgroundColor: scale.elementBackground, + backgroundHoverColor: scale.hoverElementBackground, + backgroundFocusColor: scale.activeElementBackground, + borderColor: scale.border, + borderHoverColor: scale.hoverBorder, + borderFocusColor: scale.primary, + footerButtonIcon: loggedIn ? Icons.edit_outlined : Icons.login_outlined, + footerCallback: clickHandler, + footerButtonIconColor: scale.border, + footerButtonIconHoverColor: scale.hoverElementBackground, + footerButtonIconFocusColor: scale.activeElementBackground, + ); + } + + Widget _getAccountList( + {required TypedKey? activeLocalAccount, + required AccountRecordsBlocMapState accountRecords}) { + final theme = Theme.of(context); + final scale = theme.extension()!; + + final accountRepo = AccountRepository.instance; + final localAccounts = accountRepo.getLocalAccounts(); + //final userLogins = accountRepo.getUserLogins(); + + final loggedInAccounts = []; + final loggedOutAccounts = []; + + for (final la in localAccounts) { + final superIdentityRecordKey = la.superIdentity.recordKey; + + // See if this account is logged in + final acctRecord = accountRecords.get(superIdentityRecordKey); + if (acctRecord != null) { + // Account is logged in + final loggedInAccount = acctRecord.when( + data: (value) => _makeAccountWidget( + name: value.profile.name, + loggedIn: true, + clickHandler: () { + _doEditClick(superIdentityRecordKey); + }), + loading: () => _wrapInBox( + child: buildProgressIndicator(), + color: scale.grayScale.subtleBorder), + error: (err, st) => _wrapInBox( + child: errorPage(err, st), color: scale.errorScale.subtleBorder), + ); + loggedInAccounts.add(loggedInAccount); + } else { + // Account is not logged in + final loggedOutAccount = _makeAccountWidget( + name: la.name, + loggedIn: false, + clickHandler: () { + _doLoginClick(superIdentityRecordKey); + }); + loggedOutAccounts.add(loggedOutAccount); + } + } + + // Assemble main menu + final mainMenu = [...loggedInAccounts, ...loggedOutAccounts]; + + // Return main menu widgets + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [...mainMenu], + ); + } + + Widget _getButton( + {required Icon icon, + required ScaleColor scale, + required String tooltip, + required void Function()? onPressed}) => + IconButton( + icon: icon, + color: scale.hoverBorder, + constraints: const BoxConstraints.expand(height: 64, width: 64), + style: ButtonStyle( + backgroundColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.hovered)) { + return scale.hoverElementBackground; + } + if (states.contains(WidgetState.focused)) { + return scale.activeElementBackground; + } + return scale.elementBackground; + }), shape: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.hovered)) { + return RoundedRectangleBorder( + side: BorderSide(color: scale.hoverBorder), + borderRadius: const BorderRadius.all(Radius.circular(16))); + } + if (states.contains(WidgetState.focused)) { + return RoundedRectangleBorder( + side: BorderSide(color: scale.primary), + borderRadius: const BorderRadius.all(Radius.circular(16))); + } + return RoundedRectangleBorder( + side: BorderSide(color: scale.border), + borderRadius: const BorderRadius.all(Radius.circular(16))); + })), + tooltip: tooltip, + onPressed: onPressed); + + Widget _getBottomButtons() { + final theme = Theme.of(context); + final scale = theme.extension()!; + + final settingsButton = _getButton( + icon: const Icon(Icons.settings), + tooltip: translate('menu.settings_tooltip'), + scale: scale.tertiaryScale, + onPressed: () async { + await GoRouterHelper(context).push('/settings'); + }).paddingLTRB(0, 0, 16, 0); + + final addButton = _getButton( + icon: const Icon(Icons.add), + tooltip: translate('menu.add_account_tooltip'), + scale: scale.tertiaryScale, + onPressed: () async { + await GoRouterHelper(context).push('/new_account'); + }).paddingLTRB(0, 0, 16, 0); + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [settingsButton, addButton]).paddingLTRB(0, 16, 0, 0); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + //final textTheme = theme.textTheme; + final accountRecords = context.watch().state; + final activeLocalAccount = context.watch().state; + final gradient = LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + scale.tertiaryScale.hoverElementBackground, + scale.tertiaryScale.subtleBackground, + ]); + + return DecoratedBox( + decoration: ShapeDecoration( + shadows: [ + BoxShadow( + color: scale.tertiaryScale.appBackground, + blurRadius: 6, + offset: const Offset( + 0, + 3, + ), + ), + ], + gradient: gradient, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: Radius.circular(16), + bottomRight: Radius.circular(16)))), + child: Column(children: [ + FittedBox( + fit: BoxFit.scaleDown, + child: Row(children: [ + SvgPicture.asset( + height: 48, + 'assets/images/icon.svg', + ).paddingLTRB(0, 0, 16, 0), + SvgPicture.asset( + height: 48, + 'assets/images/title.svg', + ), + ])), + const Spacer(), + _getAccountList( + activeLocalAccount: activeLocalAccount, + accountRecords: accountRecords), + _getBottomButtons(), + const Spacer(), + Text('Version $packageInfoVersion', + style: theme.textTheme.labelMedium! + .copyWith(color: scale.tertiaryScale.hoverBorder)) + ]).paddingAll(16), + ); + } +} diff --git a/lib/layout/home/drawer_menu/menu_item_widget.dart b/lib/layout/home/drawer_menu/menu_item_widget.dart new file mode 100644 index 0000000..68fb178 --- /dev/null +++ b/lib/layout/home/drawer_menu/menu_item_widget.dart @@ -0,0 +1,130 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +class MenuItemWidget extends StatelessWidget { + const MenuItemWidget({ + required this.title, + required this.titleStyle, + required this.foregroundColor, + this.headerWidget, + this.widthBox, + this.callback, + this.backgroundColor, + this.backgroundHoverColor, + this.backgroundFocusColor, + this.borderColor, + this.borderHoverColor, + this.borderFocusColor, + this.footerButtonIcon, + this.footerButtonIconColor, + this.footerButtonIconHoverColor, + this.footerButtonIconFocusColor, + this.footerCallback, + super.key, + }); + + @override + Widget build(BuildContext context) => TextButton( + onPressed: () => callback, + style: TextButton.styleFrom(foregroundColor: foregroundColor).copyWith( + backgroundColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.hovered)) { + return backgroundHoverColor; + } + if (states.contains(WidgetState.focused)) { + return backgroundFocusColor; + } + return backgroundColor; + }), + side: WidgetStateBorderSide.resolveWith((states) { + if (states.contains(WidgetState.hovered)) { + return borderColor != null + ? BorderSide(color: borderHoverColor!) + : null; + } + if (states.contains(WidgetState.focused)) { + return borderColor != null + ? BorderSide(color: borderFocusColor!) + : null; + } + return borderColor != null ? BorderSide(color: borderColor!) : null; + }), + shape: WidgetStateProperty.all( + RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)))), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (headerWidget != null) headerWidget!, + if (widthBox != null) widthBox!, + Expanded( + child: FittedBox( + alignment: Alignment.centerLeft, + fit: BoxFit.scaleDown, + child: Text( + title, + style: titleStyle, + ).paddingAll(8)), + ), + if (footerButtonIcon != null) + IconButton.outlined( + color: footerButtonIconColor, + focusColor: footerButtonIconFocusColor, + hoverColor: footerButtonIconHoverColor, + icon: Icon( + footerButtonIcon, + size: 24, + ), + onPressed: footerCallback), + ], + ), + )); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('textStyle', titleStyle)) + ..add(ObjectFlagProperty.has('callback', callback)) + ..add(DiagnosticsProperty('foregroundColor', foregroundColor)) + ..add(StringProperty('title', title)) + ..add( + DiagnosticsProperty('footerButtonIcon', footerButtonIcon)) + ..add(ObjectFlagProperty.has( + 'footerCallback', footerCallback)) + ..add(ColorProperty('footerButtonIconColor', footerButtonIconColor)) + ..add(ColorProperty( + 'footerButtonIconHoverColor', footerButtonIconHoverColor)) + ..add(ColorProperty( + 'footerButtonIconFocusColor', footerButtonIconFocusColor)) + ..add(ColorProperty('backgroundColor', backgroundColor)) + ..add(ColorProperty('backgroundHoverColor', backgroundHoverColor)) + ..add(ColorProperty('backgroundFocusColor', backgroundFocusColor)) + ..add(ColorProperty('borderColor', borderColor)) + ..add(ColorProperty('borderHoverColor', borderHoverColor)) + ..add(ColorProperty('borderFocusColor', borderFocusColor)); + } + + //////////////////////////////////////////////////////////////////////////// + + final String title; + final Widget? headerWidget; + final Widget? widthBox; + final TextStyle titleStyle; + final Color foregroundColor; + final void Function()? callback; + final IconData? footerButtonIcon; + final void Function()? footerCallback; + final Color? backgroundColor; + final Color? backgroundHoverColor; + final Color? backgroundFocusColor; + final Color? borderColor; + final Color? borderHoverColor; + final Color? borderFocusColor; + final Color? footerButtonIconColor; + final Color? footerButtonIconHoverColor; + final Color? footerButtonIconFocusColor; +} diff --git a/lib/layout/home/home.dart b/lib/layout/home/home.dart index 5b1b3d1..68e4042 100644 --- a/lib/layout/home/home.dart +++ b/lib/layout/home/home.dart @@ -1,3 +1,4 @@ +export 'drawer_menu/drawer_menu.dart'; export 'home_account_invalid.dart'; export 'home_account_locked.dart'; export 'home_account_missing.dart'; 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 9fec3ce..7fafc7f 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 @@ -2,7 +2,7 @@ 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 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart'; import '../../../account_manager/account_manager.dart'; import '../../../chat/chat.dart'; @@ -36,7 +36,7 @@ class _HomeAccountReadyMainState extends State { return Column(children: [ Row(children: [ IconButton( - icon: const Icon(Icons.settings), + icon: const Icon(Icons.menu), color: scale.secondaryScale.borderText, constraints: const BoxConstraints.expand(height: 64, width: 64), style: ButtonStyle( @@ -46,7 +46,9 @@ class _HomeAccountReadyMainState extends State { borderRadius: BorderRadius.all(Radius.circular(16))))), tooltip: translate('app_bar.settings_tooltip'), onPressed: () async { - await GoRouterHelper(context).push('/settings'); + final ctrl = context.read(); + await ctrl.toggle?.call(); + //await GoRouterHelper(context).push('/settings'); }).paddingLTRB(0, 0, 8, 0), asyncValueBuilder(account, (_, account) => ProfileWidget(profile: account.profile)) diff --git a/lib/layout/home/home_shell.dart b/lib/layout/home/home_shell.dart index 8851730..422c0dd 100644 --- a/lib/layout/home/home_shell.dart +++ b/lib/layout/home/home_shell.dart @@ -1,9 +1,14 @@ +import 'dart:math'; + +import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart'; import 'package:provider/provider.dart'; import '../../account_manager/account_manager.dart'; import '../../theme/theme.dart'; +import 'drawer_menu/drawer_menu.dart'; import 'home_account_invalid.dart'; import 'home_account_locked.dart'; import 'home_account_missing.dart'; @@ -31,7 +36,7 @@ class HomeShellState extends State { Widget buildWithLogin(BuildContext context) { final activeLocalAccount = context.watch().state; - + final accountRecordsCubit = context.watch(); if (activeLocalAccount == null) { // If no logged in user is active, show the loading panel return const HomeNoActive(); @@ -39,6 +44,11 @@ class HomeShellState extends State { final accountInfo = AccountRepository.instance.getAccountInfo(activeLocalAccount); + final activeCubit = + accountRecordsCubit.tryOperate(activeLocalAccount, closure: (c) => c); + if (activeCubit == null) { + return waitingPage(); + } switch (accountInfo.status) { case AccountInfoStatus.noAccount: @@ -48,14 +58,13 @@ class HomeShellState extends State { case AccountInfoStatus.accountLocked: return const HomeAccountLocked(); case AccountInfoStatus.accountReady: - return Provider.value( + return MultiProvider(providers: [ + Provider.value( value: accountInfo.activeAccountInfo!, - child: BlocProvider( - create: (context) => AccountRecordCubit( - open: () async => AccountRepository.instance - .openAccountRecord( - accountInfo.activeAccountInfo!.userLogin)), - child: widget.accountReadyBuilder)); + ), + Provider.value(value: activeCubit), + Provider.value(value: _zoomDrawerController), + ], child: widget.accountReadyBuilder); } } @@ -64,11 +73,38 @@ class HomeShellState extends State { final theme = Theme.of(context); final scale = theme.extension()!; - // XXX: eventually write account switcher here + final gradient = LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + scale.tertiaryScale.subtleBackground, + scale.tertiaryScale.appBackground, + ]); + return SafeArea( child: DecoratedBox( - decoration: BoxDecoration( - color: scale.primaryScale.activeElementBackground), - child: buildWithLogin(context))); + decoration: BoxDecoration(gradient: gradient), + child: ZoomDrawer( + controller: _zoomDrawerController, + //menuBackgroundColor: Colors.transparent, + menuScreen: const DrawerMenu(), + mainScreen: DecoratedBox( + decoration: BoxDecoration( + color: scale.primaryScale.activeElementBackground), + child: buildWithLogin(context)), + borderRadius: 24, + showShadow: true, + angle: 0, + drawerShadowsBackgroundColor: theme.shadowColor, + mainScreenOverlayColor: theme.shadowColor.withAlpha(0x3F), + openCurve: Curves.fastEaseInToSlowEaseOut, + // duration: const Duration(milliseconds: 250), + // reverseDuration: const Duration(milliseconds: 250), + menuScreenTapClose: true, + mainScreenScale: .25, + slideWidth: min(360, MediaQuery.of(context).size.width * 0.9), + ))); } + + final ZoomDrawerController _zoomDrawerController = ZoomDrawerController(); } diff --git a/lib/main.dart b/lib/main.dart index d8bd6df..4edaa5b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:intl/date_symbol_data_local.dart'; + import 'package:stack_trace/stack_trace.dart'; import 'app.dart'; @@ -45,6 +46,9 @@ void main() async { fallbackLocale: 'en_US', supportedLocales: ['en_US']); await initializeDateFormatting(); + // Get package info + await initPackageInfo(); + // Run the app // Hot reloads will only restart this part, not Veilid runApp(LocalizedApp(localizationDelegate, diff --git a/lib/tools/package_info.dart b/lib/tools/package_info.dart new file mode 100644 index 0000000..7acd109 --- /dev/null +++ b/lib/tools/package_info.dart @@ -0,0 +1,14 @@ +import 'package:package_info_plus/package_info_plus.dart'; + +String packageInfoAppName = ''; +String packageInfoPackageName = ''; +String packageInfoVersion = ''; +String packageInfoBuildNumber = ''; + +Future initPackageInfo() async { + final packageInfo = await PackageInfo.fromPlatform(); + packageInfoAppName = packageInfo.appName; + packageInfoPackageName = packageInfo.packageName; + packageInfoVersion = packageInfo.version; + packageInfoBuildNumber = packageInfo.buildNumber; +} diff --git a/lib/tools/tools.dart b/lib/tools/tools.dart index 6b48001..b61570e 100644 --- a/lib/tools/tools.dart +++ b/lib/tools/tools.dart @@ -1,8 +1,10 @@ + export 'animations.dart'; export 'enter_password.dart'; export 'enter_pin.dart'; export 'loggy.dart'; export 'misc.dart'; +export 'package_info.dart'; export 'phono_byte.dart'; export 'pop_control.dart'; export 'responsive.dart'; diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index fa5dd07..b888e6d 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import mobile_scanner +import package_info_plus import pasteboard import path_provider_foundation import screen_retriever @@ -19,6 +20,7 @@ import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index faa2836..98cf433 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -2,6 +2,8 @@ PODS: - FlutterMacOS (1.0.0) - mobile_scanner (5.1.1): - FlutterMacOS + - package_info_plus (0.0.1): + - FlutterMacOS - pasteboard (0.0.1): - FlutterMacOS - path_provider_foundation (0.0.1): @@ -29,6 +31,7 @@ PODS: DEPENDENCIES: - FlutterMacOS (from `Flutter/ephemeral`) - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`) + - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) @@ -45,6 +48,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral mobile_scanner: :path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos + package_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos pasteboard: :path: Flutter/ephemeral/.symlinks/plugins/pasteboard/macos path_provider_foundation: @@ -69,6 +74,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 mobile_scanner: 1efac1e53c294b24e3bb55bcc7f4deee0233a86b + package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index f74ee28..e3bc5f9 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -36,10 +36,11 @@ packages: async_tools: dependency: "direct main" description: - path: "../../../dart_async_tools" - relative: true - source: path - version: "0.1.1" + name: async_tools + sha256: "72590010ed6c6f5cbd5d40e33834abc08a43da6a73ac3c3645517d53899b8684" + url: "https://pub.dev" + source: hosted + version: "0.1.2" bloc: dependency: "direct main" description: @@ -51,10 +52,11 @@ packages: bloc_advanced_tools: dependency: "direct main" description: - path: "../../../bloc_advanced_tools" - relative: true - source: path - version: "0.1.1" + name: bloc_advanced_tools + sha256: "0cf9b3a73a67addfe22ec3f97a1ac240f6ad53870d6b21a980260f390d7901cd" + url: "https://pub.dev" + source: hosted + version: "0.1.2" boolean_selector: dependency: transitive description: diff --git a/pubspec.lock b/pubspec.lock index 03260fe..36fd6d0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -585,6 +585,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_zoom_drawer: + dependency: "direct main" + description: + name: flutter_zoom_drawer + sha256: "5a3708548868463fb36e0e3171761ab7cb513df88d2f14053802812d2e855060" + url: "https://pub.dev" + source: hosted + version: "3.2.0" form_builder_validators: dependency: "direct main" description: @@ -857,6 +865,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0 + url: "https://pub.dev" + source: hosted + version: "8.0.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e + url: "https://pub.dev" + source: hosted + version: "3.0.0" pasteboard: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 60c759a..f27a839 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,6 +45,7 @@ dependencies: flutter_spinkit: ^5.2.1 flutter_svg: ^2.0.10+1 flutter_translate: ^4.1.0 + flutter_zoom_drawer: ^3.2.0 form_builder_validators: ^10.0.1 freezed_annotation: ^2.4.1 go_router: ^14.1.4 @@ -56,6 +57,7 @@ dependencies: meta: ^1.12.0 mobile_scanner: ^5.1.1 motion_toast: ^2.10.0 + package_info_plus: ^8.0.0 pasteboard: ^0.2.0 path: ^1.9.0 path_provider: ^2.1.3 From 56d65442f439fbdc13aa14e50030ff658cb8c4ec Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 13 Jun 2024 14:52:34 -0400 Subject: [PATCH 138/270] refactor and cleanup in prep for profile changing --- assets/i18n/en.json | 16 ++ .../cubits/account_record_cubit.dart | 26 ++- .../account_records_bloc_map_cubit.dart | 11 +- ...it.dart => active_account_info_cubit.dart} | 14 +- lib/account_manager/cubits/cubits.dart | 2 +- lib/account_manager/models/account_info.dart | 6 +- lib/account_manager/models/models.dart | 2 +- ...t_info.dart => unlocked_account_info.dart} | 4 +- .../repository/account_repository.dart | 215 +++++++++--------- .../views/edit_account_page.dart | 163 +++++++++++++ .../views/new_account_page.dart | 121 +++------- .../views/profile_edit_form.dart | 106 +++++++++ .../views/show_recovery_key_page.dart | 2 +- lib/app.dart | 4 +- lib/chat/cubits/active_chat_cubit.dart | 8 +- lib/chat/cubits/chat_component_cubit.dart | 4 +- .../cubits/single_contact_messages_cubit.dart | 4 +- lib/chat/views/chat_component_widget.dart | 4 +- lib/chat_list/cubits/chat_list_cubit.dart | 6 +- lib/chat_list/cubits/cubits.dart | 2 - .../cubits/contact_invitation_list_cubit.dart | 10 +- .../cubits/contact_request_inbox_cubit.dart | 4 +- .../cubits/waiting_invitation_cubit.dart | 6 +- .../waiting_invitations_bloc_map_cubit.dart | 8 +- .../models/valid_contact_invitation.dart | 6 +- .../views/invitation_dialog.dart | 2 +- lib/contacts/cubits/contact_list_cubit.dart | 10 +- lib/contacts/cubits/cubits.dart | 1 - lib/conversation/conversation.dart | 1 + .../active_conversations_bloc_map_cubit.dart | 17 +- ...ve_single_contact_chat_bloc_map_cubit.dart | 6 +- .../cubits/conversation_cubit.dart | 26 +-- lib/conversation/cubits/cubits.dart | 3 + lib/layout/home/drawer_menu/drawer_menu.dart | 102 ++++++--- .../home/drawer_menu/menu_item_widget.dart | 2 +- .../home_account_ready_main.dart | 2 +- .../home_account_ready_shell.dart | 73 +++--- lib/layout/home/home_shell.dart | 21 +- lib/proto/veilidchat.pb.dart | 195 +++++++++++++++- lib/proto/veilidchat.pbjson.dart | 65 +++++- lib/proto/veilidchat.proto | 34 ++- .../views/signal_strength_meter.dart | 36 ++- .../lib/dht_support/proto/dht.proto | 22 -- packages/veilid_support/lib/proto/dht.pb.dart | 171 -------------- .../veilid_support/lib/proto/dht.pbjson.dart | 45 ---- packages/veilid_support/pubspec.lock | 9 +- packages/veilid_support/pubspec.yaml | 8 +- pubspec.lock | 9 +- pubspec.yaml | 8 +- 49 files changed, 967 insertions(+), 655 deletions(-) rename lib/account_manager/cubits/{active_local_account_cubit.dart => active_account_info_cubit.dart} (67%) rename lib/account_manager/models/{active_account_info.dart => unlocked_account_info.dart} (96%) create mode 100644 lib/account_manager/views/edit_account_page.dart create mode 100644 lib/account_manager/views/profile_edit_form.dart create mode 100644 lib/conversation/conversation.dart rename lib/{chat_list => conversation}/cubits/active_conversations_bloc_map_cubit.dart (88%) rename lib/{chat_list => conversation}/cubits/active_single_contact_chat_bloc_map_cubit.dart (96%) rename lib/{contacts => conversation}/cubits/conversation_cubit.dart (94%) create mode 100644 lib/conversation/cubits/cubits.dart diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 5773265..035682e 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -27,6 +27,22 @@ "name": "Name", "pronouns": "Pronouns" }, + "edit_account_page": { + "titlebar": "Edit Account", + "header": "Account Profile", + "update": "Update", + "instructions": "This information will be shared with the people you invite to connect with you on VeilidChat.", + "error": "Account modification error", + "name": "Name", + "pronouns": "Pronouns", + "remove_account": "Remove Account", + "delete_identity": "Delete Identity", + "remove_account_confirm": "Confirm Account Removal?", + "remove_account_description": "Remove account from this device only", + "delete_identity_description": "Delete identity and all messages completely", + "delete_identity_confirm_message": "This action is PERMANENT, and your identity will no longer be recoverable with the recovery key. This will not remove your messages you have sent from other people's devices.", + "confirm_are_you_sure": "Are you sure you want to do this?" + }, "show_recovery_key_page": { "titlebar": "Save Recovery Key", "instructions": "You must save this recovery key somewhere safe. This key is the ONLY way to recover your VeilidChat account in the event of a forgotton password or a lost, stolen, or compromised device.", diff --git a/lib/account_manager/cubits/account_record_cubit.dart b/lib/account_manager/cubits/account_record_cubit.dart index 8ea1b5d..aca66e1 100644 --- a/lib/account_manager/cubits/account_record_cubit.dart +++ b/lib/account_manager/cubits/account_record_cubit.dart @@ -3,13 +3,33 @@ import 'dart:async'; import 'package:veilid_support/veilid_support.dart'; import '../../proto/proto.dart' as proto; +import '../account_manager.dart'; typedef AccountRecordState = proto.Account; class AccountRecordCubit extends DefaultDHTRecordCubit { - AccountRecordCubit({ - required super.open, - }) : super(decodeState: proto.Account.fromBuffer); + AccountRecordCubit( + {required AccountRepository accountRepository, + required TypedKey superIdentityRecordKey}) + : super( + decodeState: proto.Account.fromBuffer, + open: () => _open(accountRepository, superIdentityRecordKey)); + + static Future _open(AccountRepository accountRepository, + TypedKey superIdentityRecordKey) async { + final localAccount = + accountRepository.fetchLocalAccount(superIdentityRecordKey)!; + final userLogin = accountRepository.fetchUserLogin(superIdentityRecordKey)!; + + // Record not yet open, do it + final pool = DHTRecordPool.instance; + final record = await pool.openRecordOwned( + userLogin.accountRecordInfo.accountRecord, + debugName: 'AccountRecordCubit::_open::AccountRecord', + parent: localAccount.superIdentity.currentInstance.recordKey); + + return record; + } @override Future close() async { diff --git a/lib/account_manager/cubits/account_records_bloc_map_cubit.dart b/lib/account_manager/cubits/account_records_bloc_map_cubit.dart index be9b581..bf7bce3 100644 --- a/lib/account_manager/cubits/account_records_bloc_map_cubit.dart +++ b/lib/account_manager/cubits/account_records_bloc_map_cubit.dart @@ -15,11 +15,13 @@ class AccountRecordsBlocMapCubit extends BlocMapCubit _addAccountRecordCubit({required UserLogin userLogin}) async => + Future _addAccountRecordCubit( + {required TypedKey superIdentityRecordKey}) async => add(() => MapEntry( - userLogin.superIdentityRecordKey, + superIdentityRecordKey, AccountRecordCubit( - open: () => _accountRepository.openAccountRecord(userLogin)))); + accountRepository: _accountRepository, + superIdentityRecordKey: superIdentityRecordKey))); /// StateFollower ///////////////////////// @@ -28,7 +30,8 @@ class AccountRecordsBlocMapCubit extends BlocMapCubit updateState(TypedKey key, UserLogin value) async { - await _addAccountRecordCubit(userLogin: value); + await _addAccountRecordCubit( + superIdentityRecordKey: value.superIdentityRecordKey); } //////////////////////////////////////////////////////////////////////////// diff --git a/lib/account_manager/cubits/active_local_account_cubit.dart b/lib/account_manager/cubits/active_account_info_cubit.dart similarity index 67% rename from lib/account_manager/cubits/active_local_account_cubit.dart rename to lib/account_manager/cubits/active_account_info_cubit.dart index 58a9cb8..b61401f 100644 --- a/lib/account_manager/cubits/active_local_account_cubit.dart +++ b/lib/account_manager/cubits/active_account_info_cubit.dart @@ -1,23 +1,23 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; -import 'package:veilid_support/veilid_support.dart'; +import '../models/models.dart'; import '../repository/account_repository.dart'; -class ActiveLocalAccountCubit extends Cubit { - ActiveLocalAccountCubit(AccountRepository accountRepository) +class ActiveAccountInfoCubit extends Cubit { + ActiveAccountInfoCubit(AccountRepository accountRepository) : _accountRepository = accountRepository, - super(accountRepository.getActiveLocalAccount()) { + super(accountRepository + .getAccountInfo(accountRepository.getActiveLocalAccount())) { // Subscribe to streams _accountRepositorySubscription = _accountRepository.stream.listen((change) { switch (change) { case AccountRepositoryChange.activeLocalAccount: - emit(_accountRepository.getActiveLocalAccount()); - break; - // Ignore these case AccountRepositoryChange.localAccounts: case AccountRepositoryChange.userLogins: + emit(accountRepository + .getAccountInfo(accountRepository.getActiveLocalAccount())); break; } }); diff --git a/lib/account_manager/cubits/cubits.dart b/lib/account_manager/cubits/cubits.dart index ee9d91b..fc794af 100644 --- a/lib/account_manager/cubits/cubits.dart +++ b/lib/account_manager/cubits/cubits.dart @@ -1,5 +1,5 @@ export 'account_record_cubit.dart'; export 'account_records_bloc_map_cubit.dart'; -export 'active_local_account_cubit.dart'; +export 'active_account_info_cubit.dart'; export 'local_accounts_cubit.dart'; export 'user_logins_cubit.dart'; diff --git a/lib/account_manager/models/account_info.dart b/lib/account_manager/models/account_info.dart index 7f2e058..8adb2cb 100644 --- a/lib/account_manager/models/account_info.dart +++ b/lib/account_manager/models/account_info.dart @@ -1,6 +1,6 @@ import 'package:meta/meta.dart'; -import 'active_account_info.dart'; +import 'unlocked_account_info.dart'; enum AccountInfoStatus { noAccount, @@ -14,10 +14,10 @@ class AccountInfo { const AccountInfo({ required this.status, required this.active, - required this.activeAccountInfo, + required this.unlockedAccountInfo, }); final AccountInfoStatus status; final bool active; - final ActiveAccountInfo? activeAccountInfo; + final UnlockedAccountInfo? unlockedAccountInfo; } diff --git a/lib/account_manager/models/models.dart b/lib/account_manager/models/models.dart index d4b0ab5..bb08695 100644 --- a/lib/account_manager/models/models.dart +++ b/lib/account_manager/models/models.dart @@ -1,5 +1,5 @@ export 'account_info.dart'; -export 'active_account_info.dart'; +export 'unlocked_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/models/active_account_info.dart b/lib/account_manager/models/unlocked_account_info.dart similarity index 96% rename from lib/account_manager/models/active_account_info.dart rename to lib/account_manager/models/unlocked_account_info.dart index 5f69e32..4e810eb 100644 --- a/lib/account_manager/models/active_account_info.dart +++ b/lib/account_manager/models/unlocked_account_info.dart @@ -7,8 +7,8 @@ import 'local_account/local_account.dart'; import 'user_login/user_login.dart'; @immutable -class ActiveAccountInfo { - const ActiveAccountInfo({ +class UnlockedAccountInfo { + const UnlockedAccountInfo({ required this.localAccount, required this.userLogin, }); diff --git a/lib/account_manager/repository/account_repository.dart b/lib/account_manager/repository/account_repository.dart index 61ac5a3..762a238 100644 --- a/lib/account_manager/repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository.dart @@ -45,19 +45,6 @@ class AccountRepository { valueToJson: (val) => val?.toJson(), makeInitialValue: () => null); - ////////////////////////////////////////////////////////////// - /// Fields - - final TableDBValue> _localAccounts; - final TableDBValue> _userLogins; - final TableDBValue _activeLocalAccount; - final StreamController _streamController; - - ////////////////////////////////////////////////////////////// - /// Singleton initialization - - static AccountRepository instance = AccountRepository._(); - Future init() async { await _localAccounts.get(); await _userLogins.get(); @@ -71,12 +58,10 @@ class AccountRepository { } ////////////////////////////////////////////////////////////// - /// Streams - + /// Public Interface + /// Stream get stream => _streamController.stream; - ////////////////////////////////////////////////////////////// - /// Selectors IList getLocalAccounts() => _localAccounts.value; TypedKey? getActiveLocalAccount() => _activeLocalAccount.value; IList getUserLogins() => _userLogins.value; @@ -116,7 +101,7 @@ class AccountRepository { return const AccountInfo( status: AccountInfoStatus.noAccount, active: false, - activeAccountInfo: null); + unlockedAccountInfo: null); } superIdentityRecordKey = activeLocalAccount; } @@ -129,7 +114,7 @@ class AccountRepository { return AccountInfo( status: AccountInfoStatus.noAccount, active: active, - activeAccountInfo: null); + unlockedAccountInfo: null); } // See if we've logged into this account or if it is locked @@ -139,21 +124,18 @@ class AccountRepository { return AccountInfo( status: AccountInfoStatus.accountLocked, active: active, - activeAccountInfo: null); + unlockedAccountInfo: null); } // Got account, decrypted and decoded return AccountInfo( status: AccountInfoStatus.accountReady, active: active, - activeAccountInfo: - ActiveAccountInfo(localAccount: localAccount, userLogin: userLogin), + unlockedAccountInfo: + UnlockedAccountInfo(localAccount: localAccount, userLogin: userLogin), ); } - ////////////////////////////////////////////////////////////// - /// Mutators - /// Reorder accounts Future reorderAccount(int oldIndex, int newIndex) async { final localAccounts = await _localAccounts.get(); @@ -168,15 +150,14 @@ class AccountRepository { /// Creates a new super identity, an identity instance, an account associated /// with the identity instance, stores the account in the identity key and /// then logs into that account with no password set at this time - Future createWithNewSuperIdentity( - NewProfileSpec newProfileSpec) async { + Future createWithNewSuperIdentity(proto.Profile newProfile) async { log.debug('Creating super identity'); final wsi = await WritableSuperIdentity.create(); try { final localAccount = await _newLocalAccount( superIdentity: wsi.superIdentity, identitySecret: wsi.identitySecret, - newProfileSpec: newProfileSpec); + newProfile: newProfile); // Log in the new account by default with no pin final ok = await login( @@ -190,85 +171,18 @@ class AccountRepository { } } - /// Creates a new Account associated with the current instance of the identity - /// Adds a logged-out LocalAccount to track its existence on this device - Future _newLocalAccount( - {required SuperIdentity superIdentity, - required SecretKey identitySecret, - required NewProfileSpec newProfileSpec, - EncryptionKeyType encryptionKeyType = EncryptionKeyType.none, - String encryptionKey = ''}) async { - log.debug('Creating new local account'); + Future editAccountProfile( + TypedKey superIdentityRecordKey, proto.Profile newProfile) async { + log.debug('Editing profile for $superIdentityRecordKey'); final localAccounts = await _localAccounts.get(); - // Add account with profile to DHT - await superIdentity.currentInstance.addAccount( - superRecordKey: superIdentity.recordKey, - secretKey: identitySecret, - accountKey: veilidChatAccountKey, - createAccountCallback: (parent) async { - // Make empty contact list - log.debug('Creating contacts list'); - final contactList = await (await DHTShortArray.create( - debugName: 'AccountRepository::_newLocalAccount::Contacts', - parent: parent)) - .scope((r) async => r.recordPointer); - - // Make empty contact invitation record list - log.debug('Creating contact invitation records list'); - final contactInvitationRecords = await (await DHTShortArray.create( - debugName: - 'AccountRepository::_newLocalAccount::ContactInvitations', - parent: parent)) - .scope((r) async => r.recordPointer); - - // Make empty chat record list - log.debug('Creating chat records list'); - final chatRecords = await (await DHTShortArray.create( - debugName: 'AccountRepository::_newLocalAccount::Chats', - parent: parent)) - .scope((r) async => r.recordPointer); - - // Make account object - final account = proto.Account() - ..profile = (proto.Profile() - ..name = newProfileSpec.name - ..pronouns = newProfileSpec.pronouns) - ..contactList = contactList.toProto() - ..contactInvitationRecords = contactInvitationRecords.toProto() - ..chatList = chatRecords.toProto(); - return account.writeToBuffer(); - }); - - // Encrypt identitySecret with key - final identitySecretBytes = await encryptionKeyType.encryptSecretToBytes( - secret: identitySecret, - cryptoKind: superIdentity.currentInstance.recordKey.kind, - encryptionKey: encryptionKey, - ); - - // 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( - superIdentity: superIdentity, - identitySecretBytes: identitySecretBytes, - encryptionKeyType: encryptionKeyType, - biometricsEnabled: false, - hiddenAccount: false, - name: newProfileSpec.name, - ); - - // Add local account object to internal store - final newLocalAccounts = localAccounts.add(localAccount); + final newLocalAccounts = localAccounts.replaceFirstWhere( + (x) => x.superIdentity.recordKey == superIdentityRecordKey, + (localAccount) => localAccount!.copyWith(name: newProfile.name)); await _localAccounts.set(newLocalAccounts); _streamController.add(AccountRepositoryChange.localAccounts); - - // Return local account object - return localAccount; } /// Remove an account and wipe the messages for this account from this device @@ -310,6 +224,88 @@ class AccountRepository { _streamController.add(AccountRepositoryChange.activeLocalAccount); } + ////////////////////////////////////////////////////////////// + /// Internal Implementation + + /// Creates a new Account associated with the current instance of the identity + /// Adds a logged-out LocalAccount to track its existence on this device + Future _newLocalAccount( + {required SuperIdentity superIdentity, + required SecretKey identitySecret, + required proto.Profile newProfile, + EncryptionKeyType encryptionKeyType = EncryptionKeyType.none, + String encryptionKey = ''}) async { + log.debug('Creating new local account'); + + final localAccounts = await _localAccounts.get(); + + // Add account with profile to DHT + await superIdentity.currentInstance.addAccount( + superRecordKey: superIdentity.recordKey, + secretKey: identitySecret, + accountKey: veilidChatAccountKey, + createAccountCallback: (parent) async { + // Make empty contact list + log.debug('Creating contacts list'); + final contactList = await (await DHTShortArray.create( + debugName: 'AccountRepository::_newLocalAccount::Contacts', + parent: parent)) + .scope((r) async => r.recordPointer); + + // Make empty contact invitation record list + log.debug('Creating contact invitation records list'); + final contactInvitationRecords = await (await DHTShortArray.create( + debugName: + 'AccountRepository::_newLocalAccount::ContactInvitations', + parent: parent)) + .scope((r) async => r.recordPointer); + + // Make empty chat record list + log.debug('Creating chat records list'); + final chatRecords = await (await DHTShortArray.create( + debugName: 'AccountRepository::_newLocalAccount::Chats', + parent: parent)) + .scope((r) async => r.recordPointer); + + // Make account object + final account = proto.Account() + ..profile = newProfile + ..contactList = contactList.toProto() + ..contactInvitationRecords = contactInvitationRecords.toProto() + ..chatList = chatRecords.toProto(); + return account.writeToBuffer(); + }); + + // Encrypt identitySecret with key + final identitySecretBytes = await encryptionKeyType.encryptSecretToBytes( + secret: identitySecret, + cryptoKind: superIdentity.currentInstance.recordKey.kind, + encryptionKey: encryptionKey, + ); + + // 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( + superIdentity: superIdentity, + identitySecretBytes: identitySecretBytes, + encryptionKeyType: encryptionKeyType, + biometricsEnabled: false, + hiddenAccount: false, + name: newProfile.name, + ); + + // Add local account object to internal store + final newLocalAccounts = localAccounts.add(localAccount); + + await _localAccounts.set(newLocalAccounts); + _streamController.add(AccountRepositoryChange.localAccounts); + + // Return local account object + return localAccount; + } + Future _decryptedLogin( SuperIdentity superIdentity, SecretKey identitySecret) async { // Verify identity secret works and return the valid cryptosystem @@ -402,16 +398,13 @@ class AccountRepository { _streamController.add(AccountRepositoryChange.userLogins); } - Future openAccountRecord(UserLogin userLogin) async { - final localAccount = fetchLocalAccount(userLogin.superIdentityRecordKey)!; + ////////////////////////////////////////////////////////////// + /// Fields - // Record not yet open, do it - final pool = DHTRecordPool.instance; - final record = await pool.openRecordOwned( - userLogin.accountRecordInfo.accountRecord, - debugName: 'AccountRepository::openAccountRecord::AccountRecord', - parent: localAccount.superIdentity.currentInstance.recordKey); + static AccountRepository instance = AccountRepository._(); - return record; - } + final TableDBValue> _localAccounts; + final TableDBValue> _userLogins; + final TableDBValue _activeLocalAccount; + final StreamController _streamController; } diff --git a/lib/account_manager/views/edit_account_page.dart b/lib/account_manager/views/edit_account_page.dart new file mode 100644 index 0000000..83e2067 --- /dev/null +++ b/lib/account_manager/views/edit_account_page.dart @@ -0,0 +1,163 @@ +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:go_router/go_router.dart'; +import 'package:protobuf/protobuf.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../contacts/cubits/contact_list_cubit.dart'; +import '../../conversation/conversation.dart'; +import '../../layout/default_app_bar.dart'; +import '../../proto/proto.dart' as proto; +import '../../theme/theme.dart'; +import '../../tools/tools.dart'; +import '../../veilid_processor/veilid_processor.dart'; +import '../account_manager.dart'; +import 'profile_edit_form.dart'; + +class EditAccountPage extends StatefulWidget { + const EditAccountPage( + {required this.superIdentityRecordKey, + required this.existingProfile, + super.key}); + + @override + State createState() => _EditAccountPageState(); + + final TypedKey superIdentityRecordKey; + final proto.Profile existingProfile; + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty( + 'superIdentityRecordKey', superIdentityRecordKey)) + ..add(DiagnosticsProperty( + 'existingProfile', existingProfile)); + } +} + +class _EditAccountPageState extends State { + final _formKey = GlobalKey(); + bool _isInAsyncCall = false; + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + await changeWindowSetup( + TitleBarStyle.normal, OrientationCapability.portraitOnly); + }); + } + + Widget _editAccountForm(BuildContext context, + {required Future Function(GlobalKey) + onSubmit}) => + EditProfileForm( + header: translate('edit_account_page.header'), + instructions: translate('edit_account_page.instructions'), + submitText: translate('edit_account_page.update'), + submitDisabledText: translate('button.waiting_for_network'), + onSubmit: onSubmit); + + @override + Widget build(BuildContext context) { + final displayModalHUD = _isInAsyncCall; + final accountRecordCubit = context.read(); + final activeConversationsBlocMapCubit = + context.read(); + final contactListCubit = context.read(); + + return Scaffold( + // resizeToAvoidBottomInset: false, + appBar: DefaultAppBar( + title: Text(translate('edit_account_page.titlebar')), + actions: [ + const SignalStrengthMeterWidget(), + IconButton( + icon: const Icon(Icons.settings), + tooltip: translate('menu.settings_tooltip'), + onPressed: () async { + await GoRouterHelper(context).push('/settings'); + }) + ]), + body: _editAccountForm( + context, + onSubmit: (formKey) async { + // dismiss the keyboard by unfocusing the textfield + FocusScope.of(context).unfocus(); + + try { + final name = _formKey.currentState! + .fields[EditProfileForm.formFieldName]!.value as String; + final pronouns = _formKey + .currentState! + .fields[EditProfileForm.formFieldPronouns]! + .value as String? ?? + ''; + final newProfile = widget.existingProfile.deepCopy() + ..name = name + ..pronouns = pronouns; + + setState(() { + _isInAsyncCall = true; + }); + try { + // Update account profile DHT record + final newValue = await accountRecordCubit.record + .tryWriteProtobuf(proto.Account.fromBuffer, newProfile); + if (newValue != null) { + if (context.mounted) { + await showErrorModal( + context, + translate('edit_account_page.error'), + 'Failed to update profile online'); + return; + } + } + + // Update local account profile + await AccountRepository.instance.editAccountProfile( + widget.superIdentityRecordKey, newProfile); + + // Update all conversations with new profile + final updates = >[]; + for (final key in activeConversationsBlocMapCubit.state.keys) { + await activeConversationsBlocMapCubit.operateAsync(key, + closure: (cubit) async { + final newLocalConversation = + cubit.state.asData?.value.localConversation.deepCopy(); + if (newLocalConversation != null) { + newLocalConversation.profile = newProfile; + updates.add(cubit.input.writeLocalConversation( + conversation: newLocalConversation)); + } + }); + } + + // Wait for updates + await updates.wait; + + // XXX: how to do this for non-chat contacts? + } finally { + if (mounted) { + setState(() { + _isInAsyncCall = false; + }); + } + } + } on Exception catch (e) { + if (context.mounted) { + await showErrorModal(context, + translate('edit_account_page.error'), 'Exception: $e'); + } + } + }, + ).paddingSymmetric(horizontal: 24, vertical: 8), + ).withModalHUD(context, displayModalHUD); + } +} diff --git a/lib/account_manager/views/new_account_page.dart b/lib/account_manager/views/new_account_page.dart index 34a17af..88ccef7 100644 --- a/lib/account_manager/views/new_account_page.dart +++ b/lib/account_manager/views/new_account_page.dart @@ -1,30 +1,28 @@ 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:form_builder_validators/form_builder_validators.dart'; import 'package:go_router/go_router.dart'; import '../../layout/default_app_bar.dart'; +import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; import '../../tools/tools.dart'; import '../../veilid_processor/veilid_processor.dart'; import '../account_manager.dart'; +import 'profile_edit_form.dart'; class NewAccountPage extends StatefulWidget { const NewAccountPage({super.key}); @override - NewAccountPageState createState() => NewAccountPageState(); + State createState() => _NewAccountPageState(); } -class NewAccountPageState extends State { +class _NewAccountPageState extends State { final _formKey = GlobalKey(); - late bool isInAsyncCall = false; - static const String formFieldName = 'name'; - static const String formFieldPronouns = 'pronouns'; + bool _isInAsyncCall = false; @override void initState() { @@ -47,70 +45,17 @@ class NewAccountPageState extends State { false; final canSubmit = networkReady; - return FormBuilder( - key: _formKey, - child: ListView( - children: [ - Text(translate('new_account_page.header')) - .textStyle(context.headlineSmall) - .paddingSymmetric(vertical: 16), - FormBuilderTextField( - autofocus: true, - name: formFieldName, - decoration: - InputDecoration(labelText: translate('account.form_name')), - maxLength: 64, - // The validator receives the text that the user has entered. - 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(), - Text(translate('new_account_page.instructions')) - .toCenter() - .flexible(flex: 6), - const Spacer(), - ]).paddingSymmetric(vertical: 4), - ElevatedButton( - onPressed: !canSubmit - ? null - : () async { - if (_formKey.currentState?.saveAndValidate() ?? false) { - setState(() { - isInAsyncCall = true; - }); - try { - await onSubmit(_formKey); - } finally { - if (mounted) { - setState(() { - isInAsyncCall = false; - }); - } - } - } - }, - child: Text(translate(!networkReady - ? 'button.waiting_for_network' - : 'new_account_page.create')), - ).paddingSymmetric(vertical: 4).alignAtCenterRight(), - ], - ), - ); + return EditProfileForm( + header: translate('new_account_page.header'), + instructions: translate('new_account_page.instructions'), + submitText: translate('new_account_page.create'), + submitDisabledText: translate('button.waiting_for_network'), + onSubmit: !canSubmit ? null : onSubmit); } @override Widget build(BuildContext context) { - final displayModalHUD = isInAsyncCall; + final displayModalHUD = _isInAsyncCall; return Scaffold( // resizeToAvoidBottomInset: false, @@ -120,7 +65,7 @@ class NewAccountPageState extends State { const SignalStrengthMeterWidget(), IconButton( icon: const Icon(Icons.settings), - tooltip: translate('app_bar.settings_tooltip'), + tooltip: translate('menu.settings_tooltip'), onPressed: () async { await GoRouterHelper(context).push('/settings'); }) @@ -132,19 +77,33 @@ class NewAccountPageState extends State { FocusScope.of(context).unfocus(); try { - final name = - _formKey.currentState!.fields[formFieldName]!.value as String; - final pronouns = _formKey.currentState!.fields[formFieldPronouns]! + final name = _formKey.currentState! + .fields[EditProfileForm.formFieldName]!.value as String; + final pronouns = _formKey + .currentState! + .fields[EditProfileForm.formFieldPronouns]! .value as String? ?? ''; - final newProfileSpec = - NewProfileSpec(name: name, pronouns: pronouns); + final newProfile = proto.Profile() + ..name = name + ..pronouns = pronouns; - final superSecret = await AccountRepository.instance - .createWithNewSuperIdentity(newProfileSpec); - - GoRouterHelper(context).pushReplacement('/new_account/recovery_key', - extra: superSecret); + setState(() { + _isInAsyncCall = true; + }); + try { + final superSecret = await AccountRepository.instance + .createWithNewSuperIdentity(newProfile); + GoRouterHelper(context).pushReplacement( + '/new_account/recovery_key', + extra: superSecret); + } finally { + if (mounted) { + setState(() { + _isInAsyncCall = false; + }); + } + } } on Exception catch (e) { if (context.mounted) { await showErrorModal(context, translate('new_account_page.error'), @@ -155,10 +114,4 @@ class NewAccountPageState extends State { ).paddingSymmetric(horizontal: 24, vertical: 8), ).withModalHUD(context, displayModalHUD); } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('isInAsyncCall', isInAsyncCall)); - } } diff --git a/lib/account_manager/views/profile_edit_form.dart b/lib/account_manager/views/profile_edit_form.dart new file mode 100644 index 0000000..a055396 --- /dev/null +++ b/lib/account_manager/views/profile_edit_form.dart @@ -0,0 +1,106 @@ +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_translate/flutter_translate.dart'; +import 'package:form_builder_validators/form_builder_validators.dart'; + +class EditProfileForm extends StatefulWidget { + const EditProfileForm({ + required this.header, + required this.instructions, + required this.submitText, + required this.submitDisabledText, + super.key, + this.onSubmit, + }); + + @override + State createState() => _EditProfileFormState(); + + final String header; + final String instructions; + final Future Function(GlobalKey)? onSubmit; + final String submitText; + final String submitDisabledText; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(StringProperty('header', header)) + ..add(StringProperty('instructions', instructions)) + ..add(ObjectFlagProperty< + Future Function( + GlobalKey p1)?>.has('onSubmit', onSubmit)) + ..add(StringProperty('submitText', submitText)) + ..add(StringProperty('submitDisabledText', submitDisabledText)); + } + + static const String formFieldName = 'name'; + static const String formFieldPronouns = 'pronouns'; +} + +class _EditProfileFormState extends State { + final _formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + } + + Widget _editProfileForm( + BuildContext context, + ) => + FormBuilder( + key: _formKey, + child: ListView( + children: [ + Text(widget.header) + .textStyle(context.headlineSmall) + .paddingSymmetric(vertical: 16), + FormBuilderTextField( + autofocus: true, + name: EditProfileForm.formFieldName, + decoration: + InputDecoration(labelText: translate('account.form_name')), + maxLength: 64, + // The validator receives the text that the user has entered. + validator: FormBuilderValidators.compose([ + FormBuilderValidators.required(), + ]), + textInputAction: TextInputAction.next, + ), + FormBuilderTextField( + name: EditProfileForm.formFieldPronouns, + maxLength: 64, + decoration: InputDecoration( + labelText: translate('account.form_pronouns')), + textInputAction: TextInputAction.next, + ), + Row(children: [ + const Spacer(), + Text(widget.instructions).toCenter().flexible(flex: 6), + const Spacer(), + ]).paddingSymmetric(vertical: 4), + ElevatedButton( + onPressed: widget.onSubmit == null + ? null + : () async { + if (_formKey.currentState?.saveAndValidate() ?? false) { + await widget.onSubmit!(_formKey); + } + }, + child: Text((widget.onSubmit == null) + ? widget.submitDisabledText + : widget.submitText), + ).paddingSymmetric(vertical: 4).alignAtCenterRight(), + ], + ), + ); + + @override + Widget build(BuildContext context) => _editProfileForm( + context, + ); +} diff --git a/lib/account_manager/views/show_recovery_key_page.dart b/lib/account_manager/views/show_recovery_key_page.dart index 4218d12..62d4f21 100644 --- a/lib/account_manager/views/show_recovery_key_page.dart +++ b/lib/account_manager/views/show_recovery_key_page.dart @@ -48,7 +48,7 @@ class ShowRecoveryKeyPageState extends State { const SignalStrengthMeterWidget(), IconButton( icon: const Icon(Icons.settings), - tooltip: translate('app_bar.settings_tooltip'), + tooltip: translate('menu.settings_tooltip'), onPressed: () async { await GoRouterHelper(context).push('/settings'); }) diff --git a/lib/app.dart b/lib/app.dart index e10ce32..19754ce 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -122,9 +122,9 @@ class VeilidChatApp extends StatelessWidget { create: (context) => UserLoginsCubit(AccountRepository.instance), ), - BlocProvider( + BlocProvider( create: (context) => - ActiveLocalAccountCubit(AccountRepository.instance), + ActiveAccountInfoCubit(AccountRepository.instance), ), BlocProvider( create: (context) => diff --git a/lib/chat/cubits/active_chat_cubit.dart b/lib/chat/cubits/active_chat_cubit.dart index 2e72abc..ead18aa 100644 --- a/lib/chat/cubits/active_chat_cubit.dart +++ b/lib/chat/cubits/active_chat_cubit.dart @@ -1,13 +1,19 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:veilid_support/veilid_support.dart'; +import '../../router/router.dart'; + // XXX: if we ever want to have more than one chat 'open', we should put the // operations and state for that here. class ActiveChatCubit extends Cubit { - ActiveChatCubit(super.initialState); + ActiveChatCubit(super.initialState, {required RouterCubit routerCubit}) + : _routerCubit = routerCubit; void setActiveChat(TypedKey? activeChatLocalConversationRecordKey) { emit(activeChatLocalConversationRecordKey); + _routerCubit.setHasActiveChat(activeChatLocalConversationRecordKey != null); } + + final RouterCubit _routerCubit; } diff --git a/lib/chat/cubits/chat_component_cubit.dart b/lib/chat/cubits/chat_component_cubit.dart index bc7431c..0bf566b 100644 --- a/lib/chat/cubits/chat_component_cubit.dart +++ b/lib/chat/cubits/chat_component_cubit.dart @@ -12,7 +12,7 @@ import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; -import '../../chat_list/chat_list.dart'; +import '../../conversation/conversation.dart'; import '../../proto/proto.dart' as proto; import '../models/chat_component_state.dart'; import '../models/message_state.dart'; @@ -44,7 +44,7 @@ class ChatComponentCubit extends Cubit { // ignore: prefer_constructors_over_static_methods static ChatComponentCubit singleContact( - {required ActiveAccountInfo activeAccountInfo, + {required UnlockedAccountInfo activeAccountInfo, required proto.Account accountRecordInfo, required ActiveConversationState activeConversationState, required SingleContactMessagesCubit messagesCubit}) { diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 0894ac1..9f24a90 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -50,7 +50,7 @@ typedef SingleContactMessagesState = AsyncValue>; // Builds the reconciled chat record from the local and remote conversation keys class SingleContactMessagesCubit extends Cubit { SingleContactMessagesCubit({ - required ActiveAccountInfo activeAccountInfo, + required UnlockedAccountInfo activeAccountInfo, required TypedKey remoteIdentityPublicKey, required TypedKey localConversationRecordKey, required TypedKey localMessagesRecordKey, @@ -402,7 +402,7 @@ class SingleContactMessagesCubit extends Cubit { ///////////////////////////////////////////////////////////////////////// final WaitSet _initWait = WaitSet(); - final ActiveAccountInfo _activeAccountInfo; + final UnlockedAccountInfo _activeAccountInfo; final TypedKey _remoteIdentityPublicKey; final TypedKey _localConversationRecordKey; final TypedKey _localMessagesRecordKey; diff --git a/lib/chat/views/chat_component_widget.dart b/lib/chat/views/chat_component_widget.dart index a3b2e33..a65c5ef 100644 --- a/lib/chat/views/chat_component_widget.dart +++ b/lib/chat/views/chat_component_widget.dart @@ -9,7 +9,7 @@ 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 '../../conversation/conversation.dart'; import '../../theme/theme.dart'; import '../chat.dart'; @@ -23,7 +23,7 @@ class ChatComponentWidget extends StatelessWidget { {required TypedKey localConversationRecordKey, Key? key}) => Builder(builder: (context) { // Get all watched dependendies - final activeAccountInfo = context.watch(); + final activeAccountInfo = context.watch(); final accountRecordInfo = context.watch().state.asData?.value; if (accountRecordInfo == null) { diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index 3ea368b..f9dda7f 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -19,15 +19,15 @@ typedef ChatListCubitState = DHTShortArrayBusyState; class ChatListCubit extends DHTShortArrayCubit with StateMapFollowable { ChatListCubit({ - required ActiveAccountInfo activeAccountInfo, + required UnlockedAccountInfo unlockedAccountInfo, required proto.Account account, required this.activeChatCubit, }) : super( - open: () => _open(activeAccountInfo, account), + open: () => _open(unlockedAccountInfo, account), decodeElement: proto.Chat.fromBuffer); static Future _open( - ActiveAccountInfo activeAccountInfo, proto.Account account) async { + UnlockedAccountInfo activeAccountInfo, proto.Account account) async { final accountRecordKey = activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; diff --git a/lib/chat_list/cubits/cubits.dart b/lib/chat_list/cubits/cubits.dart index 35595db..cafafff 100644 --- a/lib/chat_list/cubits/cubits.dart +++ b/lib/chat_list/cubits/cubits.dart @@ -1,3 +1 @@ -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/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index f2f44e9..2012089 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -36,16 +36,16 @@ class ContactInvitationListCubit StateMapFollowable { ContactInvitationListCubit({ - required ActiveAccountInfo activeAccountInfo, + required UnlockedAccountInfo unlockedAccountInfo, required proto.Account account, - }) : _activeAccountInfo = activeAccountInfo, + }) : _activeAccountInfo = unlockedAccountInfo, _account = account, super( - open: () => _open(activeAccountInfo, account), + open: () => _open(unlockedAccountInfo, account), decodeElement: proto.ContactInvitationRecord.fromBuffer); static Future _open( - ActiveAccountInfo activeAccountInfo, proto.Account account) async { + UnlockedAccountInfo activeAccountInfo, proto.Account account) async { final accountRecordKey = activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; @@ -318,6 +318,6 @@ class ContactInvitationListCubit } // - final ActiveAccountInfo _activeAccountInfo; + final UnlockedAccountInfo _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 214d08b..f65363e 100644 --- a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart +++ b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart @@ -23,7 +23,7 @@ class ContactRequestInboxCubit // : super.value(decodeState: proto.SignedContactResponse.fromBuffer); static Future _open( - {required ActiveAccountInfo activeAccountInfo, + {required UnlockedAccountInfo activeAccountInfo, required proto.ContactInvitationRecord contactInvitationRecord}) async { final pool = DHTRecordPool.instance; final accountRecordKey = @@ -42,6 +42,6 @@ class ContactRequestInboxCubit defaultSubkey: 1); } - final ActiveAccountInfo activeAccountInfo; + final UnlockedAccountInfo activeAccountInfo; final proto.ContactInvitationRecord contactInvitationRecord; } diff --git a/lib/contact_invitation/cubits/waiting_invitation_cubit.dart b/lib/contact_invitation/cubits/waiting_invitation_cubit.dart index 120a2d7..d020f4e 100644 --- a/lib/contact_invitation/cubits/waiting_invitation_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitation_cubit.dart @@ -7,7 +7,7 @@ import 'package:meta/meta.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; -import '../../contacts/contacts.dart'; +import '../../conversation/conversation.dart'; import '../../proto/proto.dart' as proto; import '../../tools/tools.dart'; import '../models/accepted_contact.dart'; @@ -25,7 +25,7 @@ class InvitationStatus extends Equatable { class WaitingInvitationCubit extends AsyncTransformerCubit { WaitingInvitationCubit(ContactRequestInboxCubit super.input, - {required ActiveAccountInfo activeAccountInfo, + {required UnlockedAccountInfo activeAccountInfo, required proto.Account account, required proto.ContactInvitationRecord contactInvitationRecord}) : super( @@ -37,7 +37,7 @@ class WaitingInvitationCubit extends AsyncTransformerCubit> _transform( proto.SignedContactResponse? signedContactResponse, - {required ActiveAccountInfo activeAccountInfo, + {required UnlockedAccountInfo activeAccountInfo, required proto.Account account, required proto.ContactInvitationRecord contactInvitationRecord}) async { if (signedContactResponse == null) { 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 968e108..938a2a5 100644 --- a/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart @@ -18,7 +18,7 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit, TypedKey, proto.ContactInvitationRecord> { WaitingInvitationsBlocMapCubit( - {required this.activeAccountInfo, required this.account}); + {required this.unlockedAccountInfo, required this.account}); Future _addWaitingInvitation( {required proto.ContactInvitationRecord @@ -27,9 +27,9 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit { Future _onAccept() async { final navigator = Navigator.of(context); - final activeAccountInfo = widget.modalContext.read(); + final activeAccountInfo = widget.modalContext.read(); final contactList = widget.modalContext.read(); setState(() { diff --git a/lib/contacts/cubits/contact_list_cubit.dart b/lib/contacts/cubits/contact_list_cubit.dart index 5ab14ea..9b05cee 100644 --- a/lib/contacts/cubits/contact_list_cubit.dart +++ b/lib/contacts/cubits/contact_list_cubit.dart @@ -13,15 +13,15 @@ import 'conversation_cubit.dart'; class ContactListCubit extends DHTShortArrayCubit { ContactListCubit({ - required ActiveAccountInfo activeAccountInfo, + required UnlockedAccountInfo unlockedAccountInfo, required proto.Account account, - }) : _activeAccountInfo = activeAccountInfo, + }) : _activeAccountInfo = unlockedAccountInfo, super( - open: () => _open(activeAccountInfo, account), + open: () => _open(unlockedAccountInfo, account), decodeElement: proto.Contact.fromBuffer); static Future _open( - ActiveAccountInfo activeAccountInfo, proto.Account account) async { + UnlockedAccountInfo activeAccountInfo, proto.Account account) async { final accountRecordKey = activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; @@ -99,5 +99,5 @@ class ContactListCubit extends DHTShortArrayCubit { } } - final ActiveAccountInfo _activeAccountInfo; + final UnlockedAccountInfo _activeAccountInfo; } diff --git a/lib/contacts/cubits/cubits.dart b/lib/contacts/cubits/cubits.dart index 3d16d52..795d497 100644 --- a/lib/contacts/cubits/cubits.dart +++ b/lib/contacts/cubits/cubits.dart @@ -1,2 +1 @@ export 'contact_list_cubit.dart'; -export 'conversation_cubit.dart'; diff --git a/lib/conversation/conversation.dart b/lib/conversation/conversation.dart new file mode 100644 index 0000000..d09042f --- /dev/null +++ b/lib/conversation/conversation.dart @@ -0,0 +1 @@ +export 'cubits/cubits.dart'; diff --git a/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart b/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart similarity index 88% rename from lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart rename to lib/conversation/cubits/active_conversations_bloc_map_cubit.dart index c497941..6a1fbef 100644 --- a/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart +++ b/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart @@ -6,6 +6,7 @@ import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../contacts/contacts.dart'; +import '../../conversation/conversation.dart'; import '../../proto/proto.dart' as proto; import 'cubits.dart'; @@ -26,7 +27,9 @@ class ActiveConversationState extends Equatable { } typedef ActiveConversationCubit = TransformerCubit< - AsyncValue, AsyncValue>; + AsyncValue, + AsyncValue, + ConversationCubit>; typedef ActiveConversationsBlocMapState = BlocMapState>; @@ -41,11 +44,17 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit, ActiveConversationCubit> with StateMapFollower { ActiveConversationsBlocMapCubit( - {required ActiveAccountInfo activeAccountInfo, + {required UnlockedAccountInfo unlockedAccountInfo, required ContactListCubit contactListCubit}) - : _activeAccountInfo = activeAccountInfo, + : _activeAccountInfo = unlockedAccountInfo, _contactListCubit = contactListCubit; + //////////////////////////////////////////////////////////////////////////// + // Public Interface + + //////////////////////////////////////////////////////////////////////////// + // Private Implementation + // Add an active conversation to be tracked for changes Future _addConversation({required proto.Contact contact}) async => add(() => MapEntry( @@ -97,6 +106,6 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit> { ActiveSingleContactChatBlocMapCubit( - {required ActiveAccountInfo activeAccountInfo, + {required UnlockedAccountInfo unlockedAccountInfo, required ContactListCubit contactListCubit, required ChatListCubit chatListCubit}) - : _activeAccountInfo = activeAccountInfo, + : _activeAccountInfo = unlockedAccountInfo, _contactListCubit = contactListCubit, _chatListCubit = chatListCubit; @@ -95,7 +95,7 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit> { ConversationCubit( - {required ActiveAccountInfo activeAccountInfo, + {required UnlockedAccountInfo activeAccountInfo, required TypedKey remoteIdentityPublicKey, TypedKey? localConversationRecordKey, TypedKey? remoteConversationRecordKey}) - : _activeAccountInfo = activeAccountInfo, + : _unlockedAccountInfo = activeAccountInfo, _localConversationRecordKey = localConversationRecordKey, _remoteIdentityPublicKey = remoteIdentityPublicKey, _remoteConversationRecordKey = remoteConversationRecordKey, @@ -41,13 +41,13 @@ class ConversationCubit extends Cubit> { if (_localConversationRecordKey != null) { _initWait.add(() async { await _setLocalConversation(() async { - final accountRecordKey = _activeAccountInfo + final accountRecordKey = _unlockedAccountInfo .userLogin.accountRecordInfo.accountRecord.recordKey; // Open local record key if it is specified final pool = DHTRecordPool.instance; final crypto = await _cachedConversationCrypto(); - final writer = _activeAccountInfo.identityWriter; + final writer = _unlockedAccountInfo.identityWriter; final record = await pool.openRecordWrite( _localConversationRecordKey!, writer, debugName: 'ConversationCubit::LocalConversation', @@ -61,7 +61,7 @@ class ConversationCubit extends Cubit> { if (_remoteConversationRecordKey != null) { _initWait.add(() async { await _setRemoteConversation(() async { - final accountRecordKey = _activeAccountInfo + final accountRecordKey = _unlockedAccountInfo .userLogin.accountRecordInfo.accountRecord.recordKey; // Open remote record key if it is specified @@ -217,11 +217,11 @@ class ConversationCubit extends Cubit> { 'must not have a local conversation yet'); final pool = DHTRecordPool.instance; - final accountRecordKey = - _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + final accountRecordKey = _unlockedAccountInfo + .userLogin.accountRecordInfo.accountRecord.recordKey; final crypto = await _cachedConversationCrypto(); - final writer = _activeAccountInfo.identityWriter; + final writer = _unlockedAccountInfo.identityWriter; // Open with SMPL scheme for identity writer late final DHTRecord localConversationRecord; @@ -247,7 +247,7 @@ class ConversationCubit extends Cubit> { .deleteScope((localConversation) async { // Make messages log return _initLocalMessages( - activeAccountInfo: _activeAccountInfo, + activeAccountInfo: _unlockedAccountInfo, remoteIdentityPublicKey: _remoteIdentityPublicKey, localConversationKey: localConversation.key, callback: (messages) async { @@ -255,7 +255,7 @@ class ConversationCubit extends Cubit> { final conversation = proto.Conversation() ..profile = profile ..superIdentityJson = jsonEncode( - _activeAccountInfo.localAccount.superIdentity.toJson()) + _unlockedAccountInfo.localAccount.superIdentity.toJson()) ..messages = messages.recordKey.toProto(); // Write initial conversation to record @@ -282,7 +282,7 @@ class ConversationCubit extends Cubit> { // Initialize local messages Future _initLocalMessages({ - required ActiveAccountInfo activeAccountInfo, + required UnlockedAccountInfo activeAccountInfo, required TypedKey remoteIdentityPublicKey, required TypedKey localConversationKey, required FutureOr Function(DHTLog) callback, @@ -332,14 +332,14 @@ class ConversationCubit extends Cubit> { if (conversationCrypto != null) { return conversationCrypto; } - conversationCrypto = await _activeAccountInfo + conversationCrypto = await _unlockedAccountInfo .makeConversationCrypto(_remoteIdentityPublicKey); _conversationCrypto = conversationCrypto; return conversationCrypto; } - final ActiveAccountInfo _activeAccountInfo; + final UnlockedAccountInfo _unlockedAccountInfo; final TypedKey _remoteIdentityPublicKey; TypedKey? _localConversationRecordKey; final TypedKey? _remoteConversationRecordKey; diff --git a/lib/conversation/cubits/cubits.dart b/lib/conversation/cubits/cubits.dart new file mode 100644 index 0000000..029764f --- /dev/null +++ b/lib/conversation/cubits/cubits.dart @@ -0,0 +1,3 @@ +export 'active_conversations_bloc_map_cubit.dart'; +export 'active_single_contact_chat_bloc_map_cubit.dart'; +export 'conversation_cubit.dart'; diff --git a/lib/layout/home/drawer_menu/drawer_menu.dart b/lib/layout/home/drawer_menu/drawer_menu.dart index b037c84..f1e11ff 100644 --- a/lib/layout/home/drawer_menu/drawer_menu.dart +++ b/lib/layout/home/drawer_menu/drawer_menu.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'; @@ -9,6 +10,7 @@ import 'package:veilid_support/veilid_support.dart'; import '../../../account_manager/account_manager.dart'; import '../../../theme/theme.dart'; import '../../../tools/tools.dart'; +import '../../../veilid_processor/veilid_processor.dart'; import 'menu_item_widget.dart'; class DrawerMenu extends StatefulWidget { @@ -29,8 +31,10 @@ class _DrawerMenuState extends State { super.dispose(); } - void _doLoginClick(TypedKey superIdentityRecordKey) { - // + void _doSwitchClick(TypedKey superIdentityRecordKey) { + singleFuture(this, () async { + await AccountRepository.instance.switchToAccount(superIdentityRecordKey); + }); } void _doEditClick(TypedKey superIdentityRecordKey) { @@ -47,10 +51,12 @@ class _DrawerMenuState extends State { Widget _makeAccountWidget( {required String name, + required bool selected, + required ScaleColor scale, required bool loggedIn, - required void Function() clickHandler}) { + required void Function()? callback, + required void Function()? footerCallback}) { final theme = Theme.of(context); - final scale = theme.extension()!.tertiaryScale; final abbrev = name.split(' ').map((s) => s.isEmpty ? '' : s[0]).join(); late final String shortname; if (abbrev.length >= 3) { @@ -65,30 +71,36 @@ class _DrawerMenuState extends State { foregroundColor: loggedIn ? scale.primaryText : scale.subtleText, child: Text(shortname, style: theme.textTheme.titleLarge)); - return MenuItemWidget( - title: name, - headerWidget: avatar, - titleStyle: theme.textTheme.titleLarge!, - foregroundColor: scale.primary, - backgroundColor: scale.elementBackground, - backgroundHoverColor: scale.hoverElementBackground, - backgroundFocusColor: scale.activeElementBackground, - borderColor: scale.border, - borderHoverColor: scale.hoverBorder, - borderFocusColor: scale.primary, - footerButtonIcon: loggedIn ? Icons.edit_outlined : Icons.login_outlined, - footerCallback: clickHandler, - footerButtonIconColor: scale.border, - footerButtonIconHoverColor: scale.hoverElementBackground, - footerButtonIconFocusColor: scale.activeElementBackground, - ); + return AnimatedPadding( + padding: EdgeInsets.fromLTRB(selected ? 0 : 0, 0, selected ? 0 : 8, 0), + duration: const Duration(milliseconds: 50), + child: MenuItemWidget( + title: name, + headerWidget: avatar, + titleStyle: theme.textTheme.titleLarge!, + foregroundColor: scale.primary, + backgroundColor: selected + ? scale.activeElementBackground + : scale.elementBackground, + backgroundHoverColor: scale.hoverElementBackground, + backgroundFocusColor: scale.activeElementBackground, + borderColor: scale.border, + borderHoverColor: scale.hoverBorder, + borderFocusColor: scale.primary, + callback: callback, + footerButtonIcon: loggedIn ? Icons.edit_outlined : null, + footerCallback: footerCallback, + footerButtonIconColor: scale.border, + footerButtonIconHoverColor: scale.hoverElementBackground, + footerButtonIconFocusColor: scale.activeElementBackground, + )); } Widget _getAccountList( {required TypedKey? activeLocalAccount, required AccountRecordsBlocMapState accountRecords}) { final theme = Theme.of(context); - final scale = theme.extension()!; + final scaleScheme = theme.extension()!; final accountRepo = AccountRepository.instance; final localAccounts = accountRepo.getLocalAccounts(); @@ -104,28 +116,38 @@ class _DrawerMenuState extends State { final acctRecord = accountRecords.get(superIdentityRecordKey); if (acctRecord != null) { // Account is logged in + final scale = theme.extension()!.tertiaryScale; final loggedInAccount = acctRecord.when( data: (value) => _makeAccountWidget( name: value.profile.name, + scale: scale, + selected: superIdentityRecordKey == activeLocalAccount, loggedIn: true, - clickHandler: () { + callback: () { + _doSwitchClick(superIdentityRecordKey); + }, + footerCallback: () { _doEditClick(superIdentityRecordKey); }), loading: () => _wrapInBox( child: buildProgressIndicator(), - color: scale.grayScale.subtleBorder), + color: scaleScheme.grayScale.subtleBorder), error: (err, st) => _wrapInBox( - child: errorPage(err, st), color: scale.errorScale.subtleBorder), + child: errorPage(err, st), + color: scaleScheme.errorScale.subtleBorder), ); - loggedInAccounts.add(loggedInAccount); + loggedInAccounts.add(loggedInAccount.paddingLTRB(0, 0, 0, 8)); } else { // Account is not logged in + final scale = theme.extension()!.grayScale; final loggedOutAccount = _makeAccountWidget( - name: la.name, - loggedIn: false, - clickHandler: () { - _doLoginClick(superIdentityRecordKey); - }); + name: la.name, + scale: scale, + selected: superIdentityRecordKey == activeLocalAccount, + loggedIn: false, + callback: () => {_doSwitchClick(superIdentityRecordKey)}, + footerCallback: null, + ); loggedOutAccounts.add(loggedOutAccount); } } @@ -208,7 +230,7 @@ class _DrawerMenuState extends State { final scale = theme.extension()!; //final textTheme = theme.textTheme; final accountRecords = context.watch().state; - final activeLocalAccount = context.watch().state; + final activeLocalAccount = context.watch().state; final gradient = LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, @@ -249,13 +271,21 @@ class _DrawerMenuState extends State { ])), const Spacer(), _getAccountList( - activeLocalAccount: activeLocalAccount, + activeLocalAccount: + activeLocalAccount.unlockedAccountInfo?.superIdentityRecordKey, accountRecords: accountRecords), _getBottomButtons(), const Spacer(), - Text('Version $packageInfoVersion', - style: theme.textTheme.labelMedium! - .copyWith(color: scale.tertiaryScale.hoverBorder)) + Row(children: [ + Text('Version $packageInfoVersion', + style: theme.textTheme.labelMedium! + .copyWith(color: scale.tertiaryScale.hoverBorder)), + const Spacer(), + SignalStrengthMeterWidget( + color: scale.tertiaryScale.hoverBorder, + inactiveColor: scale.tertiaryScale.border, + ), + ]) ]).paddingAll(16), ); } diff --git a/lib/layout/home/drawer_menu/menu_item_widget.dart b/lib/layout/home/drawer_menu/menu_item_widget.dart index 68fb178..260646f 100644 --- a/lib/layout/home/drawer_menu/menu_item_widget.dart +++ b/lib/layout/home/drawer_menu/menu_item_widget.dart @@ -27,7 +27,7 @@ class MenuItemWidget extends StatelessWidget { @override Widget build(BuildContext context) => TextButton( - onPressed: () => callback, + onPressed: callback, style: TextButton.styleFrom(foregroundColor: foregroundColor).copyWith( backgroundColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.hovered)) { 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 7fafc7f..e758ba1 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 @@ -44,7 +44,7 @@ class _HomeAccountReadyMainState extends State { WidgetStateProperty.all(scale.primaryScale.hoverBorder), shape: WidgetStateProperty.all(const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(16))))), - tooltip: translate('app_bar.settings_tooltip'), + tooltip: translate('menu.settings_tooltip'), onPressed: () async { final ctrl = context.read(); await ctrl.toggle?.call(); 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 c41185b..7baa37b 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,15 +1,14 @@ import 'package:async_tools/async_tools.dart'; -import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:flutter/foundation.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 '../../../chat/chat.dart'; import '../../../chat_list/chat_list.dart'; import '../../../contact_invitation/contact_invitation.dart'; import '../../../contacts/contacts.dart'; +import '../../../conversation/conversation.dart'; import '../../../router/router.dart'; import '../../../theme/theme.dart'; @@ -18,20 +17,17 @@ class HomeAccountReadyShell extends StatefulWidget { {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 activeAccountInfo = context.read(); + final unlockedAccountInfo = context.watch(); final routerCubit = context.read(); return HomeAccountReadyShell._( - activeLocalAccount: activeLocalAccount, - activeAccountInfo: activeAccountInfo, + unlockedAccountInfo: unlockedAccountInfo, routerCubit: routerCubit, key: key, child: child); } const HomeAccountReadyShell._( - {required this.activeLocalAccount, - required this.activeAccountInfo, + {required this.unlockedAccountInfo, required this.routerCubit, required this.child, super.key}); @@ -40,18 +36,15 @@ class HomeAccountReadyShell extends StatefulWidget { HomeAccountReadyShellState createState() => HomeAccountReadyShellState(); final Widget child; - final TypedKey activeLocalAccount; - final ActiveAccountInfo activeAccountInfo; + final UnlockedAccountInfo unlockedAccountInfo; final RouterCubit routerCubit; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties - ..add(DiagnosticsProperty( - 'activeLocalAccount', activeLocalAccount)) - ..add(DiagnosticsProperty( - 'activeAccountInfo', activeAccountInfo)) + ..add(DiagnosticsProperty( + 'unlockedAccountInfo', unlockedAccountInfo)) ..add(DiagnosticsProperty('routerCubit', routerCubit)); } } @@ -115,39 +108,41 @@ class HomeAccountReadyShellState extends State { } return MultiBlocProvider( providers: [ + // Contact Cubits BlocProvider( create: (context) => ContactInvitationListCubit( - activeAccountInfo: widget.activeAccountInfo, + unlockedAccountInfo: widget.unlockedAccountInfo, account: account)), BlocProvider( create: (context) => ContactListCubit( - activeAccountInfo: widget.activeAccountInfo, + unlockedAccountInfo: widget.unlockedAccountInfo, 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()) - ..follow(context.read())), - BlocProvider( - create: (context) => ActiveSingleContactChatBlocMapCubit( - activeAccountInfo: widget.activeAccountInfo, - contactListCubit: context.read(), - chatListCubit: context.read()) - ..follow(context.read())), BlocProvider( create: (context) => WaitingInvitationsBlocMapCubit( - activeAccountInfo: widget.activeAccountInfo, account: account) - ..follow(context.read())) + unlockedAccountInfo: widget.unlockedAccountInfo, + account: account) + ..follow(context.watch())), + // Chat Cubits + BlocProvider( + create: (context) => ActiveChatCubit(null, + routerCubit: context.watch())), + BlocProvider( + create: (context) => ChatListCubit( + unlockedAccountInfo: widget.unlockedAccountInfo, + activeChatCubit: context.watch(), + account: account)), + // Conversation Cubits + BlocProvider( + create: (context) => ActiveConversationsBlocMapCubit( + unlockedAccountInfo: widget.unlockedAccountInfo, + contactListCubit: context.watch()) + ..follow(context.watch())), + BlocProvider( + create: (context) => ActiveSingleContactChatBlocMapCubit( + unlockedAccountInfo: widget.unlockedAccountInfo, + contactListCubit: context.watch(), + chatListCubit: context.watch()) + ..follow(context.watch())), ], child: MultiBlocListener(listeners: [ BlocListener { } Widget buildWithLogin(BuildContext context) { - final activeLocalAccount = context.watch().state; + final accountInfo = context.watch().state; final accountRecordsCubit = context.watch(); - if (activeLocalAccount == null) { + if (!accountInfo.active) { // If no logged in user is active, show the loading panel return const HomeNoActive(); } - final accountInfo = - AccountRepository.instance.getAccountInfo(activeLocalAccount); - final activeCubit = - accountRecordsCubit.tryOperate(activeLocalAccount, closure: (c) => c); + final superIdentityRecordKey = + accountInfo.unlockedAccountInfo?.superIdentityRecordKey; + final activeCubit = superIdentityRecordKey == null + ? null + : accountRecordsCubit.tryOperate(superIdentityRecordKey, + closure: (c) => c); if (activeCubit == null) { return waitingPage(); } @@ -59,8 +59,8 @@ class HomeShellState extends State { return const HomeAccountLocked(); case AccountInfoStatus.accountReady: return MultiProvider(providers: [ - Provider.value( - value: accountInfo.activeAccountInfo!, + Provider.value( + value: accountInfo.unlockedAccountInfo!, ), Provider.value(value: activeCubit), Provider.value(value: _zoomDrawerController), @@ -101,6 +101,7 @@ class HomeShellState extends State { // duration: const Duration(milliseconds: 250), // reverseDuration: const Duration(milliseconds: 250), menuScreenTapClose: true, + mainScreenTapClose: true, mainScreenScale: .25, slideWidth: min(360, MediaQuery.of(context).size.width * 0.9), ))); diff --git a/lib/proto/veilidchat.pb.dart b/lib/proto/veilidchat.pb.dart index 1e0395b..ff755fe 100644 --- a/lib/proto/veilidchat.pb.dart +++ b/lib/proto/veilidchat.pb.dart @@ -20,6 +20,177 @@ import 'veilidchat.pbenum.dart'; export 'veilidchat.pbenum.dart'; +class DHTDataReference extends $pb.GeneratedMessage { + factory DHTDataReference() => create(); + DHTDataReference._() : super(); + factory DHTDataReference.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory DHTDataReference.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DHTDataReference', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'dhtData', subBuilder: $0.TypedKey.create) + ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'hash', subBuilder: $0.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') + DHTDataReference clone() => DHTDataReference()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + DHTDataReference copyWith(void Function(DHTDataReference) updates) => super.copyWith((message) => updates(message as DHTDataReference)) as DHTDataReference; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DHTDataReference create() => DHTDataReference._(); + DHTDataReference createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DHTDataReference getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static DHTDataReference? _defaultInstance; + + @$pb.TagNumber(1) + $0.TypedKey get dhtData => $_getN(0); + @$pb.TagNumber(1) + set dhtData($0.TypedKey v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasDhtData() => $_has(0); + @$pb.TagNumber(1) + void clearDhtData() => clearField(1); + @$pb.TagNumber(1) + $0.TypedKey ensureDhtData() => $_ensure(0); + + @$pb.TagNumber(2) + $0.TypedKey get hash => $_getN(1); + @$pb.TagNumber(2) + set hash($0.TypedKey v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasHash() => $_has(1); + @$pb.TagNumber(2) + void clearHash() => clearField(2); + @$pb.TagNumber(2) + $0.TypedKey ensureHash() => $_ensure(1); +} + +class BlockStoreDataReference extends $pb.GeneratedMessage { + factory BlockStoreDataReference() => create(); + BlockStoreDataReference._() : super(); + factory BlockStoreDataReference.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory BlockStoreDataReference.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'BlockStoreDataReference', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'block', subBuilder: $0.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') + BlockStoreDataReference clone() => BlockStoreDataReference()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + BlockStoreDataReference copyWith(void Function(BlockStoreDataReference) updates) => super.copyWith((message) => updates(message as BlockStoreDataReference)) as BlockStoreDataReference; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static BlockStoreDataReference create() => BlockStoreDataReference._(); + BlockStoreDataReference createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static BlockStoreDataReference getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static BlockStoreDataReference? _defaultInstance; + + @$pb.TagNumber(1) + $0.TypedKey get block => $_getN(0); + @$pb.TagNumber(1) + set block($0.TypedKey v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasBlock() => $_has(0); + @$pb.TagNumber(1) + void clearBlock() => clearField(1); + @$pb.TagNumber(1) + $0.TypedKey ensureBlock() => $_ensure(0); +} + +enum DataReference_Kind { + dhtData, + blockStoreData, + notSet +} + +class DataReference extends $pb.GeneratedMessage { + factory DataReference() => create(); + DataReference._() : super(); + factory DataReference.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory DataReference.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static const $core.Map<$core.int, DataReference_Kind> _DataReference_KindByTag = { + 1 : DataReference_Kind.dhtData, + 2 : DataReference_Kind.blockStoreData, + 0 : DataReference_Kind.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DataReference', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..oo(0, [1, 2]) + ..aOM(1, _omitFieldNames ? '' : 'dhtData', subBuilder: DHTDataReference.create) + ..aOM(2, _omitFieldNames ? '' : 'blockStoreData', subBuilder: BlockStoreDataReference.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') + DataReference clone() => DataReference()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + DataReference copyWith(void Function(DataReference) updates) => super.copyWith((message) => updates(message as DataReference)) as DataReference; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DataReference create() => DataReference._(); + DataReference createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DataReference getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static DataReference? _defaultInstance; + + DataReference_Kind whichKind() => _DataReference_KindByTag[$_whichOneof(0)]!; + void clearKind() => clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + DHTDataReference get dhtData => $_getN(0); + @$pb.TagNumber(1) + set dhtData(DHTDataReference v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasDhtData() => $_has(0); + @$pb.TagNumber(1) + void clearDhtData() => clearField(1); + @$pb.TagNumber(1) + DHTDataReference ensureDhtData() => $_ensure(0); + + @$pb.TagNumber(2) + BlockStoreDataReference get blockStoreData => $_getN(1); + @$pb.TagNumber(2) + set blockStoreData(BlockStoreDataReference v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasBlockStoreData() => $_has(1); + @$pb.TagNumber(2) + void clearBlockStoreData() => clearField(2); + @$pb.TagNumber(2) + BlockStoreDataReference ensureBlockStoreData() => $_ensure(1); +} + enum Attachment_Kind { media, notSet @@ -98,7 +269,7 @@ class AttachmentMedia extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AttachmentMedia', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) ..aOS(1, _omitFieldNames ? '' : 'mime') ..aOS(2, _omitFieldNames ? '' : 'name') - ..aOM<$1.DataReference>(3, _omitFieldNames ? '' : 'content', subBuilder: $1.DataReference.create) + ..aOM(3, _omitFieldNames ? '' : 'content', subBuilder: DataReference.create) ..hasRequiredFields = false ; @@ -142,15 +313,15 @@ class AttachmentMedia extends $pb.GeneratedMessage { void clearName() => clearField(2); @$pb.TagNumber(3) - $1.DataReference get content => $_getN(2); + DataReference get content => $_getN(2); @$pb.TagNumber(3) - set content($1.DataReference v) { setField(3, v); } + set content(DataReference v) { setField(3, v); } @$pb.TagNumber(3) $core.bool hasContent() => $_has(2); @$pb.TagNumber(3) void clearContent() => clearField(3); @$pb.TagNumber(3) - $1.DataReference ensureContent() => $_ensure(2); + DataReference ensureContent() => $_ensure(2); } class Permissions extends $pb.GeneratedMessage { @@ -276,7 +447,7 @@ class ChatSettings extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ChatSettings', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) ..aOS(1, _omitFieldNames ? '' : 'title') ..aOS(2, _omitFieldNames ? '' : 'description') - ..aOM<$1.DataReference>(3, _omitFieldNames ? '' : 'icon', subBuilder: $1.DataReference.create) + ..aOM(3, _omitFieldNames ? '' : 'icon', subBuilder: DataReference.create) ..a<$fixnum.Int64>(4, _omitFieldNames ? '' : 'defaultExpiration', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) ..hasRequiredFields = false ; @@ -321,15 +492,15 @@ class ChatSettings extends $pb.GeneratedMessage { void clearDescription() => clearField(2); @$pb.TagNumber(3) - $1.DataReference get icon => $_getN(2); + DataReference get icon => $_getN(2); @$pb.TagNumber(3) - set icon($1.DataReference v) { setField(3, v); } + set icon(DataReference v) { setField(3, v); } @$pb.TagNumber(3) $core.bool hasIcon() => $_has(2); @$pb.TagNumber(3) void clearIcon() => clearField(3); @$pb.TagNumber(3) - $1.DataReference ensureIcon() => $_ensure(2); + DataReference ensureIcon() => $_ensure(2); @$pb.TagNumber(4) $fixnum.Int64 get defaultExpiration => $_getI64(3); @@ -1224,7 +1395,7 @@ class Profile extends $pb.GeneratedMessage { ..aOS(3, _omitFieldNames ? '' : 'about') ..aOS(4, _omitFieldNames ? '' : 'status') ..e(5, _omitFieldNames ? '' : 'availability', $pb.PbFieldType.OE, defaultOrMaker: Availability.AVAILABILITY_UNSPECIFIED, valueOf: Availability.valueOf, enumValues: Availability.values) - ..aOM<$0.TypedKey>(6, _omitFieldNames ? '' : 'avatar', subBuilder: $0.TypedKey.create) + ..aOM(6, _omitFieldNames ? '' : 'avatar', subBuilder: DataReference.create) ..hasRequiredFields = false ; @@ -1295,15 +1466,15 @@ class Profile extends $pb.GeneratedMessage { void clearAvailability() => clearField(5); @$pb.TagNumber(6) - $0.TypedKey get avatar => $_getN(5); + DataReference get avatar => $_getN(5); @$pb.TagNumber(6) - set avatar($0.TypedKey v) { setField(6, v); } + set avatar(DataReference v) { setField(6, v); } @$pb.TagNumber(6) $core.bool hasAvatar() => $_has(5); @$pb.TagNumber(6) void clearAvatar() => clearField(6); @$pb.TagNumber(6) - $0.TypedKey ensureAvatar() => $_ensure(5); + DataReference ensureAvatar() => $_ensure(5); } class Account extends $pb.GeneratedMessage { diff --git a/lib/proto/veilidchat.pbjson.dart b/lib/proto/veilidchat.pbjson.dart index ed0bda4..29bb11b 100644 --- a/lib/proto/veilidchat.pbjson.dart +++ b/lib/proto/veilidchat.pbjson.dart @@ -65,6 +65,51 @@ final $typed_data.Uint8List scopeDescriptor = $convert.base64Decode( 'CgVTY29wZRIMCghXQVRDSEVSUxAAEg0KCU1PREVSQVRFRBABEgsKB1RBTEtFUlMQAhIOCgpNT0' 'RFUkFUT1JTEAMSCgoGQURNSU5TEAQ='); +@$core.Deprecated('Use dHTDataReferenceDescriptor instead') +const DHTDataReference$json = { + '1': 'DHTDataReference', + '2': [ + {'1': 'dht_data', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'dhtData'}, + {'1': 'hash', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'hash'}, + ], +}; + +/// Descriptor for `DHTDataReference`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List dHTDataReferenceDescriptor = $convert.base64Decode( + 'ChBESFREYXRhUmVmZXJlbmNlEisKCGRodF9kYXRhGAEgASgLMhAudmVpbGlkLlR5cGVkS2V5Ug' + 'dkaHREYXRhEiQKBGhhc2gYAiABKAsyEC52ZWlsaWQuVHlwZWRLZXlSBGhhc2g='); + +@$core.Deprecated('Use blockStoreDataReferenceDescriptor instead') +const BlockStoreDataReference$json = { + '1': 'BlockStoreDataReference', + '2': [ + {'1': 'block', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'block'}, + ], +}; + +/// Descriptor for `BlockStoreDataReference`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List blockStoreDataReferenceDescriptor = $convert.base64Decode( + 'ChdCbG9ja1N0b3JlRGF0YVJlZmVyZW5jZRImCgVibG9jaxgBIAEoCzIQLnZlaWxpZC5UeXBlZE' + 'tleVIFYmxvY2s='); + +@$core.Deprecated('Use dataReferenceDescriptor instead') +const DataReference$json = { + '1': 'DataReference', + '2': [ + {'1': 'dht_data', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.DHTDataReference', '9': 0, '10': 'dhtData'}, + {'1': 'block_store_data', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.BlockStoreDataReference', '9': 0, '10': 'blockStoreData'}, + ], + '8': [ + {'1': 'kind'}, + ], +}; + +/// Descriptor for `DataReference`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List dataReferenceDescriptor = $convert.base64Decode( + 'Cg1EYXRhUmVmZXJlbmNlEjkKCGRodF9kYXRhGAEgASgLMhwudmVpbGlkY2hhdC5ESFREYXRhUm' + 'VmZXJlbmNlSABSB2RodERhdGESTwoQYmxvY2tfc3RvcmVfZGF0YRgCIAEoCzIjLnZlaWxpZGNo' + 'YXQuQmxvY2tTdG9yZURhdGFSZWZlcmVuY2VIAFIOYmxvY2tTdG9yZURhdGFCBgoEa2luZA=='); + @$core.Deprecated('Use attachmentDescriptor instead') const Attachment$json = { '1': 'Attachment', @@ -89,14 +134,14 @@ const AttachmentMedia$json = { '2': [ {'1': 'mime', '3': 1, '4': 1, '5': 9, '10': 'mime'}, {'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'}, - {'1': 'content', '3': 3, '4': 1, '5': 11, '6': '.dht.DataReference', '10': 'content'}, + {'1': 'content', '3': 3, '4': 1, '5': 11, '6': '.veilidchat.DataReference', '10': 'content'}, ], }; /// Descriptor for `AttachmentMedia`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List attachmentMediaDescriptor = $convert.base64Decode( 'Cg9BdHRhY2htZW50TWVkaWESEgoEbWltZRgBIAEoCVIEbWltZRISCgRuYW1lGAIgASgJUgRuYW' - '1lEiwKB2NvbnRlbnQYAyABKAsyEi5kaHQuRGF0YVJlZmVyZW5jZVIHY29udGVudA=='); + '1lEjMKB2NvbnRlbnQYAyABKAsyGS52ZWlsaWRjaGF0LkRhdGFSZWZlcmVuY2VSB2NvbnRlbnQ='); @$core.Deprecated('Use permissionsDescriptor instead') const Permissions$json = { @@ -140,7 +185,7 @@ const ChatSettings$json = { '2': [ {'1': 'title', '3': 1, '4': 1, '5': 9, '10': 'title'}, {'1': 'description', '3': 2, '4': 1, '5': 9, '10': 'description'}, - {'1': 'icon', '3': 3, '4': 1, '5': 11, '6': '.dht.DataReference', '9': 0, '10': 'icon', '17': true}, + {'1': 'icon', '3': 3, '4': 1, '5': 11, '6': '.veilidchat.DataReference', '9': 0, '10': 'icon', '17': true}, {'1': 'default_expiration', '3': 4, '4': 1, '5': 4, '10': 'defaultExpiration'}, ], '8': [ @@ -151,9 +196,9 @@ const ChatSettings$json = { /// Descriptor for `ChatSettings`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List chatSettingsDescriptor = $convert.base64Decode( 'CgxDaGF0U2V0dGluZ3MSFAoFdGl0bGUYASABKAlSBXRpdGxlEiAKC2Rlc2NyaXB0aW9uGAIgAS' - 'gJUgtkZXNjcmlwdGlvbhIrCgRpY29uGAMgASgLMhIuZGh0LkRhdGFSZWZlcmVuY2VIAFIEaWNv' - 'bogBARItChJkZWZhdWx0X2V4cGlyYXRpb24YBCABKARSEWRlZmF1bHRFeHBpcmF0aW9uQgcKBV' - '9pY29u'); + 'gJUgtkZXNjcmlwdGlvbhIyCgRpY29uGAMgASgLMhkudmVpbGlkY2hhdC5EYXRhUmVmZXJlbmNl' + 'SABSBGljb26IAQESLQoSZGVmYXVsdF9leHBpcmF0aW9uGAQgASgEUhFkZWZhdWx0RXhwaXJhdG' + 'lvbkIHCgVfaWNvbg=='); @$core.Deprecated('Use messageDescriptor instead') const Message$json = { @@ -365,7 +410,7 @@ const Profile$json = { {'1': 'about', '3': 3, '4': 1, '5': 9, '10': 'about'}, {'1': 'status', '3': 4, '4': 1, '5': 9, '10': 'status'}, {'1': 'availability', '3': 5, '4': 1, '5': 14, '6': '.veilidchat.Availability', '10': 'availability'}, - {'1': 'avatar', '3': 6, '4': 1, '5': 11, '6': '.veilid.TypedKey', '9': 0, '10': 'avatar', '17': true}, + {'1': 'avatar', '3': 6, '4': 1, '5': 11, '6': '.veilidchat.DataReference', '9': 0, '10': 'avatar', '17': true}, ], '8': [ {'1': '_avatar'}, @@ -376,9 +421,9 @@ const Profile$json = { final $typed_data.Uint8List profileDescriptor = $convert.base64Decode( 'CgdQcm9maWxlEhIKBG5hbWUYASABKAlSBG5hbWUSGgoIcHJvbm91bnMYAiABKAlSCHByb25vdW' '5zEhQKBWFib3V0GAMgASgJUgVhYm91dBIWCgZzdGF0dXMYBCABKAlSBnN0YXR1cxI8CgxhdmFp' - 'bGFiaWxpdHkYBSABKA4yGC52ZWlsaWRjaGF0LkF2YWlsYWJpbGl0eVIMYXZhaWxhYmlsaXR5Ei' - '0KBmF2YXRhchgGIAEoCzIQLnZlaWxpZC5UeXBlZEtleUgAUgZhdmF0YXKIAQFCCQoHX2F2YXRh' - 'cg=='); + 'bGFiaWxpdHkYBSABKA4yGC52ZWlsaWRjaGF0LkF2YWlsYWJpbGl0eVIMYXZhaWxhYmlsaXR5Ej' + 'YKBmF2YXRhchgGIAEoCzIZLnZlaWxpZGNoYXQuRGF0YVJlZmVyZW5jZUgAUgZhdmF0YXKIAQFC' + 'CQoHX2F2YXRhcg=='); @$core.Deprecated('Use accountDescriptor instead') const Account$json = { diff --git a/lib/proto/veilidchat.proto b/lib/proto/veilidchat.proto index dd2de0b..c99a691 100644 --- a/lib/proto/veilidchat.proto +++ b/lib/proto/veilidchat.proto @@ -47,6 +47,31 @@ enum Scope { ADMINS = 4; } +//////////////////////////////////////////////////////////////////////////////////// +// Data +//////////////////////////////////////////////////////////////////////////////////// + +// Reference to data on the DHT +message DHTDataReference { + veilid.TypedKey dht_data = 1; + veilid.TypedKey hash = 2; +} + +// Reference to data on the BlockStore +message BlockStoreDataReference { + veilid.TypedKey block = 1; +} + +// DataReference +// Pointer to data somewhere in Veilid +// Abstraction over DHTData and BlockStore +message DataReference { + oneof kind { + DHTDataReference dht_data = 1; + BlockStoreDataReference block_store_data = 2; + } +} + //////////////////////////////////////////////////////////////////////////////////// // Attachments //////////////////////////////////////////////////////////////////////////////////// @@ -67,10 +92,9 @@ message AttachmentMedia { // Title or filename string name = 2; // Pointer to the data content - dht.DataReference content = 3; + DataReference content = 3; } - //////////////////////////////////////////////////////////////////////////////////// // Chat room controls //////////////////////////////////////////////////////////////////////////////////// @@ -106,7 +130,7 @@ message ChatSettings { // Description for the chat string description = 2; // Icon for the chat - optional dht.DataReference icon = 3; + optional DataReference icon = 3; // Default message expiration duration (in us) uint64 default_expiration = 4; } @@ -285,8 +309,8 @@ message Profile { string status = 4; // Availability Availability availability = 5; - // Avatar DHTData - optional veilid.TypedKey avatar = 6; + // Avatar + optional DataReference avatar = 6; } // A record of an individual account diff --git a/lib/veilid_processor/views/signal_strength_meter.dart b/lib/veilid_processor/views/signal_strength_meter.dart index 4691e87..73842f1 100644 --- a/lib/veilid_processor/views/signal_strength_meter.dart +++ b/lib/veilid_processor/views/signal_strength_meter.dart @@ -1,5 +1,6 @@ import 'package:async_tools/async_tools.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:go_router/go_router.dart'; @@ -11,7 +12,7 @@ import '../../theme/theme.dart'; import '../cubit/connection_state_cubit.dart'; class SignalStrengthMeterWidget extends StatelessWidget { - const SignalStrengthMeterWidget({super.key}); + const SignalStrengthMeterWidget({super.key, this.color, this.inactiveColor}); @override // ignore: prefer_expression_function_bodies @@ -33,32 +34,35 @@ class SignalStrengthMeterWidget extends StatelessWidget { switch (connectionState.attachment.state) { case AttachmentState.detached: iconWidget = Icon(Icons.signal_cellular_nodata, - size: iconSize, color: scale.primaryScale.primaryText); + size: iconSize, + color: this.color ?? scale.primaryScale.primaryText); return; case AttachmentState.detaching: iconWidget = Icon(Icons.signal_cellular_off, - size: iconSize, color: scale.primaryScale.primaryText); + size: iconSize, + color: this.color ?? scale.primaryScale.primaryText); return; case AttachmentState.attaching: value = 0; - color = scale.primaryScale.primaryText; + color = this.color ?? scale.primaryScale.primaryText; case AttachmentState.attachedWeak: value = 1; - color = scale.primaryScale.primaryText; + color = this.color ?? scale.primaryScale.primaryText; case AttachmentState.attachedStrong: value = 2; - color = scale.primaryScale.primaryText; + color = this.color ?? scale.primaryScale.primaryText; case AttachmentState.attachedGood: value = 3; - color = scale.primaryScale.primaryText; + color = this.color ?? scale.primaryScale.primaryText; case AttachmentState.fullyAttached: value = 4; - color = scale.primaryScale.primaryText; + color = this.color ?? scale.primaryScale.primaryText; case AttachmentState.overAttached: value = 4; - color = scale.primaryScale.primaryText; + color = this.color ?? scale.primaryScale.primaryText; } - inactiveColor = scale.primaryScale.primaryText; + inactiveColor = + this.inactiveColor ?? scale.primaryScale.primaryText; iconWidget = SignalStrengthIndicator.bars( value: value, @@ -86,4 +90,16 @@ class SignalStrengthMeterWidget extends StatelessWidget { child: iconWidget); }); } + + //////////////////////////////////////////////////////////////////////////// + final Color? color; + final Color? inactiveColor; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ColorProperty('color', color)) + ..add(ColorProperty('inactiveColor', inactiveColor)); + } } diff --git a/packages/veilid_support/lib/dht_support/proto/dht.proto b/packages/veilid_support/lib/dht_support/proto/dht.proto index c27915c..da1aa15 100644 --- a/packages/veilid_support/lib/dht_support/proto/dht.proto +++ b/packages/veilid_support/lib/dht_support/proto/dht.proto @@ -23,7 +23,6 @@ message DHTData { uint32 size = 4; } - // DHTLog - represents a ring buffer of many elements with append/truncate semantics // Header in subkey 0 of first key follows this structure message DHTLog { @@ -62,27 +61,6 @@ message DHTShortArray { // calculated through iteration } -// Reference to data on the DHT -message DHTDataReference { - veilid.TypedKey dht_data = 1; - veilid.TypedKey hash = 2; -} - -// Reference to data on the BlockStore -message BlockStoreDataReference { - veilid.TypedKey block = 1; -} - -// DataReference -// Pointer to data somewhere in Veilid -// Abstraction over DHTData and BlockStore -message DataReference { - oneof kind { - DHTDataReference dht_data = 1; - BlockStoreDataReference block_store_data = 2; - } -} - // A pointer to an child DHT record message OwnedDHTRecordPointer { // DHT Record key diff --git a/packages/veilid_support/lib/proto/dht.pb.dart b/packages/veilid_support/lib/proto/dht.pb.dart index 814bd22..7a9ac9a 100644 --- a/packages/veilid_support/lib/proto/dht.pb.dart +++ b/packages/veilid_support/lib/proto/dht.pb.dart @@ -195,177 +195,6 @@ class DHTShortArray extends $pb.GeneratedMessage { $core.List<$core.int> get seqs => $_getList(2); } -class DHTDataReference extends $pb.GeneratedMessage { - factory DHTDataReference() => create(); - DHTDataReference._() : super(); - factory DHTDataReference.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory DHTDataReference.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DHTDataReference', package: const $pb.PackageName(_omitMessageNames ? '' : 'dht'), createEmptyInstance: create) - ..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'dhtData', subBuilder: $0.TypedKey.create) - ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'hash', subBuilder: $0.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') - DHTDataReference clone() => DHTDataReference()..mergeFromMessage(this); - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - DHTDataReference copyWith(void Function(DHTDataReference) updates) => super.copyWith((message) => updates(message as DHTDataReference)) as DHTDataReference; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static DHTDataReference create() => DHTDataReference._(); - DHTDataReference createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static DHTDataReference getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static DHTDataReference? _defaultInstance; - - @$pb.TagNumber(1) - $0.TypedKey get dhtData => $_getN(0); - @$pb.TagNumber(1) - set dhtData($0.TypedKey v) { setField(1, v); } - @$pb.TagNumber(1) - $core.bool hasDhtData() => $_has(0); - @$pb.TagNumber(1) - void clearDhtData() => clearField(1); - @$pb.TagNumber(1) - $0.TypedKey ensureDhtData() => $_ensure(0); - - @$pb.TagNumber(2) - $0.TypedKey get hash => $_getN(1); - @$pb.TagNumber(2) - set hash($0.TypedKey v) { setField(2, v); } - @$pb.TagNumber(2) - $core.bool hasHash() => $_has(1); - @$pb.TagNumber(2) - void clearHash() => clearField(2); - @$pb.TagNumber(2) - $0.TypedKey ensureHash() => $_ensure(1); -} - -class BlockStoreDataReference extends $pb.GeneratedMessage { - factory BlockStoreDataReference() => create(); - BlockStoreDataReference._() : super(); - factory BlockStoreDataReference.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory BlockStoreDataReference.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'BlockStoreDataReference', package: const $pb.PackageName(_omitMessageNames ? '' : 'dht'), createEmptyInstance: create) - ..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'block', subBuilder: $0.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') - BlockStoreDataReference clone() => BlockStoreDataReference()..mergeFromMessage(this); - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - BlockStoreDataReference copyWith(void Function(BlockStoreDataReference) updates) => super.copyWith((message) => updates(message as BlockStoreDataReference)) as BlockStoreDataReference; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static BlockStoreDataReference create() => BlockStoreDataReference._(); - BlockStoreDataReference createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static BlockStoreDataReference getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static BlockStoreDataReference? _defaultInstance; - - @$pb.TagNumber(1) - $0.TypedKey get block => $_getN(0); - @$pb.TagNumber(1) - set block($0.TypedKey v) { setField(1, v); } - @$pb.TagNumber(1) - $core.bool hasBlock() => $_has(0); - @$pb.TagNumber(1) - void clearBlock() => clearField(1); - @$pb.TagNumber(1) - $0.TypedKey ensureBlock() => $_ensure(0); -} - -enum DataReference_Kind { - dhtData, - blockStoreData, - notSet -} - -class DataReference extends $pb.GeneratedMessage { - factory DataReference() => create(); - DataReference._() : super(); - factory DataReference.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory DataReference.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - - static const $core.Map<$core.int, DataReference_Kind> _DataReference_KindByTag = { - 1 : DataReference_Kind.dhtData, - 2 : DataReference_Kind.blockStoreData, - 0 : DataReference_Kind.notSet - }; - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DataReference', package: const $pb.PackageName(_omitMessageNames ? '' : 'dht'), createEmptyInstance: create) - ..oo(0, [1, 2]) - ..aOM(1, _omitFieldNames ? '' : 'dhtData', subBuilder: DHTDataReference.create) - ..aOM(2, _omitFieldNames ? '' : 'blockStoreData', subBuilder: BlockStoreDataReference.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') - DataReference clone() => DataReference()..mergeFromMessage(this); - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - DataReference copyWith(void Function(DataReference) updates) => super.copyWith((message) => updates(message as DataReference)) as DataReference; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static DataReference create() => DataReference._(); - DataReference createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static DataReference getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static DataReference? _defaultInstance; - - DataReference_Kind whichKind() => _DataReference_KindByTag[$_whichOneof(0)]!; - void clearKind() => clearField($_whichOneof(0)); - - @$pb.TagNumber(1) - DHTDataReference get dhtData => $_getN(0); - @$pb.TagNumber(1) - set dhtData(DHTDataReference v) { setField(1, v); } - @$pb.TagNumber(1) - $core.bool hasDhtData() => $_has(0); - @$pb.TagNumber(1) - void clearDhtData() => clearField(1); - @$pb.TagNumber(1) - DHTDataReference ensureDhtData() => $_ensure(0); - - @$pb.TagNumber(2) - BlockStoreDataReference get blockStoreData => $_getN(1); - @$pb.TagNumber(2) - set blockStoreData(BlockStoreDataReference v) { setField(2, v); } - @$pb.TagNumber(2) - $core.bool hasBlockStoreData() => $_has(1); - @$pb.TagNumber(2) - void clearBlockStoreData() => clearField(2); - @$pb.TagNumber(2) - BlockStoreDataReference ensureBlockStoreData() => $_ensure(1); -} - class OwnedDHTRecordPointer extends $pb.GeneratedMessage { factory OwnedDHTRecordPointer() => create(); OwnedDHTRecordPointer._() : super(); diff --git a/packages/veilid_support/lib/proto/dht.pbjson.dart b/packages/veilid_support/lib/proto/dht.pbjson.dart index b8575b9..9d505f0 100644 --- a/packages/veilid_support/lib/proto/dht.pbjson.dart +++ b/packages/veilid_support/lib/proto/dht.pbjson.dart @@ -60,51 +60,6 @@ final $typed_data.Uint8List dHTShortArrayDescriptor = $convert.base64Decode( 'Cg1ESFRTaG9ydEFycmF5EiQKBGtleXMYASADKAsyEC52ZWlsaWQuVHlwZWRLZXlSBGtleXMSFA' 'oFaW5kZXgYAiABKAxSBWluZGV4EhIKBHNlcXMYAyADKA1SBHNlcXM='); -@$core.Deprecated('Use dHTDataReferenceDescriptor instead') -const DHTDataReference$json = { - '1': 'DHTDataReference', - '2': [ - {'1': 'dht_data', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'dhtData'}, - {'1': 'hash', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'hash'}, - ], -}; - -/// Descriptor for `DHTDataReference`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List dHTDataReferenceDescriptor = $convert.base64Decode( - 'ChBESFREYXRhUmVmZXJlbmNlEisKCGRodF9kYXRhGAEgASgLMhAudmVpbGlkLlR5cGVkS2V5Ug' - 'dkaHREYXRhEiQKBGhhc2gYAiABKAsyEC52ZWlsaWQuVHlwZWRLZXlSBGhhc2g='); - -@$core.Deprecated('Use blockStoreDataReferenceDescriptor instead') -const BlockStoreDataReference$json = { - '1': 'BlockStoreDataReference', - '2': [ - {'1': 'block', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'block'}, - ], -}; - -/// Descriptor for `BlockStoreDataReference`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List blockStoreDataReferenceDescriptor = $convert.base64Decode( - 'ChdCbG9ja1N0b3JlRGF0YVJlZmVyZW5jZRImCgVibG9jaxgBIAEoCzIQLnZlaWxpZC5UeXBlZE' - 'tleVIFYmxvY2s='); - -@$core.Deprecated('Use dataReferenceDescriptor instead') -const DataReference$json = { - '1': 'DataReference', - '2': [ - {'1': 'dht_data', '3': 1, '4': 1, '5': 11, '6': '.dht.DHTDataReference', '9': 0, '10': 'dhtData'}, - {'1': 'block_store_data', '3': 2, '4': 1, '5': 11, '6': '.dht.BlockStoreDataReference', '9': 0, '10': 'blockStoreData'}, - ], - '8': [ - {'1': 'kind'}, - ], -}; - -/// Descriptor for `DataReference`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List dataReferenceDescriptor = $convert.base64Decode( - 'Cg1EYXRhUmVmZXJlbmNlEjIKCGRodF9kYXRhGAEgASgLMhUuZGh0LkRIVERhdGFSZWZlcmVuY2' - 'VIAFIHZGh0RGF0YRJIChBibG9ja19zdG9yZV9kYXRhGAIgASgLMhwuZGh0LkJsb2NrU3RvcmVE' - 'YXRhUmVmZXJlbmNlSABSDmJsb2NrU3RvcmVEYXRhQgYKBGtpbmQ='); - @$core.Deprecated('Use ownedDHTRecordPointerDescriptor instead') const OwnedDHTRecordPointer$json = { '1': 'OwnedDHTRecordPointer', diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index e3bc5f9..6236b75 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -52,11 +52,10 @@ packages: bloc_advanced_tools: dependency: "direct main" description: - name: bloc_advanced_tools - sha256: "0cf9b3a73a67addfe22ec3f97a1ac240f6ad53870d6b21a980260f390d7901cd" - url: "https://pub.dev" - source: hosted - version: "0.1.2" + path: "../../../bloc_advanced_tools" + relative: true + source: path + version: "0.1.3" boolean_selector: dependency: transitive description: diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index 49c5325..eb96217 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -9,7 +9,7 @@ environment: dependencies: async_tools: ^0.1.2 bloc: ^8.1.4 - bloc_advanced_tools: ^0.1.2 + bloc_advanced_tools: ^0.1.3 charcode: ^1.3.1 collection: ^1.18.0 equatable: ^2.0.5 @@ -24,11 +24,11 @@ dependencies: # veilid: ^0.0.1 path: ../../../veilid/veilid-flutter -# dependency_overrides: +dependency_overrides: # async_tools: # path: ../../../dart_async_tools -# bloc_advanced_tools: -# path: ../../../bloc_advanced_tools + bloc_advanced_tools: + path: ../../../bloc_advanced_tools dev_dependencies: build_runner: ^2.4.10 diff --git a/pubspec.lock b/pubspec.lock index 36fd6d0..d5945ce 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -100,11 +100,10 @@ packages: bloc_advanced_tools: dependency: "direct main" description: - name: bloc_advanced_tools - sha256: "0cf9b3a73a67addfe22ec3f97a1ac240f6ad53870d6b21a980260f390d7901cd" - url: "https://pub.dev" - source: hosted - version: "0.1.2" + path: "../bloc_advanced_tools" + relative: true + source: path + version: "0.1.3" blurry_modal_progress_hud: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index f27a839..7ef00a0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: badges: ^3.1.2 basic_utils: ^5.7.0 bloc: ^8.1.4 - bloc_advanced_tools: ^0.1.2 + bloc_advanced_tools: ^0.1.3 blurry_modal_progress_hud: ^1.1.1 change_case: ^2.1.0 charcode: ^1.3.1 @@ -93,11 +93,11 @@ dependencies: xterm: ^4.0.0 zxing2: ^0.2.3 -# dependency_overrides: +dependency_overrides: # async_tools: # path: ../dart_async_tools -# bloc_advanced_tools: -# path: ../bloc_advanced_tools + bloc_advanced_tools: + path: ../bloc_advanced_tools # flutter_chat_ui: # path: ../flutter_chat_ui From 751022e743e37927123e85f985c9c727e75f44b5 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 15 Jun 2024 00:01:08 -0400 Subject: [PATCH 139/270] checkpoint --- .../cubits/account_record_cubit.dart | 18 + .../account_records_bloc_map_cubit.dart | 3 +- .../views/edit_account_page.dart | 76 ++-- .../views/new_account_page.dart | 13 +- .../views/profile_edit_form.dart | 10 +- .../views/show_recovery_key_page.dart | 12 +- lib/account_manager/views/views.dart | 1 + lib/contacts/cubits/contact_list_cubit.dart | 54 ++- .../active_conversations_bloc_map_cubit.dart | 86 +++-- ...ve_single_contact_chat_bloc_map_cubit.dart | 2 +- .../cubits/conversation_cubit.dart | 331 ++++++++++-------- lib/layout/home/drawer_menu/drawer_menu.dart | 11 +- .../home_account_ready_shell.dart | 20 +- lib/layout/home/home_shell.dart | 23 +- lib/proto/veilidchat.pb.dart | 10 + lib/proto/veilidchat.pbjson.dart | 5 +- lib/proto/veilidchat.proto | 2 + lib/router/cubit/router_cubit.dart | 39 ++- packages/veilid_support/example/pubspec.yaml | 2 +- .../src/dht_record/dht_record.dart | 11 +- .../veilid_support/lib/src/json_tools.dart | 11 +- .../lib/src/protobuf_tools.dart | 11 +- packages/veilid_support/pubspec.lock | 9 +- packages/veilid_support/pubspec.yaml | 10 +- pubspec.lock | 9 +- pubspec.yaml | 6 +- 26 files changed, 482 insertions(+), 303 deletions(-) diff --git a/lib/account_manager/cubits/account_record_cubit.dart b/lib/account_manager/cubits/account_record_cubit.dart index aca66e1..1424ac6 100644 --- a/lib/account_manager/cubits/account_record_cubit.dart +++ b/lib/account_manager/cubits/account_record_cubit.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:protobuf/protobuf.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../proto/proto.dart' as proto; @@ -7,6 +8,11 @@ import '../account_manager.dart'; typedef AccountRecordState = proto.Account; +/// The saved state of a VeilidChat Account on the DHT +/// Used to synchronize status, profile, and options for a specific account +/// across multiple clients. This DHT record is the 'source of truth' for an +/// account and is privately encrypted with an owned recrod from the 'userLogin' +/// tabledb-local storage, encrypted by the unlock code for the account. class AccountRecordCubit extends DefaultDHTRecordCubit { AccountRecordCubit( {required AccountRepository accountRepository, @@ -35,4 +41,16 @@ class AccountRecordCubit extends DefaultDHTRecordCubit { Future close() async { await super.close(); } + + //////////////////////////////////////////////////////////////////////////// + // Public Interface + + Future updateProfile(proto.Profile profile) async { + await record.eventualUpdateProtobuf(proto.Account.fromBuffer, (old) async { + if (old == null || old.profile == profile) { + return null; + } + return old.deepCopy()..profile = profile; + }); + } } diff --git a/lib/account_manager/cubits/account_records_bloc_map_cubit.dart b/lib/account_manager/cubits/account_records_bloc_map_cubit.dart index bf7bce3..32f5856 100644 --- a/lib/account_manager/cubits/account_records_bloc_map_cubit.dart +++ b/lib/account_manager/cubits/account_records_bloc_map_cubit.dart @@ -7,7 +7,8 @@ import '../../account_manager/account_manager.dart'; typedef AccountRecordsBlocMapState = BlocMapState>; -// Map of the logged in user accounts to their account information +/// Map of the logged in user accounts to their AccountRecordCubit +/// Ensures there is an single account record cubit for each logged in account class AccountRecordsBlocMapCubit extends BlocMapCubit, AccountRecordCubit> with StateMapFollower { diff --git a/lib/account_manager/views/edit_account_page.dart b/lib/account_manager/views/edit_account_page.dart index 83e2067..a461bb0 100644 --- a/lib/account_manager/views/edit_account_page.dart +++ b/lib/account_manager/views/edit_account_page.dart @@ -8,8 +8,6 @@ import 'package:go_router/go_router.dart'; import 'package:protobuf/protobuf.dart'; import 'package:veilid_support/veilid_support.dart'; -import '../../contacts/cubits/contact_list_cubit.dart'; -import '../../conversation/conversation.dart'; import '../../layout/default_app_bar.dart'; import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; @@ -41,7 +39,6 @@ class EditAccountPage extends StatefulWidget { } class _EditAccountPageState extends State { - final _formKey = GlobalKey(); bool _isInAsyncCall = false; @override @@ -58,24 +55,37 @@ class _EditAccountPageState extends State { {required Future Function(GlobalKey) onSubmit}) => EditProfileForm( - header: translate('edit_account_page.header'), - instructions: translate('edit_account_page.instructions'), - submitText: translate('edit_account_page.update'), - submitDisabledText: translate('button.waiting_for_network'), - onSubmit: onSubmit); + header: translate('edit_account_page.header'), + instructions: translate('edit_account_page.instructions'), + submitText: translate('edit_account_page.update'), + submitDisabledText: translate('button.waiting_for_network'), + onSubmit: onSubmit, + initialValueCallback: (key) => switch (key) { + EditProfileForm.formFieldName => widget.existingProfile.name, + EditProfileForm.formFieldPronouns => widget.existingProfile.pronouns, + String() => throw UnimplementedError(), + }, + ); @override Widget build(BuildContext context) { final displayModalHUD = _isInAsyncCall; - final accountRecordCubit = context.read(); - final activeConversationsBlocMapCubit = - context.read(); - final contactListCubit = context.read(); + final accountRecordsCubit = context.watch(); + final accountRecordCubit = accountRecordsCubit + .operate(widget.superIdentityRecordKey, closure: (c) => c); return Scaffold( // resizeToAvoidBottomInset: false, appBar: DefaultAppBar( title: Text(translate('edit_account_page.titlebar')), + leading: Navigator.canPop(context) + ? IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + Navigator.pop(context); + }, + ) + : null, actions: [ const SignalStrengthMeterWidget(), IconButton( @@ -92,57 +102,35 @@ class _EditAccountPageState extends State { FocusScope.of(context).unfocus(); try { - final name = _formKey.currentState! + final name = formKey.currentState! .fields[EditProfileForm.formFieldName]!.value as String; - final pronouns = _formKey + final pronouns = formKey .currentState! .fields[EditProfileForm.formFieldPronouns]! .value as String? ?? ''; final newProfile = widget.existingProfile.deepCopy() ..name = name - ..pronouns = pronouns; + ..pronouns = pronouns + ..timestamp = Veilid.instance.now().toInt64(); setState(() { _isInAsyncCall = true; }); try { // Update account profile DHT record - final newValue = await accountRecordCubit.record - .tryWriteProtobuf(proto.Account.fromBuffer, newProfile); - if (newValue != null) { - if (context.mounted) { - await showErrorModal( - context, - translate('edit_account_page.error'), - 'Failed to update profile online'); - return; - } - } + // This triggers ConversationCubits to update + await accountRecordCubit.updateProfile(newProfile); // Update local account profile await AccountRepository.instance.editAccountProfile( widget.superIdentityRecordKey, newProfile); - // Update all conversations with new profile - final updates = >[]; - for (final key in activeConversationsBlocMapCubit.state.keys) { - await activeConversationsBlocMapCubit.operateAsync(key, - closure: (cubit) async { - final newLocalConversation = - cubit.state.asData?.value.localConversation.deepCopy(); - if (newLocalConversation != null) { - newLocalConversation.profile = newProfile; - updates.add(cubit.input.writeLocalConversation( - conversation: newLocalConversation)); - } - }); + if (context.mounted) { + Navigator.canPop(context) + ? GoRouterHelper(context).pop() + : GoRouterHelper(context).go('/'); } - - // Wait for updates - await updates.wait; - - // XXX: how to do this for non-chat contacts? } finally { if (mounted) { setState(() { diff --git a/lib/account_manager/views/new_account_page.dart b/lib/account_manager/views/new_account_page.dart index 88ccef7..65d57ea 100644 --- a/lib/account_manager/views/new_account_page.dart +++ b/lib/account_manager/views/new_account_page.dart @@ -21,7 +21,6 @@ class NewAccountPage extends StatefulWidget { } class _NewAccountPageState extends State { - final _formKey = GlobalKey(); bool _isInAsyncCall = false; @override @@ -61,6 +60,14 @@ class _NewAccountPageState extends State { // resizeToAvoidBottomInset: false, appBar: DefaultAppBar( title: Text(translate('new_account_page.titlebar')), + leading: Navigator.canPop(context) + ? IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + Navigator.pop(context); + }, + ) + : null, actions: [ const SignalStrengthMeterWidget(), IconButton( @@ -77,9 +84,9 @@ class _NewAccountPageState extends State { FocusScope.of(context).unfocus(); try { - final name = _formKey.currentState! + final name = formKey.currentState! .fields[EditProfileForm.formFieldName]!.value as String; - final pronouns = _formKey + final pronouns = formKey .currentState! .fields[EditProfileForm.formFieldPronouns]! .value as String? ?? diff --git a/lib/account_manager/views/profile_edit_form.dart b/lib/account_manager/views/profile_edit_form.dart index a055396..2e14249 100644 --- a/lib/account_manager/views/profile_edit_form.dart +++ b/lib/account_manager/views/profile_edit_form.dart @@ -13,6 +13,7 @@ class EditProfileForm extends StatefulWidget { required this.submitDisabledText, super.key, this.onSubmit, + this.initialValueCallback, }); @override @@ -23,6 +24,7 @@ class EditProfileForm extends StatefulWidget { final Future Function(GlobalKey)? onSubmit; final String submitText; final String submitDisabledText; + final Object? Function(String key)? initialValueCallback; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -34,7 +36,9 @@ class EditProfileForm extends StatefulWidget { Future Function( GlobalKey p1)?>.has('onSubmit', onSubmit)) ..add(StringProperty('submitText', submitText)) - ..add(StringProperty('submitDisabledText', submitDisabledText)); + ..add(StringProperty('submitDisabledText', submitDisabledText)) + ..add(ObjectFlagProperty.has( + 'initialValueCallback', initialValueCallback)); } static const String formFieldName = 'name'; @@ -62,6 +66,8 @@ class _EditProfileFormState extends State { FormBuilderTextField( autofocus: true, name: EditProfileForm.formFieldName, + initialValue: widget.initialValueCallback + ?.call(EditProfileForm.formFieldName) as String?, decoration: InputDecoration(labelText: translate('account.form_name')), maxLength: 64, @@ -73,6 +79,8 @@ class _EditProfileFormState extends State { ), FormBuilderTextField( name: EditProfileForm.formFieldPronouns, + initialValue: widget.initialValueCallback + ?.call(EditProfileForm.formFieldPronouns) as String?, maxLength: 64, decoration: InputDecoration( labelText: translate('account.form_pronouns')), diff --git a/lib/account_manager/views/show_recovery_key_page.dart b/lib/account_manager/views/show_recovery_key_page.dart index 62d4f21..e22e0e1 100644 --- a/lib/account_manager/views/show_recovery_key_page.dart +++ b/lib/account_manager/views/show_recovery_key_page.dart @@ -1,18 +1,12 @@ 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:form_builder_validators/form_builder_validators.dart'; import 'package:go_router/go_router.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 '../account_manager.dart'; class ShowRecoveryKeyPage extends StatefulWidget { const ShowRecoveryKeyPage({required SecretKey secretKey, super.key}) @@ -57,7 +51,11 @@ class ShowRecoveryKeyPageState extends State { Text('ASS: $secretKey'), ElevatedButton( onPressed: () { - GoRouterHelper(context).go('/'); + if (context.mounted) { + Navigator.canPop(context) + ? GoRouterHelper(context).pop() + : GoRouterHelper(context).go('/'); + } }, child: Text(translate('button.finish'))) ]).paddingSymmetric(horizontal: 24, vertical: 8)); diff --git a/lib/account_manager/views/views.dart b/lib/account_manager/views/views.dart index 4214e05..f554e88 100644 --- a/lib/account_manager/views/views.dart +++ b/lib/account_manager/views/views.dart @@ -1,3 +1,4 @@ +export 'edit_account_page.dart'; export 'new_account_page.dart'; export 'profile_widget.dart'; export 'show_recovery_key_page.dart'; diff --git a/lib/contacts/cubits/contact_list_cubit.dart b/lib/contacts/cubits/contact_list_cubit.dart index 9b05cee..fd2f8dd 100644 --- a/lib/contacts/cubits/contact_list_cubit.dart +++ b/lib/contacts/cubits/contact_list_cubit.dart @@ -1,12 +1,14 @@ import 'dart:async'; import 'dart:convert'; +import 'package:async_tools/async_tools.dart'; +import 'package:protobuf/protobuf.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 'conversation_cubit.dart'; +import '../../conversation/cubits/conversation_cubit.dart'; ////////////////////////////////////////////////// // Mutable state for per-account contacts @@ -34,6 +36,54 @@ class ContactListCubit extends DHTShortArrayCubit { return dhtRecord; } + @override + Future close() async { + await _contactProfileUpdateMap.close(); + await super.close(); + } + //////////////////////////////////////////////////////////////////////////// + // Public Interface + + void followContactProfileChanges(TypedKey localConversationRecordKey, + Stream profileStream, proto.Profile? profileState) { + _contactProfileUpdateMap + .follow(localConversationRecordKey, profileStream, profileState, + (remoteProfile) async { + if (remoteProfile == null) { + return; + } + return updateContactRemoteProfile( + localConversationRecordKey: localConversationRecordKey, + remoteProfile: remoteProfile); + }); + } + + Future updateContactRemoteProfile({ + required TypedKey localConversationRecordKey, + required proto.Profile remoteProfile, + }) async { + // Update contact's remoteProfile + await operateWriteEventual((writer) async { + for (var pos = 0; pos < writer.length; pos++) { + final c = await writer.getProtobuf(proto.Contact.fromBuffer, pos); + if (c != null && + c.localConversationRecordKey.toVeilid() == + localConversationRecordKey) { + if (c.remoteProfile == remoteProfile) { + // Unchanged + break; + } + final newContact = c.deepCopy()..remoteProfile = remoteProfile; + final updated = await writer.tryWriteItemProtobuf( + proto.Contact.fromBuffer, pos, newContact); + if (!updated) { + throw DHTExceptionTryAgain(); + } + } + } + }); + } + Future createContact({ required proto.Profile remoteProfile, required SuperIdentity remoteSuperIdentity, @@ -100,4 +150,6 @@ class ContactListCubit extends DHTShortArrayCubit { } final UnlockedAccountInfo _activeAccountInfo; + final _contactProfileUpdateMap = + SingleStateProcessorMap(); } diff --git a/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart b/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart index 6a1fbef..d7bb24d 100644 --- a/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart +++ b/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart @@ -5,10 +5,10 @@ import 'package:meta/meta.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; +import '../../chat_list/cubits/cubits.dart'; import '../../contacts/contacts.dart'; -import '../../conversation/conversation.dart'; import '../../proto/proto.dart' as proto; -import 'cubits.dart'; +import '../conversation.dart'; @immutable class ActiveConversationState extends Equatable { @@ -43,11 +43,13 @@ typedef ActiveConversationsBlocMapState class ActiveConversationsBlocMapCubit extends BlocMapCubit, ActiveConversationCubit> with StateMapFollower { - ActiveConversationsBlocMapCubit( - {required UnlockedAccountInfo unlockedAccountInfo, - required ContactListCubit contactListCubit}) - : _activeAccountInfo = unlockedAccountInfo, - _contactListCubit = contactListCubit; + ActiveConversationsBlocMapCubit({ + required UnlockedAccountInfo unlockedAccountInfo, + required ContactListCubit contactListCubit, + required AccountRecordCubit accountRecordCubit, + }) : _activeAccountInfo = unlockedAccountInfo, + _contactListCubit = contactListCubit, + _accountRecordCubit = accountRecordCubit; //////////////////////////////////////////////////////////////////////////// // Public Interface @@ -57,30 +59,51 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit _addConversation({required proto.Contact contact}) async => - add(() => MapEntry( - contact.localConversationRecordKey.toVeilid(), - TransformerCubit( - ConversationCubit( - activeAccountInfo: _activeAccountInfo, - remoteIdentityPublicKey: contact.identityPublicKey.toVeilid(), - localConversationRecordKey: - contact.localConversationRecordKey.toVeilid(), - remoteConversationRecordKey: - contact.remoteConversationRecordKey.toVeilid(), - ), - // 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)))); + add(() { + final remoteIdentityPublicKey = contact.identityPublicKey.toVeilid(); + final localConversationRecordKey = + contact.localConversationRecordKey.toVeilid(); + final remoteConversationRecordKey = + contact.remoteConversationRecordKey.toVeilid(); + + // Conversation cubit the tracks the state between the local + // and remote halves of a contact's relationship with this account + final conversationCubit = ConversationCubit( + activeAccountInfo: _activeAccountInfo, + remoteIdentityPublicKey: remoteIdentityPublicKey, + localConversationRecordKey: localConversationRecordKey, + remoteConversationRecordKey: remoteConversationRecordKey, + )..watchAccountChanges( + _accountRecordCubit.stream, _accountRecordCubit.state); + _contactListCubit.followContactProfileChanges( + localConversationRecordKey, + conversationCubit.stream.map((x) => x.map( + data: (d) => d.value.remoteConversation?.profile, + loading: (_) => null, + error: (_) => null)), + conversationCubit.state.asData?.value.remoteConversation?.profile); + + // Transformer that only passes through completed/active conversations + // along with the contact that corresponds to the completed + // conversation + final transformedCubit = TransformerCubit< + AsyncValue, + AsyncValue, + ConversationCubit>(conversationCubit, + 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)); + + return MapEntry( + contact.localConversationRecordKey.toVeilid(), transformedCubit); + }); /// StateFollower ///////////////////////// @@ -108,4 +131,5 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit get props => [localConversation, remoteConversation]; } +/// Represents the control channel between two contacts +/// Used to pass profile, identity and status changes, and the messages key for +/// 1-1 chats class ConversationCubit extends Cubit> { ConversationCubit( {required UnlockedAccountInfo activeAccountInfo, @@ -53,6 +59,7 @@ class ConversationCubit extends Cubit> { debugName: 'ConversationCubit::LocalConversation', parent: accountRecordKey, crypto: crypto); + return record; }); }); @@ -80,6 +87,7 @@ class ConversationCubit extends Cubit> { @override Future close() async { await _initWait(); + await _accountSubscription?.cancel(); await _localSubscription?.cancel(); await _remoteSubscription?.cancel(); await _localConversationCubit?.close(); @@ -88,127 +96,16 @@ 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); - } + //////////////////////////////////////////////////////////////////////////// + // Public Interface - 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(Future Function() open) async { - assert(_localConversationCubit == null, - 'shoud not set local conversation twice'); - _localConversationCubit = DefaultDHTRecordCubit( - open: open, decodeState: proto.Conversation.fromBuffer); - _localSubscription = - _localConversationCubit!.stream.listen(_updateLocalConversationState); - } - - // Open remote converation key - Future _setRemoteConversation(Future Function() open) async { - assert(_remoteConversationCubit == null, - 'shoud not set remote conversation twice'); - _remoteConversationCubit = DefaultDHTRecordCubit( - open: open, decodeState: proto.Conversation.fromBuffer); - _remoteSubscription = - _remoteConversationCubit!.stream.listen(_updateRemoteConversationState); - } - - Future delete() async { - final pool = DHTRecordPool.instance; - - await _initWait(); - final localConversationCubit = _localConversationCubit; - final remoteConversationCubit = _remoteConversationCubit; - - final deleteSet = DelayedWaitSet(); - - if (localConversationCubit != null) { - final data = localConversationCubit.state.asData; - if (data == null) { - log.warning('could not delete local conversation'); - return false; - } - - deleteSet.add(() async { - _localConversationCubit = null; - await localConversationCubit.close(); - final conversation = data.value; - final messagesKey = conversation.messages.toVeilid(); - await pool.deleteRecord(messagesKey); - await pool.deleteRecord(_localConversationRecordKey!); - _localConversationRecordKey = null; - }); - } - - if (remoteConversationCubit != null) { - final data = remoteConversationCubit.state.asData; - if (data == null) { - log.warning('could not delete remote conversation'); - return false; - } - - deleteSet.add(() async { - _remoteConversationCubit = null; - await remoteConversationCubit.close(); - final conversation = data.value; - final messagesKey = conversation.messages.toVeilid(); - await pool.deleteRecord(messagesKey); - await pool.deleteRecord(_remoteConversationRecordKey!); - }); - } - - // Commit the delete futures - await deleteSet(); - - return true; - } - - // 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 - // The callback allows for more initialization to occur and for - // cleanup to delete records upon failure of the callback + /// 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 + /// 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, @@ -280,6 +177,167 @@ class ConversationCubit extends Cubit> { return out; } + /// Delete the conversation keys associated with this conversation + Future delete() async { + final pool = DHTRecordPool.instance; + + await _initWait(); + final localConversationCubit = _localConversationCubit; + final remoteConversationCubit = _remoteConversationCubit; + + final deleteSet = DelayedWaitSet(); + + if (localConversationCubit != null) { + final data = localConversationCubit.state.asData; + if (data == null) { + log.warning('could not delete local conversation'); + return false; + } + + deleteSet.add(() async { + _localConversationCubit = null; + await localConversationCubit.close(); + final conversation = data.value; + final messagesKey = conversation.messages.toVeilid(); + await pool.deleteRecord(messagesKey); + await pool.deleteRecord(_localConversationRecordKey!); + _localConversationRecordKey = null; + }); + } + + if (remoteConversationCubit != null) { + final data = remoteConversationCubit.state.asData; + if (data == null) { + log.warning('could not delete remote conversation'); + return false; + } + + deleteSet.add(() async { + _remoteConversationCubit = null; + await remoteConversationCubit.close(); + final conversation = data.value; + final messagesKey = conversation.messages.toVeilid(); + await pool.deleteRecord(messagesKey); + await pool.deleteRecord(_remoteConversationRecordKey!); + }); + } + + // Commit the delete futures + await deleteSet(); + + return true; + } + + /// Force refresh of conversation keys + Future refresh() async { + await _initWait(); + + final lcc = _localConversationCubit; + final rcc = _remoteConversationCubit; + + if (lcc != null) { + await lcc.refreshDefault(); + } + if (rcc != null) { + await rcc.refreshDefault(); + } + } + + /// Watch for account record changes and update the conversation + void watchAccountChanges(Stream> accountStream, + AsyncValue currentState) { + assert(_accountSubscription == null, 'only watch account once'); + _accountSubscription = accountStream.listen(_updateAccountChange); + _updateAccountChange(currentState); + } + + //////////////////////////////////////////////////////////////////////////// + // Private Implementation + + void _updateAccountChange(AsyncValue avaccount) { + final account = avaccount.asData?.value; + if (account == null) { + return; + } + final cubit = _localConversationCubit; + if (cubit == null) { + return; + } + serialFuture((this, _sfUpdateAccountChange), () async { + await cubit.record.eventualUpdateProtobuf(proto.Conversation.fromBuffer, + (old) async { + if (old == null || old.profile == account.profile) { + return null; + } + return old.deepCopy()..profile = account.profile; + }); + }); + } + + 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(Future Function() open) async { + assert(_localConversationCubit == null, + 'shoud not set local conversation twice'); + _localConversationCubit = DefaultDHTRecordCubit( + open: open, decodeState: proto.Conversation.fromBuffer); + _localSubscription = + _localConversationCubit!.stream.listen(_updateLocalConversationState); + } + + // Open remote converation key + Future _setRemoteConversation(Future Function() open) async { + assert(_remoteConversationCubit == null, + 'shoud not set remote conversation twice'); + _remoteConversationCubit = DefaultDHTRecordCubit( + open: open, decodeState: proto.Conversation.fromBuffer); + _remoteSubscription = + _remoteConversationCubit!.stream.listen(_updateRemoteConversationState); + } + // Initialize local messages Future _initLocalMessages({ required UnlockedAccountInfo activeAccountInfo, @@ -299,34 +357,6 @@ class ConversationCubit extends Cubit> { .deleteScope((messages) async => await callback(messages)); } - // Force refresh of conversation keys - Future refresh() async { - await _initWait(); - - final lcc = _localConversationCubit; - final rcc = _remoteConversationCubit; - - if (lcc != null) { - await lcc.refreshDefault(); - } - if (rcc != null) { - await rcc.refreshDefault(); - } - } - - Future writeLocalConversation({ - required proto.Conversation conversation, - }) async { - final update = await _localConversationCubit!.record - .tryWriteProtobuf(proto.Conversation.fromBuffer, conversation); - - if (update != null) { - _updateLocalConversationState(AsyncValue.data(conversation)); - } - - return update; - } - Future _cachedConversationCrypto() async { var conversationCrypto = _conversationCrypto; if (conversationCrypto != null) { @@ -339,6 +369,9 @@ class ConversationCubit extends Cubit> { return conversationCrypto; } + //////////////////////////////////////////////////////////////////////////// + // Fields + final UnlockedAccountInfo _unlockedAccountInfo; final TypedKey _remoteIdentityPublicKey; TypedKey? _localConversationRecordKey; @@ -347,9 +380,9 @@ class ConversationCubit extends Cubit> { DefaultDHTRecordCubit? _remoteConversationCubit; StreamSubscription>? _localSubscription; StreamSubscription>? _remoteSubscription; + StreamSubscription>? _accountSubscription; ConversationState _incrementalState = const ConversationState( localConversation: null, remoteConversation: null); - // VeilidCrypto? _conversationCrypto; final WaitSet _initWait = WaitSet(); } diff --git a/lib/layout/home/drawer_menu/drawer_menu.dart b/lib/layout/home/drawer_menu/drawer_menu.dart index f1e11ff..7b51346 100644 --- a/lib/layout/home/drawer_menu/drawer_menu.dart +++ b/lib/layout/home/drawer_menu/drawer_menu.dart @@ -8,6 +8,7 @@ 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 '../../../theme/theme.dart'; import '../../../tools/tools.dart'; import '../../../veilid_processor/veilid_processor.dart'; @@ -37,8 +38,12 @@ class _DrawerMenuState extends State { }); } - void _doEditClick(TypedKey superIdentityRecordKey) { - // + void _doEditClick( + TypedKey superIdentityRecordKey, proto.Profile existingProfile) { + singleFuture(this, () async { + await GoRouterHelper(context).push('/edit_account', + extra: [superIdentityRecordKey, existingProfile]); + }); } Widget _wrapInBox({required Widget child, required Color color}) => @@ -127,7 +132,7 @@ class _DrawerMenuState extends State { _doSwitchClick(superIdentityRecordKey); }, footerCallback: () { - _doEditClick(superIdentityRecordKey); + _doEditClick(superIdentityRecordKey, value.profile); }), loading: () => _wrapInBox( child: buildProgressIndicator(), 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 7baa37b..5540e77 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 @@ -102,6 +102,9 @@ class HomeAccountReadyShellState extends State { @override Widget build(BuildContext context) { + // XXX: Should probably eliminate this in favor + // of streaming changes into other cubits. Too much rebuilding! + // should not need to 'watch' all these cubits final account = context.watch().state.asData?.value; if (account == null) { return waitingPage(); @@ -121,28 +124,29 @@ class HomeAccountReadyShellState extends State { create: (context) => WaitingInvitationsBlocMapCubit( unlockedAccountInfo: widget.unlockedAccountInfo, account: account) - ..follow(context.watch())), + ..follow(context.read())), // Chat Cubits BlocProvider( create: (context) => ActiveChatCubit(null, - routerCubit: context.watch())), + routerCubit: context.read())), BlocProvider( create: (context) => ChatListCubit( unlockedAccountInfo: widget.unlockedAccountInfo, - activeChatCubit: context.watch(), + activeChatCubit: context.read(), account: account)), // Conversation Cubits BlocProvider( create: (context) => ActiveConversationsBlocMapCubit( unlockedAccountInfo: widget.unlockedAccountInfo, - contactListCubit: context.watch()) - ..follow(context.watch())), + contactListCubit: context.read(), + accountRecordCubit: context.read()) + ..follow(context.read())), BlocProvider( create: (context) => ActiveSingleContactChatBlocMapCubit( unlockedAccountInfo: widget.unlockedAccountInfo, - contactListCubit: context.watch(), - chatListCubit: context.watch()) - ..follow(context.watch())), + contactListCubit: context.read(), + chatListCubit: context.read()) + ..follow(context.read())), ], child: MultiBlocListener(listeners: [ BlocListener HomeShellState(); - final Builder accountReadyBuilder; + final Widget child; } class HomeShellState extends State { @@ -58,13 +59,17 @@ class HomeShellState extends State { case AccountInfoStatus.accountLocked: return const HomeAccountLocked(); case AccountInfoStatus.accountReady: - return MultiProvider(providers: [ - Provider.value( - value: accountInfo.unlockedAccountInfo!, - ), - Provider.value(value: activeCubit), - Provider.value(value: _zoomDrawerController), - ], child: widget.accountReadyBuilder); + return MultiBlocProvider( + providers: [ + BlocProvider.value(value: activeCubit), + ], + child: MultiProvider(providers: [ + Provider.value( + value: accountInfo.unlockedAccountInfo!, + ), + Provider.value( + value: _zoomDrawerController), + ], child: widget.child)); } } diff --git a/lib/proto/veilidchat.pb.dart b/lib/proto/veilidchat.pb.dart index ff755fe..61018fe 100644 --- a/lib/proto/veilidchat.pb.dart +++ b/lib/proto/veilidchat.pb.dart @@ -1396,6 +1396,7 @@ class Profile extends $pb.GeneratedMessage { ..aOS(4, _omitFieldNames ? '' : 'status') ..e(5, _omitFieldNames ? '' : 'availability', $pb.PbFieldType.OE, defaultOrMaker: Availability.AVAILABILITY_UNSPECIFIED, valueOf: Availability.valueOf, enumValues: Availability.values) ..aOM(6, _omitFieldNames ? '' : 'avatar', subBuilder: DataReference.create) + ..a<$fixnum.Int64>(7, _omitFieldNames ? '' : 'timestamp', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) ..hasRequiredFields = false ; @@ -1475,6 +1476,15 @@ class Profile extends $pb.GeneratedMessage { void clearAvatar() => clearField(6); @$pb.TagNumber(6) DataReference ensureAvatar() => $_ensure(5); + + @$pb.TagNumber(7) + $fixnum.Int64 get timestamp => $_getI64(6); + @$pb.TagNumber(7) + set timestamp($fixnum.Int64 v) { $_setInt64(6, v); } + @$pb.TagNumber(7) + $core.bool hasTimestamp() => $_has(6); + @$pb.TagNumber(7) + void clearTimestamp() => clearField(7); } class Account extends $pb.GeneratedMessage { diff --git a/lib/proto/veilidchat.pbjson.dart b/lib/proto/veilidchat.pbjson.dart index 29bb11b..2978fb5 100644 --- a/lib/proto/veilidchat.pbjson.dart +++ b/lib/proto/veilidchat.pbjson.dart @@ -411,6 +411,7 @@ const Profile$json = { {'1': 'status', '3': 4, '4': 1, '5': 9, '10': 'status'}, {'1': 'availability', '3': 5, '4': 1, '5': 14, '6': '.veilidchat.Availability', '10': 'availability'}, {'1': 'avatar', '3': 6, '4': 1, '5': 11, '6': '.veilidchat.DataReference', '9': 0, '10': 'avatar', '17': true}, + {'1': 'timestamp', '3': 7, '4': 1, '5': 4, '10': 'timestamp'}, ], '8': [ {'1': '_avatar'}, @@ -422,8 +423,8 @@ final $typed_data.Uint8List profileDescriptor = $convert.base64Decode( 'CgdQcm9maWxlEhIKBG5hbWUYASABKAlSBG5hbWUSGgoIcHJvbm91bnMYAiABKAlSCHByb25vdW' '5zEhQKBWFib3V0GAMgASgJUgVhYm91dBIWCgZzdGF0dXMYBCABKAlSBnN0YXR1cxI8CgxhdmFp' 'bGFiaWxpdHkYBSABKA4yGC52ZWlsaWRjaGF0LkF2YWlsYWJpbGl0eVIMYXZhaWxhYmlsaXR5Ej' - 'YKBmF2YXRhchgGIAEoCzIZLnZlaWxpZGNoYXQuRGF0YVJlZmVyZW5jZUgAUgZhdmF0YXKIAQFC' - 'CQoHX2F2YXRhcg=='); + 'YKBmF2YXRhchgGIAEoCzIZLnZlaWxpZGNoYXQuRGF0YVJlZmVyZW5jZUgAUgZhdmF0YXKIAQES' + 'HAoJdGltZXN0YW1wGAcgASgEUgl0aW1lc3RhbXBCCQoHX2F2YXRhcg=='); @$core.Deprecated('Use accountDescriptor instead') const Account$json = { diff --git a/lib/proto/veilidchat.proto b/lib/proto/veilidchat.proto index c99a691..38b4690 100644 --- a/lib/proto/veilidchat.proto +++ b/lib/proto/veilidchat.proto @@ -311,6 +311,8 @@ message Profile { Availability availability = 5; // Avatar optional DataReference avatar = 6; + // Timestamp of last change + uint64 timestamp = 7; } // A record of an individual account diff --git a/lib/router/cubit/router_cubit.dart b/lib/router/cubit/router_cubit.dart index b69037d..901a878 100644 --- a/lib/router/cubit/router_cubit.dart +++ b/lib/router/cubit/router_cubit.dart @@ -11,6 +11,7 @@ import 'package:veilid_support/veilid_support.dart'; import '../../../account_manager/account_manager.dart'; import '../../layout/layout.dart'; +import '../../proto/proto.dart' as proto; import '../../settings/settings.dart'; import '../../tools/tools.dart'; import '../../veilid_processor/views/developer.dart'; @@ -20,6 +21,7 @@ part 'router_cubit.g.dart'; final _rootNavKey = GlobalKey(debugLabel: 'rootNavKey'); final _homeNavKey = GlobalKey(debugLabel: 'homeNavKey'); +final _activeNavKey = GlobalKey(debugLabel: 'activeNavKey'); @freezed class RouterState with _$RouterState { @@ -65,21 +67,34 @@ class RouterCubit extends Cubit { List get routes => [ ShellRoute( navigatorKey: _homeNavKey, - builder: (context, state, child) => HomeShell( - accountReadyBuilder: Builder( - builder: (context) => - HomeAccountReadyShell(context: context, child: child))), + builder: (context, state, child) => HomeShell(child: child), routes: [ - GoRoute( - path: '/', - builder: (context, state) => const HomeAccountReadyMain(), - ), - GoRoute( - path: '/chat', - builder: (context, state) => const HomeAccountReadyChat(), - ), + ShellRoute( + navigatorKey: _activeNavKey, + builder: (context, state, child) => + HomeAccountReadyShell(context: context, child: child), + routes: [ + GoRoute( + path: '/', + builder: (context, state) => const HomeAccountReadyMain(), + ), + GoRoute( + path: '/chat', + builder: (context, state) => const HomeAccountReadyChat(), + ), + ]), ], ), + GoRoute( + path: '/edit_account', + builder: (context, state) { + final extra = state.extra! as List; + return EditAccountPage( + superIdentityRecordKey: extra[0]! as TypedKey, + existingProfile: extra[1]! as proto.Profile, + ); + }, + ), GoRoute( path: '/new_account', builder: (context, state) => const NewAccountPage(), diff --git a/packages/veilid_support/example/pubspec.yaml b/packages/veilid_support/example/pubspec.yaml index 2599f5f..8f76235 100644 --- a/packages/veilid_support/example/pubspec.yaml +++ b/packages/veilid_support/example/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: path: ../ dev_dependencies: - async_tools: ^0.1.2 + async_tools: ^0.1.3 integration_test: sdk: flutter lint_hard: ^4.0.0 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 cd6c859..28fa907 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 @@ -308,7 +308,7 @@ class DHTRecord implements DHTDeleteable { /// Each attempt to write the value calls an update function with the /// old value to determine what new value should be attempted for that write. Future eventualUpdateBytes( - Future Function(Uint8List? oldValue) update, + Future Function(Uint8List? oldValue) update, {int subkey = -1, VeilidCrypto? crypto, KeyPair? writer, @@ -323,7 +323,10 @@ class DHTRecord implements DHTDeleteable { do { // Update the data final updatedValue = await update(oldValue); - + if (updatedValue == null) { + // If null is returned from the update, stop trying to do the update + break; + } // Try to write it back to the network oldValue = await tryWriteBytes(updatedValue, subkey: subkey, crypto: crypto, writer: writer, outSeqNum: outSeqNum); @@ -389,7 +392,7 @@ class DHTRecord implements DHTDeleteable { /// Like 'eventualUpdateBytes' but with JSON marshal/unmarshal of the value Future eventualUpdateJson( - T Function(dynamic) fromJson, Future Function(T?) update, + T Function(dynamic) fromJson, Future Function(T?) update, {int subkey = -1, VeilidCrypto? crypto, KeyPair? writer, @@ -399,7 +402,7 @@ class DHTRecord implements DHTDeleteable { /// Like 'eventualUpdateBytes' but with protobuf marshal/unmarshal of the value Future eventualUpdateProtobuf( - T Function(List) fromBuffer, Future Function(T?) update, + T Function(List) fromBuffer, Future Function(T?) update, {int subkey = -1, VeilidCrypto? crypto, KeyPair? writer, diff --git a/packages/veilid_support/lib/src/json_tools.dart b/packages/veilid_support/lib/src/json_tools.dart index c5895d0..dd0f20b 100644 --- a/packages/veilid_support/lib/src/json_tools.dart +++ b/packages/veilid_support/lib/src/json_tools.dart @@ -12,16 +12,19 @@ Uint8List jsonEncodeBytes(Object? object, Uint8List.fromList( utf8.encode(jsonEncode(object, toEncodable: toEncodable))); -Future jsonUpdateBytes(T Function(dynamic) fromJson, - Uint8List? oldBytes, Future Function(T?) update) async { +Future jsonUpdateBytes(T Function(dynamic) fromJson, + Uint8List? oldBytes, Future Function(T?) update) async { final oldObj = oldBytes == null ? null : fromJson(jsonDecode(utf8.decode(oldBytes))); final newObj = await update(oldObj); + if (newObj == null) { + return null; + } 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 94dc6d1..0120e06 100644 --- a/packages/veilid_support/lib/src/protobuf_tools.dart +++ b/packages/veilid_support/lib/src/protobuf_tools.dart @@ -2,16 +2,19 @@ import 'dart:typed_data'; import 'package:protobuf/protobuf.dart'; -Future protobufUpdateBytes( +Future protobufUpdateBytes( T Function(List) fromBuffer, Uint8List? oldBytes, - Future Function(T?) update) async { + Future Function(T?) update) async { final oldObj = oldBytes == null ? null : fromBuffer(oldBytes); final newObj = await update(oldObj); + if (newObj == null) { + return null; + } 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 6236b75..107e8d4 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -36,11 +36,10 @@ packages: async_tools: dependency: "direct main" description: - name: async_tools - sha256: "72590010ed6c6f5cbd5d40e33834abc08a43da6a73ac3c3645517d53899b8684" - url: "https://pub.dev" - source: hosted - version: "0.1.2" + path: "../../../dart_async_tools" + relative: true + source: path + version: "0.1.3" bloc: dependency: "direct main" description: diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index eb96217..b2b0e5c 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -7,7 +7,7 @@ environment: sdk: '>=3.2.0 <4.0.0' dependencies: - async_tools: ^0.1.2 + async_tools: ^0.1.3 bloc: ^8.1.4 bloc_advanced_tools: ^0.1.3 charcode: ^1.3.1 @@ -25,10 +25,10 @@ dependencies: path: ../../../veilid/veilid-flutter dependency_overrides: -# async_tools: -# path: ../../../dart_async_tools - bloc_advanced_tools: - path: ../../../bloc_advanced_tools + async_tools: + path: ../../../dart_async_tools + bloc_advanced_tools: + path: ../../../bloc_advanced_tools dev_dependencies: build_runner: ^2.4.10 diff --git a/pubspec.lock b/pubspec.lock index d5945ce..0855cbc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -60,11 +60,10 @@ packages: async_tools: dependency: "direct main" description: - name: async_tools - sha256: "72590010ed6c6f5cbd5d40e33834abc08a43da6a73ac3c3645517d53899b8684" - url: "https://pub.dev" - source: hosted - version: "0.1.2" + path: "../dart_async_tools" + relative: true + source: path + version: "0.1.3" awesome_extensions: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 7ef00a0..858c226 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: animated_theme_switcher: ^2.0.10 ansicolor: ^2.0.2 archive: ^3.6.1 - async_tools: ^0.1.2 + async_tools: ^0.1.3 awesome_extensions: ^2.0.16 badges: ^3.1.2 basic_utils: ^5.7.0 @@ -94,8 +94,8 @@ dependencies: zxing2: ^0.2.3 dependency_overrides: -# async_tools: -# path: ../dart_async_tools + async_tools: + path: ../dart_async_tools bloc_advanced_tools: path: ../bloc_advanced_tools # flutter_chat_ui: From 2ccad50f9a9cee2bb7acf4799139569a1e2c169e Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 15 Jun 2024 23:29:15 -0400 Subject: [PATCH 140/270] clean up context locators --- .../account_records_bloc_map_cubit.dart | 9 +- lib/account_manager/models/account_info.dart | 6 +- .../models/unlocked_account_info.dart | 14 +- lib/app.dart | 7 +- lib/chat/cubits/chat_component_cubit.dart | 18 +- .../cubits/single_contact_messages_cubit.dart | 31 ++- lib/chat/views/chat_component_widget.dart | 28 +-- lib/chat_list/cubits/chat_list_cubit.dart | 33 ++- .../chat_single_contact_item_widget.dart | 22 +- .../chat_single_contact_list_widget.dart | 7 +- .../cubits/contact_invitation_list_cubit.dart | 52 ++-- .../cubits/contact_request_inbox_cubit.dart | 21 +- .../cubits/waiting_invitation_cubit.dart | 30 +-- .../waiting_invitations_bloc_map_cubit.dart | 18 +- .../models/valid_contact_invitation.dart | 91 +++---- .../views/invitation_dialog.dart | 20 +- lib/contacts/cubits/contact_list_cubit.dart | 71 +++--- lib/contacts/views/contact_item_widget.dart | 54 +++-- lib/contacts/views/contact_list_widget.dart | 7 +- .../active_conversations_bloc_map_cubit.dart | 33 +-- ...ve_single_contact_chat_bloc_map_cubit.dart | 26 +- .../cubits/conversation_cubit.dart | 56 +++-- .../home_account_ready.dart | 1 - .../home_account_ready_main.dart | 12 +- .../home_account_ready_shell.dart | 158 ------------ lib/layout/home/home_shell.dart | 228 +++++++++++++++--- lib/proto/extensions.dart | 5 + lib/proto/veilidchat.pb.dart | 34 ++- lib/proto/veilidchat.pbjson.dart | 22 +- lib/proto/veilidchat.proto | 8 +- lib/router/cubit/router_cubit.dart | 23 +- 31 files changed, 603 insertions(+), 542 deletions(-) delete mode 100644 lib/layout/home/home_account_ready/home_account_ready_shell.dart diff --git a/lib/account_manager/cubits/account_records_bloc_map_cubit.dart b/lib/account_manager/cubits/account_records_bloc_map_cubit.dart index 32f5856..eee6d1f 100644 --- a/lib/account_manager/cubits/account_records_bloc_map_cubit.dart +++ b/lib/account_manager/cubits/account_records_bloc_map_cubit.dart @@ -1,5 +1,6 @@ import 'package:async_tools/async_tools.dart'; import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; +import 'package:provider/provider.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; @@ -12,8 +13,12 @@ typedef AccountRecordsBlocMapState class AccountRecordsBlocMapCubit extends BlocMapCubit, AccountRecordCubit> with StateMapFollower { - AccountRecordsBlocMapCubit(AccountRepository accountRepository) - : _accountRepository = accountRepository; + AccountRecordsBlocMapCubit( + AccountRepository accountRepository, Locator locator) + : _accountRepository = accountRepository { + // Follow the user logins cubit + follow(locator()); + } // Add account record cubit Future _addAccountRecordCubit( diff --git a/lib/account_manager/models/account_info.dart b/lib/account_manager/models/account_info.dart index 8adb2cb..411a72a 100644 --- a/lib/account_manager/models/account_info.dart +++ b/lib/account_manager/models/account_info.dart @@ -1,3 +1,4 @@ +import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; import 'unlocked_account_info.dart'; @@ -10,7 +11,7 @@ enum AccountInfoStatus { } @immutable -class AccountInfo { +class AccountInfo extends Equatable { const AccountInfo({ required this.status, required this.active, @@ -20,4 +21,7 @@ class AccountInfo { final AccountInfoStatus status; final bool active; final UnlockedAccountInfo? unlockedAccountInfo; + + @override + List get props => [status, active, unlockedAccountInfo]; } diff --git a/lib/account_manager/models/unlocked_account_info.dart b/lib/account_manager/models/unlocked_account_info.dart index 4e810eb..2aa5bdb 100644 --- a/lib/account_manager/models/unlocked_account_info.dart +++ b/lib/account_manager/models/unlocked_account_info.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; import 'package:veilid_support/veilid_support.dart'; @@ -7,12 +8,14 @@ import 'local_account/local_account.dart'; import 'user_login/user_login.dart'; @immutable -class UnlockedAccountInfo { +class UnlockedAccountInfo extends Equatable { const UnlockedAccountInfo({ required this.localAccount, required this.userLogin, }); - // + + //////////////////////////////////////////////////////////////////////////// + // Public Interface TypedKey get superIdentityRecordKey => localAccount.superIdentity.recordKey; TypedKey get accountRecordKey => @@ -41,7 +44,12 @@ class UnlockedAccountInfo { return messagesCrypto; } - // + //////////////////////////////////////////////////////////////////////////// + // Fields + final LocalAccount localAccount; final UserLogin userLogin; + + @override + List get props => [localAccount, userLogin]; } diff --git a/lib/app.dart b/lib/app.dart index 19754ce..743a146 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -131,9 +131,8 @@ class VeilidChatApp extends StatelessWidget { PreferencesCubit(PreferencesRepository.instance), ), BlocProvider( - create: (context) => - AccountRecordsBlocMapCubit(AccountRepository.instance) - ..follow(context.read())), + create: (context) => AccountRecordsBlocMapCubit( + AccountRepository.instance, context.read)), ], child: BackgroundTicker( child: _buildShortcuts( @@ -141,7 +140,7 @@ class VeilidChatApp extends StatelessWidget { builder: (context) => MaterialApp.router( debugShowCheckedModeBanner: false, routerConfig: - context.watch().router(), + context.read().router(), title: translate('app.title'), theme: theme, localizationsDelegates: [ diff --git a/lib/chat/cubits/chat_component_cubit.dart b/lib/chat/cubits/chat_component_cubit.dart index 0bf566b..ab5a4b4 100644 --- a/lib/chat/cubits/chat_component_cubit.dart +++ b/lib/chat/cubits/chat_component_cubit.dart @@ -8,6 +8,7 @@ import 'package:flutter/widgets.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:provider/provider.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:veilid_support/veilid_support.dart'; @@ -44,23 +45,28 @@ class ChatComponentCubit extends Cubit { // ignore: prefer_constructors_over_static_methods static ChatComponentCubit singleContact( - {required UnlockedAccountInfo activeAccountInfo, - required proto.Account accountRecordInfo, - required ActiveConversationState activeConversationState, + {required Locator locator, + required ActiveConversationCubit activeConversationCubit, required SingleContactMessagesCubit messagesCubit}) { + // Get account info + final unlockedAccountInfo = + locator().state.unlockedAccountInfo!; + final account = locator().state.asData!.value; + // Make local 'User' - final localUserIdentityKey = activeAccountInfo.identityTypedPublicKey; + final localUserIdentityKey = unlockedAccountInfo.identityTypedPublicKey; final localUser = types.User( id: localUserIdentityKey.toString(), - firstName: accountRecordInfo.profile.name, + firstName: account.profile.name, metadata: {metadataKeyIdentityPublicKey: localUserIdentityKey}); + // Make remote 'User's final remoteUsers = { activeConversationState.contact.identityPublicKey.toVeilid(): types.User( id: activeConversationState.contact.identityPublicKey .toVeilid() .toString(), - firstName: activeConversationState.contact.editedProfile.name, + firstName: activeConversationState.contact.displayName, metadata: { metadataKeyIdentityPublicKey: activeConversationState.contact.identityPublicKey.toVeilid() diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 9f24a90..7ef5a45 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -4,6 +4,7 @@ import 'package:async_tools/async_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/foundation.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'; @@ -50,13 +51,13 @@ typedef SingleContactMessagesState = AsyncValue>; // Builds the reconciled chat record from the local and remote conversation keys class SingleContactMessagesCubit extends Cubit { SingleContactMessagesCubit({ - required UnlockedAccountInfo activeAccountInfo, + required Locator locator, required TypedKey remoteIdentityPublicKey, required TypedKey localConversationRecordKey, required TypedKey localMessagesRecordKey, required TypedKey remoteConversationRecordKey, required TypedKey remoteMessagesRecordKey, - }) : _activeAccountInfo = activeAccountInfo, + }) : _locator = locator, _remoteIdentityPublicKey = remoteIdentityPublicKey, _localConversationRecordKey = localConversationRecordKey, _localMessagesRecordKey = localMessagesRecordKey, @@ -86,6 +87,9 @@ class SingleContactMessagesCubit extends Cubit { // Initialize everything Future _init() async { + _unlockedAccountInfo = + _locator().state.unlockedAccountInfo!; + _unsentMessagesQueue = PersistentQueue( table: 'SingleContactUnsentMessages', key: _remoteConversationRecordKey.toString(), @@ -111,15 +115,15 @@ class SingleContactMessagesCubit extends Cubit { // Make crypto Future _initCrypto() async { - _conversationCrypto = await _activeAccountInfo + _conversationCrypto = await _unlockedAccountInfo .makeConversationCrypto(_remoteIdentityPublicKey); _senderMessageIntegrity = await MessageIntegrity.create( - author: _activeAccountInfo.identityTypedPublicKey); + author: _unlockedAccountInfo.identityTypedPublicKey); } // Open local messages key Future _initSentMessagesCubit() async { - final writer = _activeAccountInfo.identityWriter; + final writer = _unlockedAccountInfo.identityWriter; _sentMessagesCubit = DHTLogCubit( open: () async => DHTLog.openWrite(_localMessagesRecordKey, writer, @@ -149,7 +153,7 @@ class SingleContactMessagesCubit extends Cubit { Future _makeLocalMessagesCrypto() async => VeilidCryptoPrivate.fromTypedKey( - _activeAccountInfo.userLogin.identitySecret, 'tabledb'); + _unlockedAccountInfo.userLogin.identitySecret, 'tabledb'); // Open reconciled chat record key Future _initReconciledMessagesCubit() async { @@ -240,8 +244,10 @@ class SingleContactMessagesCubit extends Cubit { return; } - _reconciliation.reconcileMessages(_activeAccountInfo.identityTypedPublicKey, - sentMessages, _sentMessagesCubit!); + _reconciliation.reconcileMessages( + _unlockedAccountInfo.identityTypedPublicKey, + sentMessages, + _sentMessagesCubit!); // Update the view _renderState(); @@ -278,7 +284,7 @@ class SingleContactMessagesCubit extends Cubit { // Now sign it await _senderMessageIntegrity.signMessage( - message, _activeAccountInfo.identitySecretKey); + message, _unlockedAccountInfo.identitySecretKey); } // Async process to send messages in the background @@ -331,7 +337,7 @@ class SingleContactMessagesCubit extends Cubit { for (final m in reconciledMessages.windowElements) { final isLocal = m.content.author.toVeilid() == - _activeAccountInfo.identityTypedPublicKey; + _unlockedAccountInfo.identityTypedPublicKey; final reconciledTimestamp = Timestamp.fromInt64(m.reconciledTime); final sm = isLocal ? sentMessagesMap[m.content.authorUniqueIdString] : null; @@ -369,7 +375,7 @@ class SingleContactMessagesCubit extends Cubit { // Add common fields // id and signature will get set by _processMessageToSend message - ..author = _activeAccountInfo.identityTypedPublicKey.toProto() + ..author = _unlockedAccountInfo.identityTypedPublicKey.toProto() ..timestamp = Veilid.instance.now().toInt64(); // Put in the queue @@ -402,7 +408,8 @@ class SingleContactMessagesCubit extends Cubit { ///////////////////////////////////////////////////////////////////////// final WaitSet _initWait = WaitSet(); - final UnlockedAccountInfo _activeAccountInfo; + final Locator _locator; + late final UnlockedAccountInfo _unlockedAccountInfo; final TypedKey _remoteIdentityPublicKey; final TypedKey _localConversationRecordKey; final TypedKey _localMessagesRecordKey; diff --git a/lib/chat/views/chat_component_widget.dart b/lib/chat/views/chat_component_widget.dart index a65c5ef..f0f259d 100644 --- a/lib/chat/views/chat_component_widget.dart +++ b/lib/chat/views/chat_component_widget.dart @@ -22,26 +22,15 @@ class ChatComponentWidget extends StatelessWidget { static Widget builder( {required TypedKey localConversationRecordKey, Key? key}) => Builder(builder: (context) { - // Get all watched dependendies - final activeAccountInfo = context.watch(); - final accountRecordInfo = - context.watch().state.asData?.value; - if (accountRecordInfo == null) { - return debugPage('should always have an account record here'); - } - - final avconversation = context.select?>( - (x) => x.state[localConversationRecordKey]); - if (avconversation == null) { + // Get the active conversation cubit + final activeConversationCubit = context + .select( + (x) => x.tryOperate(localConversationRecordKey, + closure: (cubit) => cubit)); + if (activeConversationCubit == null) { return waitingPage(); } - final activeConversationState = avconversation.asData?.value; - if (activeConversationState == null) { - return avconversation.buildNotData(); - } - // Get the messages cubit final messagesCubit = context.select< ActiveSingleContactChatBlocMapCubit, @@ -55,9 +44,8 @@ class ChatComponentWidget extends StatelessWidget { // Make chat component state return BlocProvider( create: (context) => ChatComponentCubit.singleContact( - activeAccountInfo: activeAccountInfo, - accountRecordInfo: accountRecordInfo, - activeConversationState: activeConversationState, + locator: context.read, + activeConversationCubit: activeConversationCubit, messagesCubit: messagesCubit, ), child: ChatComponentWidget._(key: key)); diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index f9dda7f..697297d 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -3,9 +3,9 @@ import 'dart:async'; import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fixnum/fixnum.dart'; +import 'package:provider/provider.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'; @@ -19,21 +19,17 @@ typedef ChatListCubitState = DHTShortArrayBusyState; class ChatListCubit extends DHTShortArrayCubit with StateMapFollowable { ChatListCubit({ - required UnlockedAccountInfo unlockedAccountInfo, - required proto.Account account, - required this.activeChatCubit, - }) : super( - open: () => _open(unlockedAccountInfo, account), + required Locator locator, + required TypedKey accountRecordKey, + required OwnedDHTRecordPointer chatListRecordPointer, + }) : _locator = locator, + super( + open: () => _open(locator, accountRecordKey, chatListRecordPointer), decodeElement: proto.Chat.fromBuffer); - static Future _open( - UnlockedAccountInfo activeAccountInfo, proto.Account account) async { - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - - final chatListRecordKey = account.chatList.toVeilid(); - - final dhtRecord = await DHTShortArray.openOwned(chatListRecordKey, + static Future _open(Locator locator, TypedKey accountRecordKey, + OwnedDHTRecordPointer chatListRecordPointer) async { + final dhtRecord = await DHTShortArray.openOwned(chatListRecordPointer, debugName: 'ChatListCubit::_open::ChatList', parent: accountRecordKey); return dhtRecord; @@ -41,11 +37,11 @@ class ChatListCubit extends DHTShortArrayCubit Future getDefaultChatSettings( proto.Contact contact) async { - final pronouns = contact.editedProfile.pronouns.isEmpty + final pronouns = contact.profile.pronouns.isEmpty ? '' - : ' (${contact.editedProfile.pronouns})'; + : ' [${contact.profile.pronouns}])'; return proto.ChatSettings() - ..title = '${contact.editedProfile.name}$pronouns' + ..title = '${contact.displayName}$pronouns' ..description = '' ..defaultExpiration = Int64.ZERO; } @@ -99,6 +95,7 @@ class ChatListCubit extends DHTShortArrayCubit final deletedItem = // Ensure followers get their changes before we return await syncFollowers(() => operateWrite((writer) async { + final activeChatCubit = _locator(); if (activeChatCubit.state == localConversationRecordKey) { activeChatCubit.setActiveChat(null); } @@ -142,5 +139,5 @@ class ChatListCubit extends DHTShortArrayCubit //////////////////////////////////////////////////////////////////////////// - final ActiveChatCubit activeChatCubit; + final Locator _locator; } 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 ce9cf0e..7fd38cd 100644 --- a/lib/chat_list/views/chat_single_contact_item_widget.dart +++ b/lib/chat_list/views/chat_single_contact_item_widget.dart @@ -28,13 +28,31 @@ class ChatSingleContactItemWidget extends StatelessWidget { _contact.localConversationRecordKey.toVeilid(); final selected = activeChatCubit.state == localConversationRecordKey; + late final String title; + late final String subtitle; + if (_contact.nickname.isNotEmpty) { + title = _contact.nickname; + if (_contact.profile.pronouns.isNotEmpty) { + subtitle = '${_contact.profile.name} (${_contact.profile.pronouns})'; + } else { + subtitle = _contact.profile.name; + } + } else { + title = _contact.profile.name; + if (_contact.profile.pronouns.isNotEmpty) { + subtitle = '(${_contact.profile.pronouns})'; + } else { + subtitle = ''; + } + } + return SliderTile( key: ObjectKey(_contact), disabled: _disabled, selected: selected, tileScale: ScaleKind.secondary, - title: _contact.editedProfile.name, - subtitle: _contact.editedProfile.pronouns, + title: title, + subtitle: subtitle, icon: Icons.chat, onTap: () { singleFuture(activeChatCubit, () async { 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 9053bc6..605ade0 100644 --- a/lib/chat_list/views/chat_single_contact_list_widget.dart +++ b/lib/chat_list/views/chat_single_contact_list_widget.dart @@ -53,10 +53,13 @@ class ChatSingleContactListWidget extends StatelessWidget { if (contact == null) { return false; } - return contact.editedProfile.name + return contact.nickname .toLowerCase() .contains(lowerValue) || - contact.editedProfile.pronouns + contact.profile.name + .toLowerCase() + .contains(lowerValue) || + contact.profile.pronouns .toLowerCase() .contains(lowerValue); }).toList(); diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index 2012089..e5e4740 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -4,6 +4,7 @@ import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; +import 'package:provider/provider.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; @@ -36,22 +37,18 @@ class ContactInvitationListCubit StateMapFollowable { ContactInvitationListCubit({ - required UnlockedAccountInfo unlockedAccountInfo, - required proto.Account account, - }) : _activeAccountInfo = unlockedAccountInfo, - _account = account, + required Locator locator, + required TypedKey accountRecordKey, + required OwnedDHTRecordPointer contactInvitationListRecordPointer, + }) : _locator = locator, + _accountRecordKey = accountRecordKey, super( - open: () => _open(unlockedAccountInfo, account), + open: () => + _open(accountRecordKey, contactInvitationListRecordPointer), decodeElement: proto.ContactInvitationRecord.fromBuffer); - static Future _open( - UnlockedAccountInfo activeAccountInfo, proto.Account account) async { - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - - final contactInvitationListRecordPointer = - account.contactInvitationRecords.toVeilid(); - + static Future _open(TypedKey accountRecordKey, + OwnedDHTRecordPointer contactInvitationListRecordPointer) async { final dhtRecord = await DHTShortArray.openOwned( contactInvitationListRecordPointer, debugName: 'ContactInvitationListCubit::_open::ContactInvitationList', @@ -71,8 +68,12 @@ class ContactInvitationListCubit final crcs = await pool.veilid.bestCryptoSystem(); final contactRequestWriter = await crcs.generateKeyPair(); - final idcs = await _activeAccountInfo.identityCryptoSystem; - final identityWriter = _activeAccountInfo.identityWriter; + final activeAccountInfo = + _locator().state.unlockedAccountInfo!; + final profile = _locator().state.asData!.value.profile; + + final idcs = await activeAccountInfo.identityCryptoSystem; + final identityWriter = activeAccountInfo.identityWriter; // Encrypt the writer secret with the encryption key final encryptedSecret = await encryptionKeyType.encryptSecretToBytes( @@ -90,7 +91,7 @@ class ContactInvitationListCubit await (await pool.createRecord( debugName: 'ContactInvitationListCubit::createInvitation::' 'LocalConversation', - parent: _activeAccountInfo.accountRecordKey, + parent: _accountRecordKey, schema: DHTSchema.smpl( oCnt: 0, members: [DHTSchemaMember(mKey: identityWriter.key, mCnt: 1)]))) @@ -99,9 +100,9 @@ class ContactInvitationListCubit // Make ContactRequestPrivate and encrypt with the writer secret final crpriv = proto.ContactRequestPrivate() ..writerKey = contactRequestWriter.key.toProto() - ..profile = _account.profile + ..profile = profile ..superIdentityRecordKey = - _activeAccountInfo.userLogin.superIdentityRecordKey.toProto() + activeAccountInfo.userLogin.superIdentityRecordKey.toProto() ..chatRecordKey = localConversation.key.toProto() ..expiration = expiration?.toInt64() ?? Int64.ZERO; final crprivbytes = crpriv.writeToBuffer(); @@ -119,7 +120,7 @@ class ContactInvitationListCubit await (await pool.createRecord( debugName: 'ContactInvitationListCubit::createInvitation::' 'ContactRequestInbox', - parent: _activeAccountInfo.accountRecordKey, + parent: _accountRecordKey, schema: DHTSchema.smpl(oCnt: 1, members: [ DHTSchemaMember(mCnt: 1, mKey: contactRequestWriter.key) ]), @@ -172,8 +173,6 @@ class ContactInvitationListCubit {required bool accepted, required TypedKey contactRequestInboxRecordKey}) async { final pool = DHTRecordPool.instance; - final accountRecordKey = - _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; // Remove ContactInvitationRecord from account's list final deletedItem = await operateWrite((writer) async { @@ -198,7 +197,7 @@ class ContactInvitationListCubit await (await pool.openRecordOwned(contactRequestInbox, debugName: 'ContactInvitationListCubit::deleteInvitation::' 'ContactRequestInbox', - parent: accountRecordKey)) + parent: _accountRecordKey)) .scope((contactRequestInbox) async { // Wipe out old invitation so it shows up as invalid await contactRequestInbox.tryWriteBytes(Uint8List(0)); @@ -248,7 +247,7 @@ class ContactInvitationListCubit await (await pool.openRecordRead(contactRequestInboxKey, debugName: 'ContactInvitationListCubit::validateInvitation::' 'ContactRequestInbox', - parent: _activeAccountInfo.accountRecordKey)) + parent: _accountRecordKey)) .maybeDeleteScope(!isSelf, (contactRequestInbox) async { // final contactRequest = await contactRequestInbox @@ -293,8 +292,7 @@ class ContactInvitationListCubit secret: writerSecret); out = ValidContactInvitation( - activeAccountInfo: _activeAccountInfo, - account: _account, + locator: _locator, contactRequestInboxKey: contactRequestInboxKey, contactRequestPrivate: contactRequestPrivate, contactSuperIdentity: contactSuperIdentity, @@ -318,6 +316,6 @@ class ContactInvitationListCubit } // - final UnlockedAccountInfo _activeAccountInfo; - final proto.Account _account; + final Locator _locator; + final TypedKey _accountRecordKey; } diff --git a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart index f65363e..50710e8 100644 --- a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart +++ b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart @@ -1,3 +1,4 @@ +import 'package:provider/provider.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; @@ -7,27 +8,24 @@ import '../../proto/proto.dart' as proto; class ContactRequestInboxCubit extends DefaultDHTRecordCubit { ContactRequestInboxCubit( - {required this.activeAccountInfo, required this.contactInvitationRecord}) + {required Locator locator, required this.contactInvitationRecord}) : super( open: () => _open( - activeAccountInfo: activeAccountInfo, + locator: locator, contactInvitationRecord: contactInvitationRecord), decodeState: (buf) => buf.isEmpty ? null : proto.SignedContactResponse.fromBuffer(buf)); - // ContactRequestInboxCubit.value( - // {required super.record, - // required this.activeAccountInfo, - // required this.contactInvitationRecord}) - // : super.value(decodeState: proto.SignedContactResponse.fromBuffer); - static Future _open( - {required UnlockedAccountInfo activeAccountInfo, + {required Locator locator, required proto.ContactInvitationRecord contactInvitationRecord}) async { final pool = DHTRecordPool.instance; - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + + final unlockedAccountInfo = + locator().state.unlockedAccountInfo!; + final accountRecordKey = unlockedAccountInfo.accountRecordKey; + final writerSecret = contactInvitationRecord.writerSecret.toVeilid(); final recordKey = contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(); @@ -42,6 +40,5 @@ class ContactRequestInboxCubit defaultSubkey: 1); } - final UnlockedAccountInfo activeAccountInfo; final proto.ContactInvitationRecord contactInvitationRecord; } diff --git a/lib/contact_invitation/cubits/waiting_invitation_cubit.dart b/lib/contact_invitation/cubits/waiting_invitation_cubit.dart index d020f4e..4930e4d 100644 --- a/lib/contact_invitation/cubits/waiting_invitation_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitation_cubit.dart @@ -4,9 +4,9 @@ import 'package:async_tools/async_tools.dart'; import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; +import 'package:provider/provider.dart'; import 'package:veilid_support/veilid_support.dart'; -import '../../account_manager/account_manager.dart'; import '../../conversation/conversation.dart'; import '../../proto/proto.dart' as proto; import '../../tools/tools.dart'; @@ -25,24 +25,22 @@ class InvitationStatus extends Equatable { class WaitingInvitationCubit extends AsyncTransformerCubit { WaitingInvitationCubit(ContactRequestInboxCubit super.input, - {required UnlockedAccountInfo activeAccountInfo, - required proto.Account account, + {required Locator locator, required proto.ContactInvitationRecord contactInvitationRecord}) : super( transform: (signedContactResponse) => _transform( signedContactResponse, - activeAccountInfo: activeAccountInfo, - account: account, + locator: locator, contactInvitationRecord: contactInvitationRecord)); static Future> _transform( proto.SignedContactResponse? signedContactResponse, - {required UnlockedAccountInfo activeAccountInfo, - required proto.Account account, + {required Locator locator, required proto.ContactInvitationRecord contactInvitationRecord}) async { if (signedContactResponse == null) { return const AsyncValue.loading(); } + final contactResponseBytes = Uint8List.fromList(signedContactResponse.contactResponse); final contactResponse = @@ -71,7 +69,7 @@ class WaitingInvitationCubit extends AsyncTransformerCubit AsyncValue.data(InvitationStatus( + acceptedContact: AcceptedContact( + remoteProfile: remoteProfile, + remoteIdentity: contactSuperIdentity, + remoteConversationRecordKey: remoteConversationRecordKey, + localConversationRecordKey: localConversationRecordKey)))); } } 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 938a2a5..3fa8388 100644 --- a/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart @@ -1,8 +1,8 @@ import 'package:async_tools/async_tools.dart'; import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; +import 'package:provider/provider.dart'; import 'package:veilid_support/veilid_support.dart'; -import '../../account_manager/account_manager.dart'; import '../../proto/proto.dart' as proto; import 'cubits.dart'; @@ -17,8 +17,12 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit, TypedKey, proto.ContactInvitationRecord> { - WaitingInvitationsBlocMapCubit( - {required this.unlockedAccountInfo, required this.account}); + WaitingInvitationsBlocMapCubit({ + required Locator locator, + }) : _locator = locator { + // Follow the contact invitation list cubit + follow(locator()); + } Future _addWaitingInvitation( {required proto.ContactInvitationRecord @@ -27,10 +31,9 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit accept() async { final pool = DHTRecordPool.instance; try { + final unlockedAccountInfo = + _locator().state.unlockedAccountInfo!; + final accountRecordKey = unlockedAccountInfo.accountRecordKey; + final identityPublicKey = unlockedAccountInfo.identityPublicKey; + // 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 = _contactSuperIdentity.currentInstance.publicKey == - _activeAccountInfo.identityPublicKey; - final accountRecordKey = _activeAccountInfo.accountRecordKey; + final isSelf = + _contactSuperIdentity.currentInstance.publicKey == identityPublicKey; return (await pool.openRecordWrite(_contactRequestInboxKey, _writer, debugName: 'ValidContactInvitation::accept::' @@ -46,43 +49,42 @@ class ValidContactInvitation { // Create local conversation key for this // contact and send via contact response final conversation = ConversationCubit( - activeAccountInfo: _activeAccountInfo, + locator: _locator, remoteIdentityPublicKey: _contactSuperIdentity.currentInstance.typedPublicKey); return conversation.initLocalConversation( - profile: _account.profile, callback: (localConversation) async { - final contactResponse = proto.ContactResponse() - ..accept = true - ..remoteConversationRecordKey = localConversation.key.toProto() - ..superIdentityRecordKey = - _activeAccountInfo.superIdentityRecordKey.toProto(); - final contactResponseBytes = contactResponse.writeToBuffer(); + final contactResponse = proto.ContactResponse() + ..accept = true + ..remoteConversationRecordKey = localConversation.key.toProto() + ..superIdentityRecordKey = + unlockedAccountInfo.superIdentityRecordKey.toProto(); + final contactResponseBytes = contactResponse.writeToBuffer(); - final cs = await pool.veilid - .getCryptoSystem(_contactRequestInboxKey.kind); + final cs = + await pool.veilid.getCryptoSystem(_contactRequestInboxKey.kind); - final identitySignature = await cs.sign( - _activeAccountInfo.identityWriter.key, - _activeAccountInfo.identityWriter.secret, - contactResponseBytes); + final identitySignature = await cs.sign( + unlockedAccountInfo.identityWriter.key, + unlockedAccountInfo.identityWriter.secret, + contactResponseBytes); - final signedContactResponse = proto.SignedContactResponse() - ..contactResponse = contactResponseBytes - ..identitySignature = identitySignature.toProto(); + final signedContactResponse = proto.SignedContactResponse() + ..contactResponse = contactResponseBytes + ..identitySignature = identitySignature.toProto(); - // Write the acceptance to the inbox - await contactRequestInbox - .eventualWriteProtobuf(signedContactResponse, subkey: 1); + // Write the acceptance to the inbox + await contactRequestInbox.eventualWriteProtobuf(signedContactResponse, + subkey: 1); - return AcceptedContact( - remoteProfile: _contactRequestPrivate.profile, - remoteIdentity: _contactSuperIdentity, - remoteConversationRecordKey: - _contactRequestPrivate.chatRecordKey.toVeilid(), - localConversationRecordKey: localConversation.key, - ); - }); + return AcceptedContact( + remoteProfile: _contactRequestPrivate.profile, + remoteIdentity: _contactSuperIdentity, + remoteConversationRecordKey: + _contactRequestPrivate.chatRecordKey.toVeilid(), + localConversationRecordKey: localConversation.key, + ); + }); }); } on Exception catch (e) { log.debug('exception: $e', e); @@ -93,10 +95,14 @@ class ValidContactInvitation { Future reject() async { final pool = DHTRecordPool.instance; + final unlockedAccountInfo = + _locator().state.unlockedAccountInfo!; + final accountRecordKey = unlockedAccountInfo.accountRecordKey; + final identityPublicKey = unlockedAccountInfo.identityPublicKey; + // Ensure we don't delete this if we're trying to chat to self - final isSelf = _contactSuperIdentity.currentInstance.publicKey == - _activeAccountInfo.identityPublicKey; - final accountRecordKey = _activeAccountInfo.accountRecordKey; + final isSelf = + _contactSuperIdentity.currentInstance.publicKey == identityPublicKey; return (await pool.openRecordWrite(_contactRequestInboxKey, _writer, debugName: 'ValidContactInvitation::reject::' @@ -109,12 +115,12 @@ class ValidContactInvitation { final contactResponse = proto.ContactResponse() ..accept = false ..superIdentityRecordKey = - _activeAccountInfo.superIdentityRecordKey.toProto(); + unlockedAccountInfo.superIdentityRecordKey.toProto(); final contactResponseBytes = contactResponse.writeToBuffer(); final identitySignature = await cs.sign( - _activeAccountInfo.identityWriter.key, - _activeAccountInfo.identityWriter.secret, + unlockedAccountInfo.identityWriter.key, + unlockedAccountInfo.identityWriter.secret, contactResponseBytes); final signedContactResponse = proto.SignedContactResponse() @@ -129,8 +135,7 @@ class ValidContactInvitation { } // - final UnlockedAccountInfo _activeAccountInfo; - final proto.Account _account; + final Locator _locator; final TypedKey _contactRequestInboxKey; final SuperIdentity _contactSuperIdentity; final KeyPair _writer; diff --git a/lib/contact_invitation/views/invitation_dialog.dart b/lib/contact_invitation/views/invitation_dialog.dart index 58a4d05..f868277 100644 --- a/lib/contact_invitation/views/invitation_dialog.dart +++ b/lib/contact_invitation/views/invitation_dialog.dart @@ -3,8 +3,8 @@ 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 'package:provider/provider.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; @@ -15,13 +15,14 @@ import '../contact_invitation.dart'; class InvitationDialog extends StatefulWidget { const InvitationDialog( - {required this.modalContext, + {required Locator locator, required this.onValidationCancelled, required this.onValidationSuccess, required this.onValidationFailed, required this.inviteControlIsValid, required this.buildInviteControl, - super.key}); + super.key}) + : _locator = locator; final void Function() onValidationCancelled; final void Function() onValidationSuccess; @@ -32,7 +33,7 @@ class InvitationDialog extends StatefulWidget { InvitationDialogState dialogState, Future Function({required Uint8List inviteData}) validateInviteData) buildInviteControl; - final BuildContext modalContext; + final Locator _locator; @override InvitationDialogState createState() => InvitationDialogState(); @@ -54,8 +55,7 @@ class InvitationDialog extends StatefulWidget { InvitationDialogState dialogState, Future Function({required Uint8List inviteData}) validateInviteData)>.has( - 'buildInviteControl', buildInviteControl)) - ..add(DiagnosticsProperty('modalContext', modalContext)); + 'buildInviteControl', buildInviteControl)); } } @@ -74,8 +74,8 @@ class InvitationDialogState extends State { Future _onAccept() async { final navigator = Navigator.of(context); - final activeAccountInfo = widget.modalContext.read(); - final contactList = widget.modalContext.read(); + final activeAccountInfo = widget._locator(); + final contactList = widget._locator(); setState(() { _isAccepting = true; @@ -90,7 +90,7 @@ class InvitationDialogState extends State { acceptedContact.remoteIdentity.currentInstance.publicKey; if (!isSelf) { await contactList.createContact( - remoteProfile: acceptedContact.remoteProfile, + profile: acceptedContact.remoteProfile, remoteSuperIdentity: acceptedContact.remoteIdentity, remoteConversationRecordKey: acceptedContact.remoteConversationRecordKey, @@ -137,7 +137,7 @@ class InvitationDialogState extends State { }) async { try { final contactInvitationListCubit = - widget.modalContext.read(); + widget._locator(); setState(() { _isValidating = true; diff --git a/lib/contacts/cubits/contact_list_cubit.dart b/lib/contacts/cubits/contact_list_cubit.dart index fd2f8dd..3b66815 100644 --- a/lib/contacts/cubits/contact_list_cubit.dart +++ b/lib/contacts/cubits/contact_list_cubit.dart @@ -3,33 +3,29 @@ import 'dart:convert'; import 'package:async_tools/async_tools.dart'; import 'package:protobuf/protobuf.dart'; +import 'package:provider/provider.dart'; import 'package:veilid_support/veilid_support.dart'; -import '../../account_manager/account_manager.dart'; +import '../../conversation/conversation.dart'; import '../../proto/proto.dart' as proto; import '../../tools/tools.dart'; -import '../../conversation/cubits/conversation_cubit.dart'; ////////////////////////////////////////////////// // Mutable state for per-account contacts class ContactListCubit extends DHTShortArrayCubit { ContactListCubit({ - required UnlockedAccountInfo unlockedAccountInfo, - required proto.Account account, - }) : _activeAccountInfo = unlockedAccountInfo, + required Locator locator, + required TypedKey accountRecordKey, + required OwnedDHTRecordPointer contactListRecordPointer, + }) : _locator = locator, super( - open: () => _open(unlockedAccountInfo, account), + open: () => _open(accountRecordKey, contactListRecordPointer), decodeElement: proto.Contact.fromBuffer); - static Future _open( - UnlockedAccountInfo activeAccountInfo, proto.Account account) async { - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - - final contactListRecordKey = account.contactList.toVeilid(); - - final dhtRecord = await DHTShortArray.openOwned(contactListRecordKey, + static Future _open(TypedKey accountRecordKey, + OwnedDHTRecordPointer contactListRecordPointer) async { + final dhtRecord = await DHTShortArray.openOwned(contactListRecordPointer, debugName: 'ContactListCubit::_open::ContactList', parent: accountRecordKey); @@ -52,15 +48,15 @@ class ContactListCubit extends DHTShortArrayCubit { if (remoteProfile == null) { return; } - return updateContactRemoteProfile( + return updateContactProfile( localConversationRecordKey: localConversationRecordKey, - remoteProfile: remoteProfile); + profile: remoteProfile); }); } - Future updateContactRemoteProfile({ + Future updateContactProfile({ required TypedKey localConversationRecordKey, - required proto.Profile remoteProfile, + required proto.Profile profile, }) async { // Update contact's remoteProfile await operateWriteEventual((writer) async { @@ -69,36 +65,36 @@ class ContactListCubit extends DHTShortArrayCubit { if (c != null && c.localConversationRecordKey.toVeilid() == localConversationRecordKey) { - if (c.remoteProfile == remoteProfile) { + if (c.profile == profile) { // Unchanged break; } - final newContact = c.deepCopy()..remoteProfile = remoteProfile; + final newContact = c.deepCopy()..profile = profile; final updated = await writer.tryWriteItemProtobuf( proto.Contact.fromBuffer, pos, newContact); if (!updated) { throw DHTExceptionTryAgain(); } + break; } } }); } Future createContact({ - required proto.Profile remoteProfile, + required proto.Profile profile, required SuperIdentity remoteSuperIdentity, - required TypedKey remoteConversationRecordKey, required TypedKey localConversationRecordKey, + required TypedKey remoteConversationRecordKey, }) async { // Create Contact final contact = proto.Contact() - ..editedProfile = remoteProfile - ..remoteProfile = remoteProfile + ..profile = profile ..superIdentityJson = jsonEncode(remoteSuperIdentity.toJson()) ..identityPublicKey = remoteSuperIdentity.currentInstance.typedPublicKey.toProto() - ..remoteConversationRecordKey = remoteConversationRecordKey.toProto() ..localConversationRecordKey = localConversationRecordKey.toProto() + ..remoteConversationRecordKey = remoteConversationRecordKey.toProto() ..showAvailability = false; // Add Contact to account's list @@ -108,13 +104,8 @@ class ContactListCubit extends DHTShortArrayCubit { }); } - Future deleteContact({required proto.Contact contact}) async { - final remoteIdentityPublicKey = contact.identityPublicKey.toVeilid(); - final localConversationRecordKey = - contact.localConversationRecordKey.toVeilid(); - final remoteConversationRecordKey = - contact.remoteConversationRecordKey.toVeilid(); - + Future deleteContact( + {required TypedKey localConversationRecordKey}) async { // Remove Contact from account's list final deletedItem = await operateWrite((writer) async { for (var i = 0; i < writer.length; i++) { @@ -122,8 +113,8 @@ class ContactListCubit extends DHTShortArrayCubit { if (item == null) { throw Exception('Failed to get contact'); } - if (item.localConversationRecordKey == - contact.localConversationRecordKey) { + if (item.localConversationRecordKey.toVeilid() == + localConversationRecordKey) { await writer.remove(i); return item; } @@ -135,10 +126,12 @@ class ContactListCubit extends DHTShortArrayCubit { try { // Make a conversation cubit to manipulate the conversation final conversationCubit = ConversationCubit( - activeAccountInfo: _activeAccountInfo, - remoteIdentityPublicKey: remoteIdentityPublicKey, - localConversationRecordKey: localConversationRecordKey, - remoteConversationRecordKey: remoteConversationRecordKey, + locator: _locator, + remoteIdentityPublicKey: deletedItem.identityPublicKey.toVeilid(), + localConversationRecordKey: + deletedItem.localConversationRecordKey.toVeilid(), + remoteConversationRecordKey: + deletedItem.remoteConversationRecordKey.toVeilid(), ); // Delete the local and remote conversation records @@ -149,7 +142,7 @@ class ContactListCubit extends DHTShortArrayCubit { } } - final UnlockedAccountInfo _activeAccountInfo; final _contactProfileUpdateMap = SingleStateProcessorMap(); + final Locator _locator; } diff --git a/lib/contacts/views/contact_item_widget.dart b/lib/contacts/views/contact_item_widget.dart index 3deae23..a7c2836 100644 --- a/lib/contacts/views/contact_item_widget.dart +++ b/lib/contacts/views/contact_item_widget.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -11,18 +10,9 @@ import '../contacts.dart'; class ContactItemWidget extends StatelessWidget { const ContactItemWidget( - {required this.contact, required this.disabled, super.key}); - - final proto.Contact contact; - final bool disabled; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('contact', contact)) - ..add(DiagnosticsProperty('disabled', disabled)); - } + {required proto.Contact contact, required bool disabled, super.key}) + : _disabled = disabled, + _contact = contact; @override // ignore: prefer_expression_function_bodies @@ -30,26 +20,44 @@ class ContactItemWidget extends StatelessWidget { BuildContext context, ) { final localConversationRecordKey = - contact.localConversationRecordKey.toVeilid(); + _contact.localConversationRecordKey.toVeilid(); const selected = false; // xxx: eventually when we have selectable contacts: // activeContactCubit.state == localConversationRecordKey; - final tileDisabled = disabled || context.watch().isBusy; + final tileDisabled = _disabled || context.watch().isBusy; + + late final String title; + late final String subtitle; + if (_contact.nickname.isNotEmpty) { + title = _contact.nickname; + if (_contact.profile.pronouns.isNotEmpty) { + subtitle = '${_contact.profile.name} (${_contact.profile.pronouns})'; + } else { + subtitle = _contact.profile.name; + } + } else { + title = _contact.profile.name; + if (_contact.profile.pronouns.isNotEmpty) { + subtitle = '(${_contact.profile.pronouns})'; + } else { + subtitle = ''; + } + } return SliderTile( - key: ObjectKey(contact), + key: ObjectKey(_contact), disabled: tileDisabled, selected: selected, tileScale: ScaleKind.primary, - title: contact.editedProfile.name, - subtitle: contact.editedProfile.pronouns, + title: title, + subtitle: subtitle, icon: Icons.person, onTap: () async { // Start a chat final chatListCubit = context.read(); - await chatListCubit.getOrCreateChatSingleContact(contact: contact); + await chatListCubit.getOrCreateChatSingleContact(contact: _contact); // Click over to chats if (context.mounted) { await MainPager.of(context) @@ -71,9 +79,15 @@ class ContactItemWidget extends StatelessWidget { localConversationRecordKey: localConversationRecordKey); // Delete the contact itself - await contactListCubit.deleteContact(contact: contact); + await contactListCubit.deleteContact( + localConversationRecordKey: localConversationRecordKey); }) ], ); } + + //////////////////////////////////////////////////////////////////////////// + + final proto.Contact _contact; + final bool _disabled; } diff --git a/lib/contacts/views/contact_list_widget.dart b/lib/contacts/views/contact_list_widget.dart index 6ef3ca0..eda6776 100644 --- a/lib/contacts/views/contact_list_widget.dart +++ b/lib/contacts/views/contact_list_widget.dart @@ -45,10 +45,13 @@ class ContactListWidget extends StatelessWidget { final lowerValue = value.toLowerCase(); return contactList .where((element) => - element.editedProfile.name + element.nickname .toLowerCase() .contains(lowerValue) || - element.editedProfile.pronouns + element.profile.name + .toLowerCase() + .contains(lowerValue) || + element.profile.pronouns .toLowerCase() .contains(lowerValue)) .toList(); diff --git a/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart b/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart index d7bb24d..aa93bfc 100644 --- a/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart +++ b/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart @@ -2,6 +2,7 @@ import 'package:async_tools/async_tools.dart'; import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; +import 'package:provider/provider.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; @@ -44,12 +45,11 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit, ActiveConversationCubit> with StateMapFollower { ActiveConversationsBlocMapCubit({ - required UnlockedAccountInfo unlockedAccountInfo, - required ContactListCubit contactListCubit, - required AccountRecordCubit accountRecordCubit, - }) : _activeAccountInfo = unlockedAccountInfo, - _contactListCubit = contactListCubit, - _accountRecordCubit = accountRecordCubit; + required Locator locator, + }) : _locator = locator { + // Follow the chat list cubit + follow(locator()); + } //////////////////////////////////////////////////////////////////////////// // Public Interface @@ -69,13 +69,20 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit(); + conversationCubit.watchAccountChanges( + accountRecordCubit.stream, accountRecordCubit.state); + + // When remote conversation changes its profile, + // update our local contact + _locator().followContactProfileChanges( localConversationRecordKey, conversationCubit.stream.map((x) => x.map( data: (d) => d.value.remoteConversation?.profile, @@ -112,7 +119,7 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit updateState(TypedKey key, proto.Chat value) async { - final contactList = _contactListCubit.state.state.asData?.value; + final contactList = _locator().state.state.asData?.value; if (contactList == null) { await addState(key, const AsyncValue.loading()); return; @@ -129,7 +136,5 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit> { - ActiveSingleContactChatBlocMapCubit( - {required UnlockedAccountInfo unlockedAccountInfo, - required ContactListCubit contactListCubit, - required ChatListCubit chatListCubit}) - : _activeAccountInfo = unlockedAccountInfo, - _contactListCubit = contactListCubit, - _chatListCubit = chatListCubit; + ActiveSingleContactChatBlocMapCubit({required Locator locator}) + : _locator = locator { + // Follow the active conversations bloc map cubit + follow(locator()); + } Future _addConversationMessages( {required proto.Contact contact, @@ -35,7 +33,7 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit MapEntry( contact.localConversationRecordKey.toVeilid(), SingleContactMessagesCubit( - activeAccountInfo: _activeAccountInfo, + locator: _locator, remoteIdentityPublicKey: contact.identityPublicKey.toVeilid(), localConversationRecordKey: contact.localConversationRecordKey.toVeilid(), @@ -54,7 +52,7 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit updateState( TypedKey key, AsyncValue value) async { // Get the contact object for this single contact chat - final contactList = _contactListCubit.state.state.asData?.value; + final contactList = _locator().state.state.asData?.value; if (contactList == null) { await addState(key, const AsyncValue.loading()); return; @@ -69,7 +67,7 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit().state.state.asData?.value; if (chatList == null) { await addState(key, const AsyncValue.loading()); return; @@ -95,7 +93,5 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit> { ConversationCubit( - {required UnlockedAccountInfo activeAccountInfo, + {required Locator locator, required TypedKey remoteIdentityPublicKey, TypedKey? localConversationRecordKey, TypedKey? remoteConversationRecordKey}) - : _unlockedAccountInfo = activeAccountInfo, + : _locator = locator, _localConversationRecordKey = localConversationRecordKey, _remoteIdentityPublicKey = remoteIdentityPublicKey, _remoteConversationRecordKey = remoteConversationRecordKey, super(const AsyncValue.loading()) { + final unlockedAccountInfo = + _locator().state.unlockedAccountInfo!; + _accountRecordKey = unlockedAccountInfo.accountRecordKey; + _identityWriter = unlockedAccountInfo.identityWriter; + if (_localConversationRecordKey != null) { _initWait.add(() async { await _setLocalConversation(() async { - final accountRecordKey = _unlockedAccountInfo - .userLogin.accountRecordInfo.accountRecord.recordKey; - // Open local record key if it is specified final pool = DHTRecordPool.instance; final crypto = await _cachedConversationCrypto(); - final writer = _unlockedAccountInfo.identityWriter; + final writer = _identityWriter; final record = await pool.openRecordWrite( _localConversationRecordKey!, writer, debugName: 'ConversationCubit::LocalConversation', - parent: accountRecordKey, + parent: _accountRecordKey, crypto: crypto); return record; @@ -68,15 +71,12 @@ class ConversationCubit extends Cubit> { if (_remoteConversationRecordKey != null) { _initWait.add(() async { await _setRemoteConversation(() async { - final accountRecordKey = _unlockedAccountInfo - .userLogin.accountRecordInfo.accountRecord.recordKey; - // Open remote record key if it is specified final pool = DHTRecordPool.instance; final crypto = await _cachedConversationCrypto(); final record = await pool.openRecordRead(_remoteConversationRecordKey, debugName: 'ConversationCubit::RemoteConversation', - parent: accountRecordKey, + parent: _accountRecordKey, crypto: crypto); return record; }); @@ -107,18 +107,19 @@ class ConversationCubit extends Cubit> { /// 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, + {required FutureOr Function(DHTRecord) callback, TypedKey? existingConversationRecordKey}) async { assert(_localConversationRecordKey == null, 'must not have a local conversation yet'); final pool = DHTRecordPool.instance; - final accountRecordKey = _unlockedAccountInfo - .userLogin.accountRecordInfo.accountRecord.recordKey; final crypto = await _cachedConversationCrypto(); - final writer = _unlockedAccountInfo.identityWriter; + final account = _locator().state.asData!.value; + final unlockedAccountInfo = + _locator().state.unlockedAccountInfo!; + final accountRecordKey = unlockedAccountInfo.accountRecordKey; + final writer = unlockedAccountInfo.identityWriter; // Open with SMPL scheme for identity writer late final DHTRecord localConversationRecord; @@ -144,15 +145,13 @@ class ConversationCubit extends Cubit> { .deleteScope((localConversation) async { // Make messages log return _initLocalMessages( - activeAccountInfo: _unlockedAccountInfo, - remoteIdentityPublicKey: _remoteIdentityPublicKey, localConversationKey: localConversation.key, callback: (messages) async { // Create initial local conversation key contents final conversation = proto.Conversation() - ..profile = profile + ..profile = account.profile ..superIdentityJson = jsonEncode( - _unlockedAccountInfo.localAccount.superIdentity.toJson()) + unlockedAccountInfo.localAccount.superIdentity.toJson()) ..messages = messages.recordKey.toProto(); // Write initial conversation to record @@ -340,14 +339,11 @@ class ConversationCubit extends Cubit> { // Initialize local messages Future _initLocalMessages({ - required UnlockedAccountInfo activeAccountInfo, - required TypedKey remoteIdentityPublicKey, required TypedKey localConversationKey, required FutureOr Function(DHTLog) callback, }) async { - final crypto = - await activeAccountInfo.makeConversationCrypto(remoteIdentityPublicKey); - final writer = activeAccountInfo.identityWriter; + final crypto = await _cachedConversationCrypto(); + final writer = _identityWriter; return (await DHTLog.create( debugName: 'ConversationCubit::initLocalMessages::LocalMessages', @@ -362,17 +358,19 @@ class ConversationCubit extends Cubit> { if (conversationCrypto != null) { return conversationCrypto; } - conversationCrypto = await _unlockedAccountInfo + final unlockedAccountInfo = + _locator().state.unlockedAccountInfo!; + conversationCrypto = await unlockedAccountInfo .makeConversationCrypto(_remoteIdentityPublicKey); - _conversationCrypto = conversationCrypto; return conversationCrypto; } //////////////////////////////////////////////////////////////////////////// // Fields - - final UnlockedAccountInfo _unlockedAccountInfo; + final Locator _locator; + late final TypedKey _accountRecordKey; + late final KeyPair _identityWriter; final TypedKey _remoteIdentityPublicKey; TypedKey? _localConversationRecordKey; final TypedKey? _remoteConversationRecordKey; 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 b198f0b..5171239 100644 --- a/lib/layout/home/home_account_ready/home_account_ready.dart +++ b/lib/layout/home/home_account_ready/home_account_ready.dart @@ -1,3 +1,2 @@ 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/home_account_ready_main.dart b/lib/layout/home/home_account_ready/home_account_ready_main.dart index e758ba1..aa9b1ba 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 @@ -6,6 +6,7 @@ import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart'; import '../../../account_manager/account_manager.dart'; import '../../../chat/chat.dart'; +import '../../../proto/proto.dart' as proto; import '../../../theme/theme.dart'; import '../../../tools/tools.dart'; import 'main_pager/main_pager.dart'; @@ -29,7 +30,8 @@ class _HomeAccountReadyMainState extends State { } Widget buildUserPanel() => Builder(builder: (context) { - final account = context.watch().state; + final profile = context.select( + (c) => c.state.asData!.value.profile); final theme = Theme.of(context); final scale = theme.extension()!; @@ -50,9 +52,7 @@ class _HomeAccountReadyMainState extends State { await ctrl.toggle?.call(); //await GoRouterHelper(context).push('/settings'); }).paddingLTRB(0, 0, 8, 0), - asyncValueBuilder(account, - (_, account) => ProfileWidget(profile: account.profile)) - .expanded(), + ProfileWidget(profile: profile).expanded(), ]).paddingAll(8), const MainPager().expanded() ]); @@ -72,8 +72,8 @@ class _HomeAccountReadyMainState extends State { return const NoConversationWidget(); } return ChatComponentWidget.builder( - localConversationRecordKey: activeChatLocalConversationKey, - ); + localConversationRecordKey: activeChatLocalConversationKey, + key: ValueKey(activeChatLocalConversationKey)); } // ignore: prefer_expression_function_bodies 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 deleted file mode 100644 index 5540e77..0000000 --- a/lib/layout/home/home_account_ready/home_account_ready_shell.dart +++ /dev/null @@ -1,158 +0,0 @@ -import 'package:async_tools/async_tools.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.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 '../../../conversation/conversation.dart'; -import '../../../router/router.dart'; -import '../../../theme/theme.dart'; - -class HomeAccountReadyShell extends StatefulWidget { - 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 unlockedAccountInfo = context.watch(); - final routerCubit = context.read(); - - return HomeAccountReadyShell._( - unlockedAccountInfo: unlockedAccountInfo, - routerCubit: routerCubit, - key: key, - child: child); - } - const HomeAccountReadyShell._( - {required this.unlockedAccountInfo, - required this.routerCubit, - required this.child, - super.key}); - - @override - HomeAccountReadyShellState createState() => HomeAccountReadyShellState(); - - final Widget child; - final UnlockedAccountInfo unlockedAccountInfo; - final RouterCubit routerCubit; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty( - 'unlockedAccountInfo', unlockedAccountInfo)) - ..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, (newState) async { - final contactListCubit = context.read(); - final contactInvitationListCubit = - context.read(); - - for (final entry in newState.entries) { - final contactRequestInboxRecordKey = entry.key; - final invStatus = entry.value.asData?.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, - remoteSuperIdentity: acceptedContact.remoteIdentity, - remoteConversationRecordKey: - acceptedContact.remoteConversationRecordKey, - localConversationRecordKey: - acceptedContact.localConversationRecordKey, - ); - } else { - // Reject - await contactInvitationListCubit.deleteInvitation( - accepted: false, - contactRequestInboxRecordKey: contactRequestInboxRecordKey); - } - } - }); - } - - @override - Widget build(BuildContext context) { - // XXX: Should probably eliminate this in favor - // of streaming changes into other cubits. Too much rebuilding! - // should not need to 'watch' all these cubits - final account = context.watch().state.asData?.value; - if (account == null) { - return waitingPage(); - } - return MultiBlocProvider( - providers: [ - // Contact Cubits - BlocProvider( - create: (context) => ContactInvitationListCubit( - unlockedAccountInfo: widget.unlockedAccountInfo, - account: account)), - BlocProvider( - create: (context) => ContactListCubit( - unlockedAccountInfo: widget.unlockedAccountInfo, - account: account)), - BlocProvider( - create: (context) => WaitingInvitationsBlocMapCubit( - unlockedAccountInfo: widget.unlockedAccountInfo, - account: account) - ..follow(context.read())), - // Chat Cubits - BlocProvider( - create: (context) => ActiveChatCubit(null, - routerCubit: context.read())), - BlocProvider( - create: (context) => ChatListCubit( - unlockedAccountInfo: widget.unlockedAccountInfo, - activeChatCubit: context.read(), - account: account)), - // Conversation Cubits - BlocProvider( - create: (context) => ActiveConversationsBlocMapCubit( - unlockedAccountInfo: widget.unlockedAccountInfo, - contactListCubit: context.read(), - accountRecordCubit: context.read()) - ..follow(context.read())), - BlocProvider( - create: (context) => ActiveSingleContactChatBlocMapCubit( - unlockedAccountInfo: widget.unlockedAccountInfo, - contactListCubit: context.read(), - chatListCubit: context.read()) - ..follow(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 c763370..a3c18b6 100644 --- a/lib/layout/home/home_shell.dart +++ b/lib/layout/home/home_shell.dart @@ -1,11 +1,20 @@ import 'dart:math'; +import 'package:async_tools/async_tools.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart'; import 'package:provider/provider.dart'; +import 'package:veilid_support/veilid_support.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 '../../conversation/conversation.dart'; +import '../../proto/proto.dart' as proto; +import '../../router/router.dart'; import '../../theme/theme.dart'; import 'drawer_menu/drawer_menu.dart'; import 'home_account_invalid.dart'; @@ -33,25 +42,133 @@ class HomeShellState extends State { super.dispose(); } - Widget buildWithLogin(BuildContext context) { - final accountInfo = context.watch().state; - final accountRecordsCubit = context.watch(); - if (!accountInfo.active) { + // Process all accepted or rejected invitations + void _invitationStatusListener( + BuildContext context, WaitingInvitationsBlocMapState state) { + _singleInvitationStatusProcessor.updateState(state, (newState) async { + final contactListCubit = context.read(); + final contactInvitationListCubit = + context.read(); + + for (final entry in newState.entries) { + final contactRequestInboxRecordKey = entry.key; + final invStatus = entry.value.asData?.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( + profile: acceptedContact.remoteProfile, + remoteSuperIdentity: acceptedContact.remoteIdentity, + remoteConversationRecordKey: + acceptedContact.remoteConversationRecordKey, + localConversationRecordKey: + acceptedContact.localConversationRecordKey, + ); + } else { + // Reject + await contactInvitationListCubit.deleteInvitation( + accepted: false, + contactRequestInboxRecordKey: contactRequestInboxRecordKey); + } + } + }); + } + + Widget _buildActiveAccount(BuildContext context) { + final accountRecordKey = context.select( + (c) => c.state.unlockedAccountInfo!.accountRecordKey); + final contactListRecordPointer = + context.select( + (c) => c.state.asData?.value.contactList.toVeilid()); + final contactInvitationListRecordPointer = + context.select( + (c) => c.state.asData?.value.contactInvitationRecords.toVeilid()); + final chatListRecordPointer = + context.select( + (c) => c.state.asData?.value.chatList.toVeilid()); + + if (contactListRecordPointer == null || + contactInvitationListRecordPointer == null || + chatListRecordPointer == null) { + return waitingPage(); + } + + return MultiBlocProvider( + providers: [ + // Contact Cubits + BlocProvider( + create: (context) => ContactInvitationListCubit( + locator: context.read, + accountRecordKey: accountRecordKey, + contactInvitationListRecordPointer: + contactInvitationListRecordPointer, + )), + BlocProvider( + create: (context) => ContactListCubit( + locator: context.read, + accountRecordKey: accountRecordKey, + contactListRecordPointer: contactListRecordPointer)), + BlocProvider( + create: (context) => WaitingInvitationsBlocMapCubit( + locator: context.read, + )), + // Chat Cubits + BlocProvider( + create: (context) => ActiveChatCubit(null, + routerCubit: context.read())), + BlocProvider( + create: (context) => ChatListCubit( + locator: context.read, + accountRecordKey: accountRecordKey, + chatListRecordPointer: chatListRecordPointer)), + // Conversation Cubits + BlocProvider( + create: (context) => ActiveConversationsBlocMapCubit( + locator: context.read, + )), + BlocProvider( + create: (context) => ActiveSingleContactChatBlocMapCubit( + locator: context.read, + )), + ], + child: MultiBlocListener(listeners: [ + BlocListener( + listener: _invitationStatusListener, + ) + ], child: widget.child)); + } + + Widget _buildWithLogin(BuildContext context) { + // Get active account info status + final ( + accountInfoStatus, + accountInfoActive, + superIdentityRecordKey + ) = context + .select( + (c) => ( + c.state.status, + c.state.active, + c.state.unlockedAccountInfo?.superIdentityRecordKey + )); + + if (!accountInfoActive) { // If no logged in user is active, show the loading panel return const HomeNoActive(); } - final superIdentityRecordKey = - accountInfo.unlockedAccountInfo?.superIdentityRecordKey; - final activeCubit = superIdentityRecordKey == null - ? null - : accountRecordsCubit.tryOperate(superIdentityRecordKey, - closure: (c) => c); - if (activeCubit == null) { - return waitingPage(); - } - - switch (accountInfo.status) { + switch (accountInfoStatus) { case AccountInfoStatus.noAccount: return const HomeAccountMissing(); case AccountInfoStatus.accountInvalid: @@ -59,17 +176,21 @@ class HomeShellState extends State { case AccountInfoStatus.accountLocked: return const HomeAccountLocked(); case AccountInfoStatus.accountReady: - return MultiBlocProvider( - providers: [ - BlocProvider.value(value: activeCubit), - ], - child: MultiProvider(providers: [ - Provider.value( - value: accountInfo.unlockedAccountInfo!, - ), - Provider.value( - value: _zoomDrawerController), - ], child: widget.child)); + + // Get the current active account record cubit + final activeAccountRecordCubit = + context.select( + (c) => superIdentityRecordKey == null + ? null + : c.tryOperate(superIdentityRecordKey, closure: (x) => x)); + if (activeAccountRecordCubit == null) { + return waitingPage(); + } + + return MultiBlocProvider(providers: [ + BlocProvider.value( + value: activeAccountRecordCubit), + ], child: Builder(builder: _buildActiveAccount)); } } @@ -96,7 +217,9 @@ class HomeShellState extends State { mainScreen: DecoratedBox( decoration: BoxDecoration( color: scale.primaryScale.activeElementBackground), - child: buildWithLogin(context)), + child: Provider.value( + value: _zoomDrawerController, + child: Builder(builder: _buildWithLogin))), borderRadius: 24, showShadow: true, angle: 0, @@ -112,5 +235,54 @@ class HomeShellState extends State { ))); } - final ZoomDrawerController _zoomDrawerController = ZoomDrawerController(); + final _zoomDrawerController = ZoomDrawerController(); + final _singleInvitationStatusProcessor = + SingleStateProcessor(); } + +// class HomeAccountReadyShell extends StatefulWidget { +// 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 unlockedAccountInfo = context.watch(); +// final routerCubit = context.read(); + +// return HomeAccountReadyShell._( +// unlockedAccountInfo: unlockedAccountInfo, +// routerCubit: routerCubit, +// key: key, +// child: child); +// } +// const HomeAccountReadyShell._( +// {required this.unlockedAccountInfo, +// required this.routerCubit, +// required this.child, +// super.key}); + +// @override +// HomeAccountReadyShellState createState() => HomeAccountReadyShellState(); + +// final Widget child; +// final UnlockedAccountInfo unlockedAccountInfo; +// final RouterCubit routerCubit; + +// @override +// void debugFillProperties(DiagnosticPropertiesBuilder properties) { +// super.debugFillProperties(properties); +// properties +// ..add(DiagnosticsProperty( +// 'unlockedAccountInfo', unlockedAccountInfo)) +// ..add(DiagnosticsProperty('routerCubit', routerCubit)); +// } +// } + +// class HomeAccountReadyShellState extends State { +// final SingleStateProcessor +// _singleInvitationStatusProcessor = SingleStateProcessor(); + +// @override +// void initState() { +// super.initState(); +// } +// } diff --git a/lib/proto/extensions.dart b/lib/proto/extensions.dart index 25b8558..a5e212e 100644 --- a/lib/proto/extensions.dart +++ b/lib/proto/extensions.dart @@ -29,3 +29,8 @@ extension MessageExt on proto.Message { static int compareTimestamp(proto.Message a, proto.Message b) => a.timestamp.compareTo(b.timestamp); } + +extension ContactExt on proto.Contact { + String get displayName => + nickname.isNotEmpty ? '$nickname (${profile.name})' : profile.name; +} diff --git a/lib/proto/veilidchat.pb.dart b/lib/proto/veilidchat.pb.dart index 61018fe..f2b43d9 100644 --- a/lib/proto/veilidchat.pb.dart +++ b/lib/proto/veilidchat.pb.dart @@ -1606,13 +1606,14 @@ class Contact extends $pb.GeneratedMessage { 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) + ..aOS(1, _omitFieldNames ? '' : 'nickname') + ..aOM(2, _omitFieldNames ? '' : 'profile', subBuilder: Profile.create) ..aOS(3, _omitFieldNames ? '' : 'superIdentityJson') ..aOM<$0.TypedKey>(4, _omitFieldNames ? '' : 'identityPublicKey', subBuilder: $0.TypedKey.create) ..aOM<$0.TypedKey>(5, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create) ..aOM<$0.TypedKey>(6, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create) ..aOB(7, _omitFieldNames ? '' : 'showAvailability') + ..aOS(8, _omitFieldNames ? '' : 'notes') ..hasRequiredFields = false ; @@ -1638,26 +1639,24 @@ class Contact extends $pb.GeneratedMessage { static Contact? _defaultInstance; @$pb.TagNumber(1) - Profile get editedProfile => $_getN(0); + $core.String get nickname => $_getSZ(0); @$pb.TagNumber(1) - set editedProfile(Profile v) { setField(1, v); } + set nickname($core.String v) { $_setString(0, v); } @$pb.TagNumber(1) - $core.bool hasEditedProfile() => $_has(0); + $core.bool hasNickname() => $_has(0); @$pb.TagNumber(1) - void clearEditedProfile() => clearField(1); - @$pb.TagNumber(1) - Profile ensureEditedProfile() => $_ensure(0); + void clearNickname() => clearField(1); @$pb.TagNumber(2) - Profile get remoteProfile => $_getN(1); + Profile get profile => $_getN(1); @$pb.TagNumber(2) - set remoteProfile(Profile v) { setField(2, v); } + set profile(Profile v) { setField(2, v); } @$pb.TagNumber(2) - $core.bool hasRemoteProfile() => $_has(1); + $core.bool hasProfile() => $_has(1); @$pb.TagNumber(2) - void clearRemoteProfile() => clearField(2); + void clearProfile() => clearField(2); @$pb.TagNumber(2) - Profile ensureRemoteProfile() => $_ensure(1); + Profile ensureProfile() => $_ensure(1); @$pb.TagNumber(3) $core.String get superIdentityJson => $_getSZ(2); @@ -1709,6 +1708,15 @@ class Contact extends $pb.GeneratedMessage { $core.bool hasShowAvailability() => $_has(6); @$pb.TagNumber(7) void clearShowAvailability() => clearField(7); + + @$pb.TagNumber(8) + $core.String get notes => $_getSZ(7); + @$pb.TagNumber(8) + set notes($core.String v) { $_setString(7, v); } + @$pb.TagNumber(8) + $core.bool hasNotes() => $_has(7); + @$pb.TagNumber(8) + void clearNotes() => clearField(8); } class ContactInvitation extends $pb.GeneratedMessage { diff --git a/lib/proto/veilidchat.pbjson.dart b/lib/proto/veilidchat.pbjson.dart index 2978fb5..d59b75c 100644 --- a/lib/proto/veilidchat.pbjson.dart +++ b/lib/proto/veilidchat.pbjson.dart @@ -455,27 +455,27 @@ final $typed_data.Uint8List accountDescriptor = $convert.base64Decode( const Contact$json = { '1': 'Contact', '2': [ - {'1': 'edited_profile', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'editedProfile'}, - {'1': 'remote_profile', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'remoteProfile'}, + {'1': 'nickname', '3': 1, '4': 1, '5': 9, '10': 'nickname'}, + {'1': 'profile', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'profile'}, {'1': 'super_identity_json', '3': 3, '4': 1, '5': 9, '10': 'superIdentityJson'}, {'1': 'identity_public_key', '3': 4, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'identityPublicKey'}, {'1': 'remote_conversation_record_key', '3': 5, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'}, {'1': 'local_conversation_record_key', '3': 6, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'}, {'1': 'show_availability', '3': 7, '4': 1, '5': 8, '10': 'showAvailability'}, + {'1': 'notes', '3': 8, '4': 1, '5': 9, '10': 'notes'}, ], }; /// Descriptor for `Contact`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List contactDescriptor = $convert.base64Decode( - 'CgdDb250YWN0EjoKDmVkaXRlZF9wcm9maWxlGAEgASgLMhMudmVpbGlkY2hhdC5Qcm9maWxlUg' - '1lZGl0ZWRQcm9maWxlEjoKDnJlbW90ZV9wcm9maWxlGAIgASgLMhMudmVpbGlkY2hhdC5Qcm9m' - 'aWxlUg1yZW1vdGVQcm9maWxlEi4KE3N1cGVyX2lkZW50aXR5X2pzb24YAyABKAlSEXN1cGVySW' - 'RlbnRpdHlKc29uEkAKE2lkZW50aXR5X3B1YmxpY19rZXkYBCABKAsyEC52ZWlsaWQuVHlwZWRL' - 'ZXlSEWlkZW50aXR5UHVibGljS2V5ElUKHnJlbW90ZV9jb252ZXJzYXRpb25fcmVjb3JkX2tleR' - 'gFIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIbcmVtb3RlQ29udmVyc2F0aW9uUmVjb3JkS2V5ElMK' - 'HWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAYgASgLMhAudmVpbGlkLlR5cGVkS2V5Uh' - 'psb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRIrChFzaG93X2F2YWlsYWJpbGl0eRgHIAEoCFIQ' - 'c2hvd0F2YWlsYWJpbGl0eQ=='); + 'CgdDb250YWN0EhoKCG5pY2tuYW1lGAEgASgJUghuaWNrbmFtZRItCgdwcm9maWxlGAIgASgLMh' + 'MudmVpbGlkY2hhdC5Qcm9maWxlUgdwcm9maWxlEi4KE3N1cGVyX2lkZW50aXR5X2pzb24YAyAB' + 'KAlSEXN1cGVySWRlbnRpdHlKc29uEkAKE2lkZW50aXR5X3B1YmxpY19rZXkYBCABKAsyEC52ZW' + 'lsaWQuVHlwZWRLZXlSEWlkZW50aXR5UHVibGljS2V5ElUKHnJlbW90ZV9jb252ZXJzYXRpb25f' + 'cmVjb3JkX2tleRgFIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIbcmVtb3RlQ29udmVyc2F0aW9uUm' + 'Vjb3JkS2V5ElMKHWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAYgASgLMhAudmVpbGlk' + 'LlR5cGVkS2V5Uhpsb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRIrChFzaG93X2F2YWlsYWJpbG' + 'l0eRgHIAEoCFIQc2hvd0F2YWlsYWJpbGl0eRIUCgVub3RlcxgIIAEoCVIFbm90ZXM='); @$core.Deprecated('Use contactInvitationDescriptor instead') const ContactInvitation$json = { diff --git a/lib/proto/veilidchat.proto b/lib/proto/veilidchat.proto index 38b4690..0c00db7 100644 --- a/lib/proto/veilidchat.proto +++ b/lib/proto/veilidchat.proto @@ -349,10 +349,10 @@ message Account { // // Stored in ContactList DHTList message Contact { - // Friend's profile as locally edited - Profile edited_profile = 1; + // Friend's nickname + string nickname = 1; // Copy of friend's profile from remote conversation - Profile remote_profile = 2; + Profile profile = 2; // Copy of friend's SuperIdentity in JSON from remote conversation string super_identity_json = 3; // Copy of friend's most recent identity public key from their identityMaster @@ -363,6 +363,8 @@ message Contact { veilid.TypedKey local_conversation_record_key = 6; // Show availability to this contact bool show_availability = 7; + // Notes about this friend + string notes = 8; } //////////////////////////////////////////////////////////////////////////////////// diff --git a/lib/router/cubit/router_cubit.dart b/lib/router/cubit/router_cubit.dart index 901a878..9c5fc84 100644 --- a/lib/router/cubit/router_cubit.dart +++ b/lib/router/cubit/router_cubit.dart @@ -21,7 +21,6 @@ part 'router_cubit.g.dart'; final _rootNavKey = GlobalKey(debugLabel: 'rootNavKey'); final _homeNavKey = GlobalKey(debugLabel: 'homeNavKey'); -final _activeNavKey = GlobalKey(debugLabel: 'activeNavKey'); @freezed class RouterState with _$RouterState { @@ -69,20 +68,14 @@ class RouterCubit extends Cubit { navigatorKey: _homeNavKey, builder: (context, state, child) => HomeShell(child: child), routes: [ - ShellRoute( - navigatorKey: _activeNavKey, - builder: (context, state, child) => - HomeAccountReadyShell(context: context, child: child), - routes: [ - GoRoute( - path: '/', - builder: (context, state) => const HomeAccountReadyMain(), - ), - GoRoute( - path: '/chat', - builder: (context, state) => const HomeAccountReadyChat(), - ), - ]), + GoRoute( + path: '/', + builder: (context, state) => const HomeAccountReadyMain(), + ), + GoRoute( + path: '/chat', + builder: (context, state) => const HomeAccountReadyChat(), + ), ], ), GoRoute( From 360ba436f80150a963a94dd9abbaf8242c529e24 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 16 Jun 2024 22:12:24 -0400 Subject: [PATCH 141/270] refactor --- ...nfo_cubit.dart => account_info_cubit.dart} | 12 +- .../cubits/account_record_cubit.dart | 2 +- .../cubits/active_local_account_cubit.dart | 35 +++ lib/account_manager/cubits/cubits.dart | 3 +- lib/account_manager/models/account_info.dart | 2 +- .../repository/account_repository.dart | 2 +- .../views/edit_account_page.dart | 15 +- lib/app.dart | 5 +- lib/chat/cubits/active_chat_cubit.dart | 8 +- lib/chat/cubits/chat_component_cubit.dart | 258 +++++++++++++----- .../cubits/single_contact_messages_cubit.dart | 2 +- lib/chat/models/chat_component_state.dart | 8 +- .../models/chat_component_state.freezed.dart | 90 ++++-- lib/chat/views/chat_component_widget.dart | 8 +- .../cubits/contact_invitation_list_cubit.dart | 5 +- .../cubits/contact_request_inbox_cubit.dart | 2 +- .../models/valid_contact_invitation.dart | 7 +- .../views/invitation_dialog.dart | 4 +- .../views/paste_invitation_dialog.dart | 19 +- .../views/scan_invitation_dialog.dart | 19 +- .../cubits/conversation_cubit.dart | 11 +- ...ctive_account_page_controller_wrapper.dart | 37 +++ lib/layout/home/drawer_menu/drawer_menu.dart | 15 +- lib/layout/home/home.dart | 3 +- .../main_pager/main_pager.dart | 3 +- .../{home_shell.dart => home_screen.dart} | 150 +++++----- lib/router/cubit/router_cubit.dart | 53 +--- lib/router/cubit/router_cubit.freezed.dart | 38 +-- lib/router/cubit/router_cubit.g.dart | 2 - 29 files changed, 501 insertions(+), 317 deletions(-) rename lib/account_manager/cubits/{active_account_info_cubit.dart => account_info_cubit.dart} (69%) create mode 100644 lib/account_manager/cubits/active_local_account_cubit.dart create mode 100644 lib/layout/home/active_account_page_controller_wrapper.dart rename lib/layout/home/{home_shell.dart => home_screen.dart} (71%) diff --git a/lib/account_manager/cubits/active_account_info_cubit.dart b/lib/account_manager/cubits/account_info_cubit.dart similarity index 69% rename from lib/account_manager/cubits/active_account_info_cubit.dart rename to lib/account_manager/cubits/account_info_cubit.dart index b61401f..234e694 100644 --- a/lib/account_manager/cubits/active_account_info_cubit.dart +++ b/lib/account_manager/cubits/account_info_cubit.dart @@ -1,23 +1,23 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; +import 'package:veilid_support/veilid_support.dart'; import '../models/models.dart'; import '../repository/account_repository.dart'; -class ActiveAccountInfoCubit extends Cubit { - ActiveAccountInfoCubit(AccountRepository accountRepository) +class AccountInfoCubit extends Cubit { + AccountInfoCubit( + AccountRepository accountRepository, TypedKey superIdentityRecordKey) : _accountRepository = accountRepository, - super(accountRepository - .getAccountInfo(accountRepository.getActiveLocalAccount())) { + super(accountRepository.getAccountInfo(superIdentityRecordKey)) { // Subscribe to streams _accountRepositorySubscription = _accountRepository.stream.listen((change) { switch (change) { case AccountRepositoryChange.activeLocalAccount: case AccountRepositoryChange.localAccounts: case AccountRepositoryChange.userLogins: - emit(accountRepository - .getAccountInfo(accountRepository.getActiveLocalAccount())); + emit(accountRepository.getAccountInfo(superIdentityRecordKey)); break; } }); diff --git a/lib/account_manager/cubits/account_record_cubit.dart b/lib/account_manager/cubits/account_record_cubit.dart index 1424ac6..b393161 100644 --- a/lib/account_manager/cubits/account_record_cubit.dart +++ b/lib/account_manager/cubits/account_record_cubit.dart @@ -11,7 +11,7 @@ typedef AccountRecordState = proto.Account; /// The saved state of a VeilidChat Account on the DHT /// Used to synchronize status, profile, and options for a specific account /// across multiple clients. This DHT record is the 'source of truth' for an -/// account and is privately encrypted with an owned recrod from the 'userLogin' +/// account and is privately encrypted with an owned record from the 'userLogin' /// tabledb-local storage, encrypted by the unlock code for the account. class AccountRecordCubit extends DefaultDHTRecordCubit { AccountRecordCubit( diff --git a/lib/account_manager/cubits/active_local_account_cubit.dart b/lib/account_manager/cubits/active_local_account_cubit.dart new file mode 100644 index 0000000..58a9cb8 --- /dev/null +++ b/lib/account_manager/cubits/active_local_account_cubit.dart @@ -0,0 +1,35 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../repository/account_repository.dart'; + +class ActiveLocalAccountCubit extends Cubit { + ActiveLocalAccountCubit(AccountRepository accountRepository) + : _accountRepository = accountRepository, + super(accountRepository.getActiveLocalAccount()) { + // Subscribe to streams + _accountRepositorySubscription = _accountRepository.stream.listen((change) { + switch (change) { + case AccountRepositoryChange.activeLocalAccount: + emit(_accountRepository.getActiveLocalAccount()); + 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/account_manager/cubits/cubits.dart b/lib/account_manager/cubits/cubits.dart index fc794af..f507ff2 100644 --- a/lib/account_manager/cubits/cubits.dart +++ b/lib/account_manager/cubits/cubits.dart @@ -1,5 +1,6 @@ +export 'account_info_cubit.dart'; export 'account_record_cubit.dart'; export 'account_records_bloc_map_cubit.dart'; -export 'active_account_info_cubit.dart'; +export 'active_local_account_cubit.dart'; export 'local_accounts_cubit.dart'; export 'user_logins_cubit.dart'; diff --git a/lib/account_manager/models/account_info.dart b/lib/account_manager/models/account_info.dart index 411a72a..78b330e 100644 --- a/lib/account_manager/models/account_info.dart +++ b/lib/account_manager/models/account_info.dart @@ -7,7 +7,7 @@ enum AccountInfoStatus { noAccount, accountInvalid, accountLocked, - accountReady, + accountUnlocked, } @immutable diff --git a/lib/account_manager/repository/account_repository.dart b/lib/account_manager/repository/account_repository.dart index 762a238..ff916ac 100644 --- a/lib/account_manager/repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository.dart @@ -129,7 +129,7 @@ class AccountRepository { // Got account, decrypted and decoded return AccountInfo( - status: AccountInfoStatus.accountReady, + status: AccountInfoStatus.accountUnlocked, active: active, unlockedAccountInfo: UnlockedAccountInfo(localAccount: localAccount, userLogin: userLogin), diff --git a/lib/account_manager/views/edit_account_page.dart b/lib/account_manager/views/edit_account_page.dart index a461bb0..19b7acf 100644 --- a/lib/account_manager/views/edit_account_page.dart +++ b/lib/account_manager/views/edit_account_page.dart @@ -70,9 +70,6 @@ class _EditAccountPageState extends State { @override Widget build(BuildContext context) { final displayModalHUD = _isInAsyncCall; - final accountRecordsCubit = context.watch(); - final accountRecordCubit = accountRecordsCubit - .operate(widget.superIdentityRecordKey, closure: (c) => c); return Scaffold( // resizeToAvoidBottomInset: false, @@ -118,9 +115,15 @@ class _EditAccountPageState extends State { _isInAsyncCall = true; }); try { - // Update account profile DHT record - // This triggers ConversationCubits to update - await accountRecordCubit.updateProfile(newProfile); + // Look up account cubit for this specific account + final accountRecordsCubit = + context.read(); + await accountRecordsCubit.operateAsync( + widget.superIdentityRecordKey, closure: (c) async { + // Update account profile DHT record + // This triggers ConversationCubits to update + await c.updateProfile(newProfile); + }); // Update local account profile await AccountRepository.instance.editAccountProfile( diff --git a/lib/app.dart b/lib/app.dart index 743a146..522dbc2 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -11,6 +11,7 @@ import 'package:provider/provider.dart'; import 'package:veilid_support/veilid_support.dart'; import 'account_manager/account_manager.dart'; +import 'account_manager/cubits/active_local_account_cubit.dart'; import 'init.dart'; import 'layout/splash.dart'; import 'router/router.dart'; @@ -122,9 +123,9 @@ class VeilidChatApp extends StatelessWidget { create: (context) => UserLoginsCubit(AccountRepository.instance), ), - BlocProvider( + BlocProvider( create: (context) => - ActiveAccountInfoCubit(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 ead18aa..2e72abc 100644 --- a/lib/chat/cubits/active_chat_cubit.dart +++ b/lib/chat/cubits/active_chat_cubit.dart @@ -1,19 +1,13 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:veilid_support/veilid_support.dart'; -import '../../router/router.dart'; - // XXX: if we ever want to have more than one chat 'open', we should put the // operations and state for that here. class ActiveChatCubit extends Cubit { - ActiveChatCubit(super.initialState, {required RouterCubit routerCubit}) - : _routerCubit = routerCubit; + ActiveChatCubit(super.initialState); void setActiveChat(TypedKey? activeChatLocalConversationRecordKey) { emit(activeChatLocalConversationRecordKey); - _routerCubit.setHasActiveChat(activeChatLocalConversationRecordKey != null); } - - final RouterCubit _routerCubit; } diff --git a/lib/chat/cubits/chat_component_cubit.dart b/lib/chat/cubits/chat_component_cubit.dart index ab5a4b4..b3e609f 100644 --- a/lib/chat/cubits/chat_component_cubit.dart +++ b/lib/chat/cubits/chat_component_cubit.dart @@ -27,15 +27,19 @@ const metadataKeyAttachments = 'attachments'; class ChatComponentCubit extends Cubit { ChatComponentCubit._({ + required Locator locator, + required List conversationCubits, required SingleContactMessagesCubit messagesCubit, - required types.User localUser, - required IMap remoteUsers, - }) : _messagesCubit = messagesCubit, + }) : _locator = locator, + _conversationCubits = conversationCubits, + _messagesCubit = messagesCubit, super(ChatComponentState( chatKey: GlobalKey(), scrollController: AutoScrollController(), - localUser: localUser, - remoteUsers: remoteUsers, + localUser: null, + remoteUsers: const IMap.empty(), + historicalRemoteUsers: const IMap.empty(), + unknownUsers: const IMap.empty(), messageWindow: const AsyncLoading(), title: '', )) { @@ -43,58 +47,40 @@ class ChatComponentCubit extends Cubit { _initWait.add(_init); } - // ignore: prefer_constructors_over_static_methods - static ChatComponentCubit singleContact( - {required Locator locator, - required ActiveConversationCubit activeConversationCubit, - required SingleContactMessagesCubit messagesCubit}) { - // Get account info - final unlockedAccountInfo = - locator().state.unlockedAccountInfo!; - final account = locator().state.asData!.value; - - // Make local 'User' - final localUserIdentityKey = unlockedAccountInfo.identityTypedPublicKey; - final localUser = types.User( - id: localUserIdentityKey.toString(), - firstName: account.profile.name, - metadata: {metadataKeyIdentityPublicKey: localUserIdentityKey}); - - // Make remote 'User's - final remoteUsers = { - activeConversationState.contact.identityPublicKey.toVeilid(): types.User( - id: activeConversationState.contact.identityPublicKey - .toVeilid() - .toString(), - firstName: activeConversationState.contact.displayName, - metadata: { - metadataKeyIdentityPublicKey: - activeConversationState.contact.identityPublicKey.toVeilid() - }) - }.toIMap(); - - return ChatComponentCubit._( - messagesCubit: messagesCubit, - localUser: localUser, - remoteUsers: remoteUsers, - ); - } + factory ChatComponentCubit.singleContact( + {required Locator locator, + required ActiveConversationCubit activeConversationCubit, + required SingleContactMessagesCubit messagesCubit}) => + ChatComponentCubit._( + locator: locator, + conversationCubits: [activeConversationCubit], + messagesCubit: messagesCubit, + ); Future _init() async { - _messagesSubscription = _messagesCubit.stream.listen((messagesState) { - emit(state.copyWith( - messageWindow: _convertMessages(messagesState), - )); - }); - emit(state.copyWith( - messageWindow: _convertMessages(_messagesCubit.state), - title: _getTitle(), - )); + // Get local user info and account record cubit + final unlockedAccountInfo = + _locator().state.unlockedAccountInfo!; + _localUserIdentityKey = unlockedAccountInfo.identityTypedPublicKey; + _localUserAccountRecordCubit = _locator(); + + // Subscribe to local user info + _localUserAccountRecordSubscription = _localUserAccountRecordCubit.stream + .listen(_onChangedLocalUserAccountRecord); + _onChangedLocalUserAccountRecord(_localUserAccountRecordCubit.state); + + // Subscribe to remote user info + await _updateConversationSubscriptions(); + + // Subscribe to messages + _messagesSubscription = _messagesCubit.stream.listen(_onChangedMessages); + _onChangedMessages(_messagesCubit.state); } @override Future close() async { await _initWait(); + await _localUserAccountRecordSubscription.cancel(); await _messagesSubscription.cancel(); await super.close(); } @@ -159,23 +145,130 @@ class ChatComponentCubit extends Cubit { //////////////////////////////////////////////////////////////////////////// // Private Implementation - String _getTitle() { - if (state.remoteUsers.length == 1) { - final remoteUser = state.remoteUsers.values.first; - return remoteUser.firstName ?? ''; - } else { - return ''; + void _onChangedLocalUserAccountRecord(AsyncValue avAccount) { + final account = avAccount.asData?.value; + if (account == null) { + emit(state.copyWith(localUser: null)); + return; } + // Make local 'User' + final localUser = types.User( + id: _localUserIdentityKey.toString(), + firstName: account.profile.name, + metadata: {metadataKeyIdentityPublicKey: _localUserIdentityKey}); + emit(state.copyWith(localUser: localUser)); } - types.Message? _messageStateToChatMessage(MessageState message) { + void _onChangedMessages( + AsyncValue> avMessagesState) { + emit(_convertMessages(state, avMessagesState)); + } + + void _onChangedConversation( + TypedKey remoteIdentityPublicKey, + AsyncValue avConversationState, + ) { + // + } + + types.User _convertRemoteUser(TypedKey remoteIdentityPublicKey, + ActiveConversationState activeConversationState) => + types.User( + id: remoteIdentityPublicKey.toString(), + firstName: activeConversationState.contact.displayName, + metadata: {metadataKeyIdentityPublicKey: remoteIdentityPublicKey}); + + types.User _convertUnknownUser(TypedKey remoteIdentityPublicKey) => + types.User( + id: remoteIdentityPublicKey.toString(), + firstName: '<$remoteIdentityPublicKey>', + metadata: {metadataKeyIdentityPublicKey: remoteIdentityPublicKey}); + + Future _updateConversationSubscriptions() async { + // Get existing subscription keys and state + final existing = _conversationSubscriptions.keys.toList(); + var currentRemoteUsersState = state.remoteUsers; + + // Process cubit list + for (final cc in _conversationCubits) { + // Get the remote identity key + final remoteIdentityPublicKey = cc.input.remoteIdentityPublicKey; + + // If the cubit is already being listened to we have nothing to do + if (existing.remove(remoteIdentityPublicKey)) { + continue; + } + + // If the cubit is not already being listened to we should do that + _conversationSubscriptions[remoteIdentityPublicKey] = cc.stream.listen( + (avConv) => _onChangedConversation(remoteIdentityPublicKey, avConv)); + final activeConversationState = cc.state.asData?.value; + if (activeConversationState != null) { + final remoteUser = _convertRemoteUser( + remoteIdentityPublicKey, activeConversationState); + currentRemoteUsersState = + currentRemoteUsersState.add(remoteIdentityPublicKey, remoteUser); + } + } + // Purge remote users we didn't see in the cubit list any more + final cancels = >[]; + for (final deadUser in existing) { + currentRemoteUsersState = currentRemoteUsersState.remove(deadUser); + cancels.add(_conversationSubscriptions.remove(deadUser)!.cancel()); + } + await cancels.wait; + + // Emit change to remote users state + emit(_updateTitle(state.copyWith(remoteUsers: currentRemoteUsersState))); + } + + ChatComponentState _updateTitle(ChatComponentState currentState) { + if (currentState.remoteUsers.length == 0) { + return currentState.copyWith(title: 'Empty Chat'); + } + if (currentState.remoteUsers.length == 1) { + final remoteUser = currentState.remoteUsers.values.first; + return currentState.copyWith(title: remoteUser.firstName ?? ''); + } + return currentState.copyWith( + title: ''); + } + + (ChatComponentState, types.Message?) _messageStateToChatMessage( + ChatComponentState currentState, MessageState message) { final authorIdentityPublicKey = message.content.author.toVeilid(); - final author = - state.remoteUsers[authorIdentityPublicKey] ?? state.localUser; + late final types.User author; + if (authorIdentityPublicKey == _localUserIdentityKey && + currentState.localUser != null) { + author = currentState.localUser!; + } else { + final remoteUser = currentState.remoteUsers[authorIdentityPublicKey]; + if (remoteUser != null) { + author = remoteUser; + } else { + final historicalRemoteUser = + currentState.historicalRemoteUsers[authorIdentityPublicKey]; + if (historicalRemoteUser != null) { + author = historicalRemoteUser; + } else { + final unknownRemoteUser = + currentState.unknownUsers[authorIdentityPublicKey]; + if (unknownRemoteUser != null) { + author = unknownRemoteUser; + } else { + final unknownUser = _convertUnknownUser(authorIdentityPublicKey); + currentState = currentState.copyWith( + unknownUsers: currentState.unknownUsers + .add(authorIdentityPublicKey, unknownUser)); + author = unknownUser; + } + } + } + } types.Status? status; if (message.sendState != null) { - assert(author == state.localUser, + assert(author.id == _localUserIdentityKey.toString(), 'send state should only be on sent messages'); switch (message.sendState!) { case MessageSendState.sending: @@ -198,7 +291,7 @@ class ChatComponentCubit extends Cubit { text: contextText.text, showStatus: status != null, status: status); - return textMessage; + return (currentState, textMessage); case proto.Message_Kind.secret: case proto.Message_Kind.delete: case proto.Message_Kind.erase: @@ -207,17 +300,24 @@ class ChatComponentCubit extends Cubit { case proto.Message_Kind.membership: case proto.Message_Kind.moderation: case proto.Message_Kind.notSet: - return null; + return (currentState, null); } } - AsyncValue> _convertMessages( + ChatComponentState _convertMessages(ChatComponentState currentState, AsyncValue> avMessagesState) { + // Clear out unknown users + currentState = state.copyWith(unknownUsers: const IMap.empty()); + final asError = avMessagesState.asError; if (asError != null) { - return AsyncValue.error(asError.error, asError.stackTrace); + return currentState.copyWith( + unknownUsers: const IMap.empty(), + messageWindow: AsyncValue.error(asError.error, asError.stackTrace)); } else if (avMessagesState.asLoading != null) { - return const AsyncValue.loading(); + return currentState.copyWith( + unknownUsers: const IMap.empty(), + messageWindow: const AsyncValue.loading()); } final messagesState = avMessagesState.asData!.value; @@ -225,7 +325,9 @@ class ChatComponentCubit extends Cubit { final chatMessages = []; final tsSet = {}; for (final message in messagesState.window) { - final chatMessage = _messageStateToChatMessage(message); + final (newState, chatMessage) = + _messageStateToChatMessage(currentState, message); + currentState = newState; if (chatMessage == null) { continue; } @@ -238,12 +340,13 @@ class ChatComponentCubit extends Cubit { assert(false, 'should not have duplicate id'); } } - return AsyncValue.data(WindowState( - window: chatMessages.toIList(), - length: messagesState.length, - windowTail: messagesState.windowTail, - windowCount: messagesState.windowCount, - follow: messagesState.follow)); + return currentState.copyWith( + messageWindow: AsyncValue.data(WindowState( + window: chatMessages.toIList(), + length: messagesState.length, + windowTail: messagesState.windowTail, + windowCount: messagesState.windowCount, + follow: messagesState.follow))); } void _addTextMessage( @@ -271,7 +374,18 @@ class ChatComponentCubit extends Cubit { //////////////////////////////////////////////////////////////////////////// final _initWait = WaitSet(); + + final Locator _locator; + final List _conversationCubits; final SingleContactMessagesCubit _messagesCubit; + + late final TypedKey _localUserIdentityKey; + late final AccountRecordCubit _localUserAccountRecordCubit; + late final StreamSubscription> + _localUserAccountRecordSubscription; + final Map>> + _conversationSubscriptions = {}; late StreamSubscription _messagesSubscription; + double scrollOffset = 0; } diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 7ef5a45..a2fcc8d 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -88,7 +88,7 @@ class SingleContactMessagesCubit extends Cubit { // Initialize everything Future _init() async { _unlockedAccountInfo = - _locator().state.unlockedAccountInfo!; + _locator().state.unlockedAccountInfo!; _unsentMessagesQueue = PersistentQueue( table: 'SingleContactUnsentMessages', diff --git a/lib/chat/models/chat_component_state.dart b/lib/chat/models/chat_component_state.dart index b8da8d4..ae69da7 100644 --- a/lib/chat/models/chat_component_state.dart +++ b/lib/chat/models/chat_component_state.dart @@ -20,9 +20,13 @@ class ChatComponentState with _$ChatComponentState { // ScrollController for the chat required AutoScrollController scrollController, // Local user - required User localUser, - // Remote users + required User? localUser, + // Active remote users required IMap remoteUsers, + // Historical remote users + required IMap historicalRemoteUsers, + // Unknown users + required IMap unknownUsers, // Messages state required AsyncValue> messageWindow, // Title of the chat diff --git a/lib/chat/models/chat_component_state.freezed.dart b/lib/chat/models/chat_component_state.freezed.dart index 859f363..ea7e8ae 100644 --- a/lib/chat/models/chat_component_state.freezed.dart +++ b/lib/chat/models/chat_component_state.freezed.dart @@ -21,8 +21,13 @@ mixin _$ChatComponentState { throw _privateConstructorUsedError; // ScrollController for the chat AutoScrollController get scrollController => throw _privateConstructorUsedError; // Local user - User get localUser => throw _privateConstructorUsedError; // Remote users + User? get localUser => + throw _privateConstructorUsedError; // Active remote users IMap, User> get remoteUsers => + throw _privateConstructorUsedError; // Historical remote users + IMap, User> get historicalRemoteUsers => + throw _privateConstructorUsedError; // Unknown users + IMap, User> get unknownUsers => throw _privateConstructorUsedError; // Messages state AsyncValue> get messageWindow => throw _privateConstructorUsedError; // Title of the chat @@ -42,8 +47,10 @@ abstract class $ChatComponentStateCopyWith<$Res> { $Res call( {GlobalKey chatKey, AutoScrollController scrollController, - User localUser, + User? localUser, IMap, User> remoteUsers, + IMap, User> historicalRemoteUsers, + IMap, User> unknownUsers, AsyncValue> messageWindow, String title}); @@ -65,8 +72,10 @@ class _$ChatComponentStateCopyWithImpl<$Res, $Val extends ChatComponentState> $Res call({ Object? chatKey = null, Object? scrollController = null, - Object? localUser = null, + Object? localUser = freezed, Object? remoteUsers = null, + Object? historicalRemoteUsers = null, + Object? unknownUsers = null, Object? messageWindow = null, Object? title = null, }) { @@ -79,14 +88,22 @@ class _$ChatComponentStateCopyWithImpl<$Res, $Val extends ChatComponentState> ? _value.scrollController : scrollController // ignore: cast_nullable_to_non_nullable as AutoScrollController, - localUser: null == localUser + localUser: freezed == localUser ? _value.localUser : localUser // ignore: cast_nullable_to_non_nullable - as User, + as User?, remoteUsers: null == remoteUsers ? _value.remoteUsers : remoteUsers // ignore: cast_nullable_to_non_nullable as IMap, User>, + historicalRemoteUsers: null == historicalRemoteUsers + ? _value.historicalRemoteUsers + : historicalRemoteUsers // ignore: cast_nullable_to_non_nullable + as IMap, User>, + unknownUsers: null == unknownUsers + ? _value.unknownUsers + : unknownUsers // ignore: cast_nullable_to_non_nullable + as IMap, User>, messageWindow: null == messageWindow ? _value.messageWindow : messageWindow // ignore: cast_nullable_to_non_nullable @@ -119,8 +136,10 @@ abstract class _$$ChatComponentStateImplCopyWith<$Res> $Res call( {GlobalKey chatKey, AutoScrollController scrollController, - User localUser, + User? localUser, IMap, User> remoteUsers, + IMap, User> historicalRemoteUsers, + IMap, User> unknownUsers, AsyncValue> messageWindow, String title}); @@ -141,8 +160,10 @@ class __$$ChatComponentStateImplCopyWithImpl<$Res> $Res call({ Object? chatKey = null, Object? scrollController = null, - Object? localUser = null, + Object? localUser = freezed, Object? remoteUsers = null, + Object? historicalRemoteUsers = null, + Object? unknownUsers = null, Object? messageWindow = null, Object? title = null, }) { @@ -155,14 +176,22 @@ class __$$ChatComponentStateImplCopyWithImpl<$Res> ? _value.scrollController : scrollController // ignore: cast_nullable_to_non_nullable as AutoScrollController, - localUser: null == localUser + localUser: freezed == localUser ? _value.localUser : localUser // ignore: cast_nullable_to_non_nullable - as User, + as User?, remoteUsers: null == remoteUsers ? _value.remoteUsers : remoteUsers // ignore: cast_nullable_to_non_nullable as IMap, User>, + historicalRemoteUsers: null == historicalRemoteUsers + ? _value.historicalRemoteUsers + : historicalRemoteUsers // ignore: cast_nullable_to_non_nullable + as IMap, User>, + unknownUsers: null == unknownUsers + ? _value.unknownUsers + : unknownUsers // ignore: cast_nullable_to_non_nullable + as IMap, User>, messageWindow: null == messageWindow ? _value.messageWindow : messageWindow // ignore: cast_nullable_to_non_nullable @@ -183,6 +212,8 @@ class _$ChatComponentStateImpl implements _ChatComponentState { required this.scrollController, required this.localUser, required this.remoteUsers, + required this.historicalRemoteUsers, + required this.unknownUsers, required this.messageWindow, required this.title}); @@ -194,10 +225,16 @@ class _$ChatComponentStateImpl implements _ChatComponentState { final AutoScrollController scrollController; // Local user @override - final User localUser; -// Remote users + final User? localUser; +// Active remote users @override final IMap, User> remoteUsers; +// Historical remote users + @override + final IMap, User> historicalRemoteUsers; +// Unknown users + @override + final IMap, User> unknownUsers; // Messages state @override final AsyncValue> messageWindow; @@ -207,7 +244,7 @@ class _$ChatComponentStateImpl implements _ChatComponentState { @override String toString() { - return 'ChatComponentState(chatKey: $chatKey, scrollController: $scrollController, localUser: $localUser, remoteUsers: $remoteUsers, messageWindow: $messageWindow, title: $title)'; + return 'ChatComponentState(chatKey: $chatKey, scrollController: $scrollController, localUser: $localUser, remoteUsers: $remoteUsers, historicalRemoteUsers: $historicalRemoteUsers, unknownUsers: $unknownUsers, messageWindow: $messageWindow, title: $title)'; } @override @@ -222,14 +259,26 @@ class _$ChatComponentStateImpl implements _ChatComponentState { other.localUser == localUser) && (identical(other.remoteUsers, remoteUsers) || other.remoteUsers == remoteUsers) && + (identical(other.historicalRemoteUsers, historicalRemoteUsers) || + other.historicalRemoteUsers == historicalRemoteUsers) && + (identical(other.unknownUsers, unknownUsers) || + other.unknownUsers == unknownUsers) && (identical(other.messageWindow, messageWindow) || other.messageWindow == messageWindow) && (identical(other.title, title) || other.title == title)); } @override - int get hashCode => Object.hash(runtimeType, chatKey, scrollController, - localUser, remoteUsers, messageWindow, title); + int get hashCode => Object.hash( + runtimeType, + chatKey, + scrollController, + localUser, + remoteUsers, + historicalRemoteUsers, + unknownUsers, + messageWindow, + title); @JsonKey(ignore: true) @override @@ -243,8 +292,11 @@ abstract class _ChatComponentState implements ChatComponentState { const factory _ChatComponentState( {required final GlobalKey chatKey, required final AutoScrollController scrollController, - required final User localUser, + required final User? localUser, required final IMap, User> remoteUsers, + required final IMap, User> + historicalRemoteUsers, + required final IMap, User> unknownUsers, required final AsyncValue> messageWindow, required final String title}) = _$ChatComponentStateImpl; @@ -253,9 +305,13 @@ abstract class _ChatComponentState implements ChatComponentState { @override // ScrollController for the chat AutoScrollController get scrollController; @override // Local user - User get localUser; - @override // Remote users + User? get localUser; + @override // Active remote users IMap, User> get remoteUsers; + @override // Historical remote users + IMap, User> get historicalRemoteUsers; + @override // Unknown users + IMap, User> get unknownUsers; @override // Messages state AsyncValue> get messageWindow; @override // Title of the chat diff --git a/lib/chat/views/chat_component_widget.dart b/lib/chat/views/chat_component_widget.dart index f0f259d..92ec587 100644 --- a/lib/chat/views/chat_component_widget.dart +++ b/lib/chat/views/chat_component_widget.dart @@ -8,7 +8,6 @@ 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 '../../conversation/conversation.dart'; import '../../theme/theme.dart'; import '../chat.dart'; @@ -147,6 +146,11 @@ class ChatComponentWidget extends StatelessWidget { final chatComponentCubit = context.watch(); final chatComponentState = chatComponentCubit.state; + final localUser = chatComponentState.localUser; + if (localUser == null) { + return waitingPage(); + } + final messageWindow = chatComponentState.messageWindow.asData?.value; if (messageWindow == null) { return chatComponentState.messageWindow.buildNotData(); @@ -269,7 +273,7 @@ class ChatComponentWidget extends StatelessWidget { _handleSendPressed(chatComponentCubit, pt), //showUserAvatars: false, //showUserNames: true, - user: chatComponentState.localUser, + user: localUser, emptyState: const EmptyChatWidget())), ), ), diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index e5e4740..2cd0a49 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -69,7 +69,7 @@ class ContactInvitationListCubit final contactRequestWriter = await crcs.generateKeyPair(); final activeAccountInfo = - _locator().state.unlockedAccountInfo!; + _locator().state.unlockedAccountInfo!; final profile = _locator().state.asData!.value.profile; final idcs = await activeAccountInfo.identityCryptoSystem; @@ -247,7 +247,8 @@ class ContactInvitationListCubit await (await pool.openRecordRead(contactRequestInboxKey, debugName: 'ContactInvitationListCubit::validateInvitation::' 'ContactRequestInbox', - parent: _accountRecordKey)) + parent: pool.getParentRecordKey(contactRequestInboxKey) ?? + _accountRecordKey)) .maybeDeleteScope(!isSelf, (contactRequestInbox) async { // final contactRequest = await contactRequestInbox diff --git a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart index 50710e8..64f9d45 100644 --- a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart +++ b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart @@ -23,7 +23,7 @@ class ContactRequestInboxCubit final pool = DHTRecordPool.instance; final unlockedAccountInfo = - locator().state.unlockedAccountInfo!; + locator().state.unlockedAccountInfo!; final accountRecordKey = unlockedAccountInfo.accountRecordKey; final writerSecret = contactInvitationRecord.writerSecret.toVeilid(); diff --git a/lib/contact_invitation/models/valid_contact_invitation.dart b/lib/contact_invitation/models/valid_contact_invitation.dart index f64312f..2f561fe 100644 --- a/lib/contact_invitation/models/valid_contact_invitation.dart +++ b/lib/contact_invitation/models/valid_contact_invitation.dart @@ -31,7 +31,7 @@ class ValidContactInvitation { final pool = DHTRecordPool.instance; try { final unlockedAccountInfo = - _locator().state.unlockedAccountInfo!; + _locator().state.unlockedAccountInfo!; final accountRecordKey = unlockedAccountInfo.accountRecordKey; final identityPublicKey = unlockedAccountInfo.identityPublicKey; @@ -43,7 +43,8 @@ class ValidContactInvitation { return (await pool.openRecordWrite(_contactRequestInboxKey, _writer, debugName: 'ValidContactInvitation::accept::' 'ContactRequestInbox', - parent: accountRecordKey)) + parent: pool.getParentRecordKey(_contactRequestInboxKey) ?? + accountRecordKey)) // ignore: prefer_expression_function_bodies .maybeDeleteScope(!isSelf, (contactRequestInbox) async { // Create local conversation key for this @@ -96,7 +97,7 @@ class ValidContactInvitation { final pool = DHTRecordPool.instance; final unlockedAccountInfo = - _locator().state.unlockedAccountInfo!; + _locator().state.unlockedAccountInfo!; final accountRecordKey = unlockedAccountInfo.accountRecordKey; final identityPublicKey = unlockedAccountInfo.identityPublicKey; diff --git a/lib/contact_invitation/views/invitation_dialog.dart b/lib/contact_invitation/views/invitation_dialog.dart index f868277..e619360 100644 --- a/lib/contact_invitation/views/invitation_dialog.dart +++ b/lib/contact_invitation/views/invitation_dialog.dart @@ -74,7 +74,7 @@ class InvitationDialogState extends State { Future _onAccept() async { final navigator = Navigator.of(context); - final activeAccountInfo = widget._locator(); + final accountInfo = widget._locator().state; final contactList = widget._locator(); setState(() { @@ -86,7 +86,7 @@ class InvitationDialogState extends State { if (acceptedContact != null) { // initiator when accept is received will create // contact in the case of a 'note to self' - final isSelf = activeAccountInfo.identityPublicKey == + final isSelf = accountInfo.unlockedAccountInfo!.identityPublicKey == acceptedContact.remoteIdentity.currentInstance.publicKey; if (!isSelf) { await contactList.createContact( diff --git a/lib/contact_invitation/views/paste_invitation_dialog.dart b/lib/contact_invitation/views/paste_invitation_dialog.dart index ead492b..377d13f 100644 --- a/lib/contact_invitation/views/paste_invitation_dialog.dart +++ b/lib/contact_invitation/views/paste_invitation_dialog.dart @@ -4,6 +4,7 @@ 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:provider/provider.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../theme/theme.dart'; @@ -11,29 +12,23 @@ import '../../tools/tools.dart'; import 'invitation_dialog.dart'; class PasteInvitationDialog extends StatefulWidget { - const PasteInvitationDialog({required this.modalContext, super.key}); + const PasteInvitationDialog({required Locator locator, super.key}) + : _locator = locator; @override PasteInvitationDialogState createState() => PasteInvitationDialogState(); static Future show(BuildContext context) async { - final modalContext = context; + final locator = context.read; await showPopControlDialog( context: context, builder: (context) => StyledDialog( title: translate('paste_invitation_dialog.title'), - child: PasteInvitationDialog(modalContext: modalContext))); + child: PasteInvitationDialog(locator: locator))); } - final BuildContext modalContext; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - .add(DiagnosticsProperty('modalContext', modalContext)); - } + final Locator _locator; } class PasteInvitationDialogState extends State { @@ -138,7 +133,7 @@ class PasteInvitationDialogState extends State { // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return InvitationDialog( - modalContext: widget.modalContext, + locator: widget._locator, onValidationCancelled: onValidationCancelled, onValidationSuccess: onValidationSuccess, onValidationFailed: onValidationFailed, diff --git a/lib/contact_invitation/views/scan_invitation_dialog.dart b/lib/contact_invitation/views/scan_invitation_dialog.dart index ab47df0..d3881c2 100644 --- a/lib/contact_invitation/views/scan_invitation_dialog.dart +++ b/lib/contact_invitation/views/scan_invitation_dialog.dart @@ -9,6 +9,7 @@ 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:provider/provider.dart'; import 'package:zxing2/qrcode.dart'; import '../../theme/theme.dart'; @@ -102,28 +103,22 @@ class ScannerOverlay extends CustomPainter { } class ScanInvitationDialog extends StatefulWidget { - const ScanInvitationDialog({required this.modalContext, super.key}); + const ScanInvitationDialog({required Locator locator, super.key}) + : _locator = locator; @override ScanInvitationDialogState createState() => ScanInvitationDialogState(); static Future show(BuildContext context) async { - final modalContext = context; + final locator = context.read; await showPopControlDialog( context: context, builder: (context) => StyledDialog( title: translate('scan_invitation_dialog.title'), - child: ScanInvitationDialog(modalContext: modalContext))); + child: ScanInvitationDialog(locator: locator))); } - final BuildContext modalContext; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - .add(DiagnosticsProperty('modalContext', modalContext)); - } + final Locator _locator; } class ScanInvitationDialogState extends State { @@ -396,7 +391,7 @@ class ScanInvitationDialogState extends State { // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return InvitationDialog( - modalContext: widget.modalContext, + locator: widget._locator, onValidationCancelled: onValidationCancelled, onValidationSuccess: onValidationSuccess, onValidationFailed: onValidationFailed, diff --git a/lib/conversation/cubits/conversation_cubit.dart b/lib/conversation/cubits/conversation_cubit.dart index fb0718c..4143d24 100644 --- a/lib/conversation/cubits/conversation_cubit.dart +++ b/lib/conversation/cubits/conversation_cubit.dart @@ -46,7 +46,7 @@ class ConversationCubit extends Cubit> { _remoteConversationRecordKey = remoteConversationRecordKey, super(const AsyncValue.loading()) { final unlockedAccountInfo = - _locator().state.unlockedAccountInfo!; + _locator().state.unlockedAccountInfo!; _accountRecordKey = unlockedAccountInfo.accountRecordKey; _identityWriter = unlockedAccountInfo.identityWriter; @@ -76,7 +76,8 @@ class ConversationCubit extends Cubit> { final crypto = await _cachedConversationCrypto(); final record = await pool.openRecordRead(_remoteConversationRecordKey, debugName: 'ConversationCubit::RemoteConversation', - parent: _accountRecordKey, + parent: pool.getParentRecordKey(_remoteConversationRecordKey) ?? + _accountRecordKey, crypto: crypto); return record; }); @@ -117,7 +118,7 @@ class ConversationCubit extends Cubit> { final crypto = await _cachedConversationCrypto(); final account = _locator().state.asData!.value; final unlockedAccountInfo = - _locator().state.unlockedAccountInfo!; + _locator().state.unlockedAccountInfo!; final accountRecordKey = unlockedAccountInfo.accountRecordKey; final writer = unlockedAccountInfo.identityWriter; @@ -359,7 +360,7 @@ class ConversationCubit extends Cubit> { return conversationCrypto; } final unlockedAccountInfo = - _locator().state.unlockedAccountInfo!; + _locator().state.unlockedAccountInfo!; conversationCrypto = await unlockedAccountInfo .makeConversationCrypto(_remoteIdentityPublicKey); _conversationCrypto = conversationCrypto; @@ -368,6 +369,8 @@ class ConversationCubit extends Cubit> { //////////////////////////////////////////////////////////////////////////// // Fields + TypedKey get remoteIdentityPublicKey => _remoteIdentityPublicKey; + final Locator _locator; late final TypedKey _accountRecordKey; late final KeyPair _identityWriter; diff --git a/lib/layout/home/active_account_page_controller_wrapper.dart b/lib/layout/home/active_account_page_controller_wrapper.dart new file mode 100644 index 0000000..79314a1 --- /dev/null +++ b/lib/layout/home/active_account_page_controller_wrapper.dart @@ -0,0 +1,37 @@ +import 'dart:async'; + +import 'package:async_tools/async_tools.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../account_manager/account_manager.dart'; + +class ActiveAccountPageControllerWrapper { + ActiveAccountPageControllerWrapper(Locator locator, int initialPage) { + pageController = PageController(initialPage: initialPage, keepPage: false); + + final activeLocalAccountCubit = locator(); + _subscription = + activeLocalAccountCubit.stream.listen((activeLocalAccountRecordKey) { + singleFuture(this, () async { + final localAccounts = locator().state; + final activeIndex = localAccounts.indexWhere( + (x) => x.superIdentity.recordKey == activeLocalAccountRecordKey); + if (pageController.page == activeIndex) { + return; + } + await pageController.animateToPage(activeIndex, + duration: const Duration(milliseconds: 250), + curve: Curves.fastOutSlowIn); + }); + }); + } + + void dispose() { + unawaited(_subscription.cancel()); + } + + late PageController pageController; + late StreamSubscription _subscription; +} diff --git a/lib/layout/home/drawer_menu/drawer_menu.dart b/lib/layout/home/drawer_menu/drawer_menu.dart index 7b51346..ea5b722 100644 --- a/lib/layout/home/drawer_menu/drawer_menu.dart +++ b/lib/layout/home/drawer_menu/drawer_menu.dart @@ -1,5 +1,6 @@ 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_svg/flutter_svg.dart'; @@ -102,15 +103,12 @@ class _DrawerMenuState extends State { } Widget _getAccountList( - {required TypedKey? activeLocalAccount, + {required IList localAccounts, + required TypedKey? activeLocalAccount, required AccountRecordsBlocMapState accountRecords}) { final theme = Theme.of(context); final scaleScheme = theme.extension()!; - final accountRepo = AccountRepository.instance; - final localAccounts = accountRepo.getLocalAccounts(); - //final userLogins = accountRepo.getUserLogins(); - final loggedInAccounts = []; final loggedOutAccounts = []; @@ -234,8 +232,9 @@ class _DrawerMenuState extends State { final theme = Theme.of(context); final scale = theme.extension()!; //final textTheme = theme.textTheme; + final localAccounts = context.watch().state; final accountRecords = context.watch().state; - final activeLocalAccount = context.watch().state; + final activeLocalAccount = context.watch().state; final gradient = LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, @@ -276,8 +275,8 @@ class _DrawerMenuState extends State { ])), const Spacer(), _getAccountList( - activeLocalAccount: - activeLocalAccount.unlockedAccountInfo?.superIdentityRecordKey, + localAccounts: localAccounts, + activeLocalAccount: activeLocalAccount, accountRecords: accountRecords), _getBottomButtons(), const Spacer(), diff --git a/lib/layout/home/home.dart b/lib/layout/home/home.dart index 68e4042..cb0cef7 100644 --- a/lib/layout/home/home.dart +++ b/lib/layout/home/home.dart @@ -1,7 +1,8 @@ +export 'active_account_page_controller_wrapper.dart'; export 'drawer_menu/drawer_menu.dart'; 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'; +export 'home_screen.dart'; 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 cdd6ac5..b51d72f 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 @@ -5,6 +5,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:preload_page_view/preload_page_view.dart'; +import 'package:provider/provider.dart'; import 'package:stylish_bottom_bar/stylish_bottom_bar.dart'; import '../../../../chat/chat.dart'; @@ -117,7 +118,7 @@ class MainPagerState extends State with TickerProviderStateMixin { style: TextStyle(fontSize: 24), ), content: ScanInvitationDialog( - modalContext: context, + locator: context.read, )); }); } diff --git a/lib/layout/home/home_shell.dart b/lib/layout/home/home_screen.dart similarity index 71% rename from lib/layout/home/home_shell.dart rename to lib/layout/home/home_screen.dart index a3c18b6..53fef55 100644 --- a/lib/layout/home/home_shell.dart +++ b/lib/layout/home/home_screen.dart @@ -14,24 +14,24 @@ import '../../contact_invitation/contact_invitation.dart'; import '../../contacts/contacts.dart'; import '../../conversation/conversation.dart'; import '../../proto/proto.dart' as proto; -import '../../router/router.dart'; import '../../theme/theme.dart'; +import '../../tools/tools.dart'; +import 'active_account_page_controller_wrapper.dart'; import 'drawer_menu/drawer_menu.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 HomeShell extends StatefulWidget { - const HomeShell({required this.child, super.key}); +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); @override - HomeShellState createState() => HomeShellState(); - - final Widget child; + HomeScreenState createState() => HomeScreenState(); } -class HomeShellState extends State { +class HomeScreenState extends State { @override void initState() { super.initState(); @@ -84,8 +84,22 @@ class HomeShellState extends State { }); } - Widget _buildActiveAccount(BuildContext context) { - final accountRecordKey = context.select( + Widget _buildAccountReadyDeviceSpecific(BuildContext context) { + final hasActiveChat = context.watch().state != null; + if (responsiveVisibility( + context: context, + tablet: false, + tabletLandscape: false, + desktop: false)) { + if (hasActiveChat) { + return const HomeAccountReadyChat(); + } + } + return const HomeAccountReadyMain(); + } + + Widget _buildUnlockedAccount(BuildContext context) { + final accountRecordKey = context.select( (c) => c.state.unlockedAccountInfo!.accountRecordKey); final contactListRecordPointer = context.select( @@ -124,8 +138,9 @@ class HomeShellState extends State { )), // Chat Cubits BlocProvider( - create: (context) => ActiveChatCubit(null, - routerCubit: context.read())), + create: (context) => ActiveChatCubit( + null, + )), BlocProvider( create: (context) => ChatListCubit( locator: context.read, @@ -146,27 +161,21 @@ class HomeShellState extends State { WaitingInvitationsBlocMapState>( listener: _invitationStatusListener, ) - ], child: widget.child)); + ], child: Builder(builder: _buildAccountReadyDeviceSpecific))); } - Widget _buildWithLogin(BuildContext context) { + Widget _buildAccount(BuildContext context) { // Get active account info status final ( accountInfoStatus, accountInfoActive, superIdentityRecordKey ) = context - .select( - (c) => ( - c.state.status, - c.state.active, - c.state.unlockedAccountInfo?.superIdentityRecordKey - )); - - if (!accountInfoActive) { - // If no logged in user is active, show the loading panel - return const HomeNoActive(); - } + .select((c) => ( + c.state.status, + c.state.active, + c.state.unlockedAccountInfo?.superIdentityRecordKey + )); switch (accountInfoStatus) { case AccountInfoStatus.noAccount: @@ -175,7 +184,7 @@ class HomeShellState extends State { return const HomeAccountInvalid(); case AccountInfoStatus.accountLocked: return const HomeAccountLocked(); - case AccountInfoStatus.accountReady: + case AccountInfoStatus.accountUnlocked: // Get the current active account record cubit final activeAccountRecordCubit = @@ -190,10 +199,50 @@ class HomeShellState extends State { return MultiBlocProvider(providers: [ BlocProvider.value( value: activeAccountRecordCubit), - ], child: Builder(builder: _buildActiveAccount)); + ], child: Builder(builder: _buildUnlockedAccount)); } } + Widget _buildAccountPageView(BuildContext context) { + final localAccounts = context.watch().state; + final activeLocalAccountCubit = context.read(); + + final activeIndex = localAccounts.indexWhere( + (x) => x.superIdentity.recordKey == activeLocalAccountCubit.state); + if (activeIndex == -1) { + return const HomeNoActive(); + } + + return Provider( + lazy: false, + create: (context) => + ActiveAccountPageControllerWrapper(context.read, activeIndex), + dispose: (context, value) { + value.dispose(); + }, + child: Builder( + builder: (context) => PageView.builder( + itemCount: localAccounts.length, + onPageChanged: (idx) { + singleFuture(this, () async { + await AccountRepository.instance.switchToAccount( + localAccounts[idx].superIdentity.recordKey); + }); + }, + controller: context + .read() + .pageController, + itemBuilder: (context, index) { + final localAccount = localAccounts[index]; + return BlocProvider( + key: ValueKey(localAccount.superIdentity.recordKey), + create: (context) => AccountInfoCubit( + AccountRepository.instance, + localAccount.superIdentity.recordKey), + child: Builder(builder: _buildAccount)); + }))); + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -219,7 +268,7 @@ class HomeShellState extends State { color: scale.primaryScale.activeElementBackground), child: Provider.value( value: _zoomDrawerController, - child: Builder(builder: _buildWithLogin))), + child: Builder(builder: _buildAccountPageView))), borderRadius: 24, showShadow: true, angle: 0, @@ -239,50 +288,3 @@ class HomeShellState extends State { final _singleInvitationStatusProcessor = SingleStateProcessor(); } - -// class HomeAccountReadyShell extends StatefulWidget { -// 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 unlockedAccountInfo = context.watch(); -// final routerCubit = context.read(); - -// return HomeAccountReadyShell._( -// unlockedAccountInfo: unlockedAccountInfo, -// routerCubit: routerCubit, -// key: key, -// child: child); -// } -// const HomeAccountReadyShell._( -// {required this.unlockedAccountInfo, -// required this.routerCubit, -// required this.child, -// super.key}); - -// @override -// HomeAccountReadyShellState createState() => HomeAccountReadyShellState(); - -// final Widget child; -// final UnlockedAccountInfo unlockedAccountInfo; -// final RouterCubit routerCubit; - -// @override -// void debugFillProperties(DiagnosticPropertiesBuilder properties) { -// super.debugFillProperties(properties); -// properties -// ..add(DiagnosticsProperty( -// 'unlockedAccountInfo', unlockedAccountInfo)) -// ..add(DiagnosticsProperty('routerCubit', routerCubit)); -// } -// } - -// class HomeAccountReadyShellState extends State { -// final SingleStateProcessor -// _singleInvitationStatusProcessor = SingleStateProcessor(); - -// @override -// void initState() { -// super.initState(); -// } -// } diff --git a/lib/router/cubit/router_cubit.dart b/lib/router/cubit/router_cubit.dart index 9c5fc84..4b4061e 100644 --- a/lib/router/cubit/router_cubit.dart +++ b/lib/router/cubit/router_cubit.dart @@ -20,13 +20,12 @@ part 'router_cubit.freezed.dart'; part 'router_cubit.g.dart'; final _rootNavKey = GlobalKey(debugLabel: 'rootNavKey'); -final _homeNavKey = GlobalKey(debugLabel: 'homeNavKey'); @freezed class RouterState with _$RouterState { - const factory RouterState( - {required bool hasAnyAccount, - required bool hasActiveChat}) = _RouterState; + const factory RouterState({ + required bool hasAnyAccount, + }) = _RouterState; factory RouterState.fromJson(dynamic json) => _$RouterStateFromJson(json as Map); @@ -36,7 +35,6 @@ class RouterCubit extends Cubit { RouterCubit(AccountRepository accountRepository) : super(RouterState( hasAnyAccount: accountRepository.getLocalAccounts().isNotEmpty, - hasActiveChat: false, )) { // Subscribe to repository streams _accountRepositorySubscription = accountRepository.stream.listen((event) { @@ -52,10 +50,6 @@ class RouterCubit extends Cubit { }); } - void setHasActiveChat(bool active) { - emit(state.copyWith(hasActiveChat: active)); - } - @override Future close() async { await _accountRepositorySubscription.cancel(); @@ -64,19 +58,9 @@ class RouterCubit extends Cubit { /// Our application routes List get routes => [ - ShellRoute( - navigatorKey: _homeNavKey, - builder: (context, state, child) => HomeShell(child: child), - routes: [ - GoRoute( - path: '/', - builder: (context, state) => const HomeAccountReadyMain(), - ), - GoRoute( - path: '/chat', - builder: (context, state) => const HomeAccountReadyChat(), - ), - ], + GoRoute( + path: '/', + builder: (context, state) => const HomeScreen(), ), GoRoute( path: '/edit_account', @@ -116,31 +100,6 @@ class RouterCubit extends Cubit { if (!state.hasAnyAccount) { return '/new_account'; } - if (responsiveVisibility( - context: context, - tablet: false, - tabletLandscape: false, - desktop: false)) { - if (state.hasActiveChat) { - return '/chat'; - } - } - return null; - case '/chat': - if (!state.hasAnyAccount) { - return '/new_account'; - } - if (responsiveVisibility( - context: context, - tablet: false, - tabletLandscape: false, - desktop: false)) { - if (!state.hasActiveChat) { - return '/'; - } - } else { - return '/'; - } return null; case '/new_account': return null; diff --git a/lib/router/cubit/router_cubit.freezed.dart b/lib/router/cubit/router_cubit.freezed.dart index c36db7d..e44cd91 100644 --- a/lib/router/cubit/router_cubit.freezed.dart +++ b/lib/router/cubit/router_cubit.freezed.dart @@ -21,7 +21,6 @@ RouterState _$RouterStateFromJson(Map json) { /// @nodoc mixin _$RouterState { bool get hasAnyAccount => throw _privateConstructorUsedError; - bool get hasActiveChat => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -35,7 +34,7 @@ abstract class $RouterStateCopyWith<$Res> { RouterState value, $Res Function(RouterState) then) = _$RouterStateCopyWithImpl<$Res, RouterState>; @useResult - $Res call({bool hasAnyAccount, bool hasActiveChat}); + $Res call({bool hasAnyAccount}); } /// @nodoc @@ -52,17 +51,12 @@ class _$RouterStateCopyWithImpl<$Res, $Val extends RouterState> @override $Res call({ Object? hasAnyAccount = null, - Object? hasActiveChat = null, }) { return _then(_value.copyWith( 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); } } @@ -75,7 +69,7 @@ abstract class _$$RouterStateImplCopyWith<$Res> __$$RouterStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({bool hasAnyAccount, bool hasActiveChat}); + $Res call({bool hasAnyAccount}); } /// @nodoc @@ -90,17 +84,12 @@ class __$$RouterStateImplCopyWithImpl<$Res> @override $Res call({ Object? hasAnyAccount = null, - Object? hasActiveChat = null, }) { return _then(_$RouterStateImpl( 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, )); } } @@ -108,20 +97,17 @@ class __$$RouterStateImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState { - const _$RouterStateImpl( - {required this.hasAnyAccount, required this.hasActiveChat}); + const _$RouterStateImpl({required this.hasAnyAccount}); factory _$RouterStateImpl.fromJson(Map json) => _$$RouterStateImplFromJson(json); @override final bool hasAnyAccount; - @override - final bool hasActiveChat; @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'RouterState(hasAnyAccount: $hasAnyAccount, hasActiveChat: $hasActiveChat)'; + return 'RouterState(hasAnyAccount: $hasAnyAccount)'; } @override @@ -129,8 +115,7 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('type', 'RouterState')) - ..add(DiagnosticsProperty('hasAnyAccount', hasAnyAccount)) - ..add(DiagnosticsProperty('hasActiveChat', hasActiveChat)); + ..add(DiagnosticsProperty('hasAnyAccount', hasAnyAccount)); } @override @@ -139,14 +124,12 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState { (other.runtimeType == runtimeType && other is _$RouterStateImpl && (identical(other.hasAnyAccount, hasAnyAccount) || - other.hasAnyAccount == hasAnyAccount) && - (identical(other.hasActiveChat, hasActiveChat) || - other.hasActiveChat == hasActiveChat)); + other.hasAnyAccount == hasAnyAccount)); } @JsonKey(ignore: true) @override - int get hashCode => Object.hash(runtimeType, hasAnyAccount, hasActiveChat); + int get hashCode => Object.hash(runtimeType, hasAnyAccount); @JsonKey(ignore: true) @override @@ -163,9 +146,8 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState { } abstract class _RouterState implements RouterState { - const factory _RouterState( - {required final bool hasAnyAccount, - required final bool hasActiveChat}) = _$RouterStateImpl; + const factory _RouterState({required final bool hasAnyAccount}) = + _$RouterStateImpl; factory _RouterState.fromJson(Map json) = _$RouterStateImpl.fromJson; @@ -173,8 +155,6 @@ abstract class _RouterState implements RouterState { @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 index 31ca24a..4d9241c 100644 --- a/lib/router/cubit/router_cubit.g.dart +++ b/lib/router/cubit/router_cubit.g.dart @@ -9,11 +9,9 @@ part of 'router_cubit.dart'; _$RouterStateImpl _$$RouterStateImplFromJson(Map json) => _$RouterStateImpl( hasAnyAccount: json['has_any_account'] as bool, - hasActiveChat: json['has_active_chat'] as bool, ); Map _$$RouterStateImplToJson(_$RouterStateImpl instance) => { 'has_any_account': instance.hasAnyAccount, - 'has_active_chat': instance.hasActiveChat, }; From 3edf2ebb468b0f560aeeb158b86445a69b25a8dc Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 17 Jun 2024 23:38:30 -0400 Subject: [PATCH 142/270] refactor cubits to keep them alive, wip --- .../cubits/account_info_cubit.dart | 8 +- .../cubits/account_record_cubit.dart | 13 +- .../account_records_bloc_map_cubit.dart | 10 +- .../cubits/local_accounts_cubit.dart | 15 +- .../cubits/per_account_collection_cubit.dart | 209 ++++++++++++++++++ .../cubits/user_logins_cubit.dart | 14 +- lib/account_manager/models/account_info.dart | 48 +++- lib/account_manager/models/models.dart | 2 +- .../per_account_collection_state.dart | 17 ++ .../per_account_collection_state.freezed.dart | 204 +++++++++++++++++ .../models/unlocked_account_info.dart | 55 ----- .../repository/account_repository.dart | 30 +-- lib/layout/home/home_screen.dart | 83 ++++--- 13 files changed, 550 insertions(+), 158 deletions(-) create mode 100644 lib/account_manager/cubits/per_account_collection_cubit.dart create mode 100644 lib/account_manager/models/per_account_collection_state/per_account_collection_state.dart create mode 100644 lib/account_manager/models/per_account_collection_state/per_account_collection_state.freezed.dart delete mode 100644 lib/account_manager/models/unlocked_account_info.dart diff --git a/lib/account_manager/cubits/account_info_cubit.dart b/lib/account_manager/cubits/account_info_cubit.dart index 234e694..d34b107 100644 --- a/lib/account_manager/cubits/account_info_cubit.dart +++ b/lib/account_manager/cubits/account_info_cubit.dart @@ -10,14 +10,18 @@ class AccountInfoCubit extends Cubit { AccountInfoCubit( AccountRepository accountRepository, TypedKey superIdentityRecordKey) : _accountRepository = accountRepository, - super(accountRepository.getAccountInfo(superIdentityRecordKey)) { + super(accountRepository.getAccountInfo(superIdentityRecordKey)!) { // Subscribe to streams _accountRepositorySubscription = _accountRepository.stream.listen((change) { switch (change) { case AccountRepositoryChange.activeLocalAccount: case AccountRepositoryChange.localAccounts: case AccountRepositoryChange.userLogins: - emit(accountRepository.getAccountInfo(superIdentityRecordKey)); + final acctInfo = + accountRepository.getAccountInfo(superIdentityRecordKey); + if (acctInfo != null) { + emit(acctInfo); + } break; } }); diff --git a/lib/account_manager/cubits/account_record_cubit.dart b/lib/account_manager/cubits/account_record_cubit.dart index b393161..4028d65 100644 --- a/lib/account_manager/cubits/account_record_cubit.dart +++ b/lib/account_manager/cubits/account_record_cubit.dart @@ -15,18 +15,13 @@ typedef AccountRecordState = proto.Account; /// tabledb-local storage, encrypted by the unlock code for the account. class AccountRecordCubit extends DefaultDHTRecordCubit { AccountRecordCubit( - {required AccountRepository accountRepository, - required TypedKey superIdentityRecordKey}) + {required LocalAccount localAccount, required UserLogin userLogin}) : super( decodeState: proto.Account.fromBuffer, - open: () => _open(accountRepository, superIdentityRecordKey)); - - static Future _open(AccountRepository accountRepository, - TypedKey superIdentityRecordKey) async { - final localAccount = - accountRepository.fetchLocalAccount(superIdentityRecordKey)!; - final userLogin = accountRepository.fetchUserLogin(superIdentityRecordKey)!; + open: () => _open(localAccount, userLogin)); + static Future _open( + LocalAccount localAccount, UserLogin userLogin) async { // Record not yet open, do it final pool = DHTRecordPool.instance; final record = await pool.openRecordOwned( diff --git a/lib/account_manager/cubits/account_records_bloc_map_cubit.dart b/lib/account_manager/cubits/account_records_bloc_map_cubit.dart index eee6d1f..50a7e60 100644 --- a/lib/account_manager/cubits/account_records_bloc_map_cubit.dart +++ b/lib/account_manager/cubits/account_records_bloc_map_cubit.dart @@ -12,12 +12,12 @@ typedef AccountRecordsBlocMapState /// Ensures there is an single account record cubit for each logged in account class AccountRecordsBlocMapCubit extends BlocMapCubit, AccountRecordCubit> - with StateMapFollower { + with StateMapFollower { AccountRecordsBlocMapCubit( AccountRepository accountRepository, Locator locator) : _accountRepository = accountRepository { - // Follow the user logins cubit - follow(locator()); + // Follow the local accounts cubit + follow(locator()); } // Add account record cubit @@ -35,9 +35,9 @@ class AccountRecordsBlocMapCubit extends BlocMapCubit removeFromState(TypedKey key) => remove(key); @override - Future updateState(TypedKey key, UserLogin value) async { + Future updateState(TypedKey key, LocalAccount value) async { await _addAccountRecordCubit( - superIdentityRecordKey: value.superIdentityRecordKey); + superIdentityRecordKey: value.superIdentity.recordKey); } //////////////////////////////////////////////////////////////////////////// diff --git a/lib/account_manager/cubits/local_accounts_cubit.dart b/lib/account_manager/cubits/local_accounts_cubit.dart index a324602..704d8c5 100644 --- a/lib/account_manager/cubits/local_accounts_cubit.dart +++ b/lib/account_manager/cubits/local_accounts_cubit.dart @@ -1,12 +1,17 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; +import 'package:bloc_advanced_tools/bloc_advanced_tools.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.dart'; -class LocalAccountsCubit extends Cubit> { +typedef LocalAccountsState = IList; + +class LocalAccountsCubit extends Cubit + with StateMapFollowable { LocalAccountsCubit(AccountRepository accountRepository) : _accountRepository = accountRepository, super(accountRepository.getLocalAccounts()) { @@ -30,6 +35,14 @@ class LocalAccountsCubit extends Cubit> { await _accountRepositorySubscription.cancel(); } + /// StateMapFollowable ///////////////////////// + @override + IMap getStateMap(LocalAccountsState state) { + final stateValue = state; + return IMap.fromIterable(stateValue, + keyMapper: (e) => e.superIdentity.recordKey, valueMapper: (e) => e); + } + final AccountRepository _accountRepository; late final StreamSubscription _accountRepositorySubscription; diff --git a/lib/account_manager/cubits/per_account_collection_cubit.dart b/lib/account_manager/cubits/per_account_collection_cubit.dart new file mode 100644 index 0000000..e76488a --- /dev/null +++ b/lib/account_manager/cubits/per_account_collection_cubit.dart @@ -0,0 +1,209 @@ +import 'dart:async'; + +import 'package:async_tools/async_tools.dart'; +import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:provider/provider.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../chat/chat.dart'; +import '../../chat_list/chat_list.dart'; +import '../../contact_invitation/contact_invitation.dart'; +import '../../contacts/contacts.dart'; +import '../../proto/proto.dart' as proto; +import '../account_manager.dart'; + +class PerAccountCollectionCubit extends Cubit { + PerAccountCollectionCubit({ + required Locator locator, + required this.accountInfoCubit, + }) : _locator = locator, + super(_initialState(accountInfoCubit)) { + // Async Init + _initWait.add(_init); + } + + @override + Future close() async { + await _processor.close(); + await accountInfoCubit.close(); + await _accountRecordSubscription?.cancel(); + await accountRecordCubit?.close(); + + await super.close(); + } + + Future _init() async { + await _initWait(); + + // subscribe to accountInfo changes + _processor.follow(accountInfoCubit.stream, accountInfoCubit.state, + _followAccountInfoState); + } + + static PerAccountCollectionState _initialState( + AccountInfoCubit accountInfoCubit) => + PerAccountCollectionState( + accountInfo: accountInfoCubit.state, + avAccountRecordState: const AsyncValue.loading(), + contactInvitationListCubit: null); + + Future _followAccountInfoState(AccountInfo accountInfo) async { + var nextState = state.copyWith(accountInfo: accountInfo); + + if (accountInfo.userLogin == null) { + /////////////// Not logged in ///////////////// + + // Unsubscribe AccountRecordCubit + await _accountRecordSubscription?.cancel(); + _accountRecordSubscription = null; + + // Update state + nextState = + _updateAccountRecordState(nextState, const AsyncValue.loading()); + emit(nextState); + + // Close AccountRecordCubit + await accountRecordCubit?.close(); + accountRecordCubit = null; + } else { + ///////////////// Logged in /////////////////// + + // AccountRecordCubit + accountRecordCubit ??= AccountRecordCubit( + localAccount: accountInfo.localAccount, + userLogin: accountInfo.userLogin!); + + // Update State + nextState = + _updateAccountRecordState(nextState, accountRecordCubit!.state); + emit(nextState); + + // Subscribe AccountRecordCubit + _accountRecordSubscription ??= + accountRecordCubit!.stream.listen((avAccountRecordState) { + emit(_updateAccountRecordState(state, avAccountRecordState)); + }); + } + } + + PerAccountCollectionState _updateAccountRecordState( + PerAccountCollectionState prevState, + AsyncValue avAccountRecordState) { + // Get next state + final nextState = + state.copyWith(avAccountRecordState: accountRecordCubit!.state); + + // Get bloc parameters + final accountRecordKey = nextState.accountInfo.accountRecordKey; + + // ContactInvitationListCubit + final contactInvitationListRecordPointer = nextState + .avAccountRecordState.asData?.value.contactInvitationRecords + .toVeilid(); + + contactInvitationListCubitUpdater.update( + contactInvitationListRecordPointer == null + ? null + : ( + collectionLocator, + accountRecordKey, + contactInvitationListRecordPointer + )); + + // ContactListCubit + final contactListRecordPointer = + nextState.avAccountRecordState.asData?.value.contactList.toVeilid(); + + contactListCubitUpdater.update(contactListRecordPointer == null + ? null + : (collectionLocator, accountRecordKey, contactListRecordPointer)); + + // WaitingInvitationsBlocMapCubit + waitingInvitationsBlocMapCubitUpdater.update( + nextState.avAccountRecordState.isData ? collectionLocator : null); + + // ActiveChatCubit + activeChatCubitUpdater + .update(nextState.avAccountRecordState.isData ? true : null); + + // ChatListCubit + final chatListRecordPointer = + nextState.avAccountRecordState.asData?.value.chatList.toVeilid(); + + chatListCubitUpdater.update(chatListRecordPointer == null + ? null + : (collectionLocator, accountRecordKey, chatListRecordPointer)); + + // ActiveConversationsBlocMapCubit + // xxx + + return nextState; + } + + T collectionLocator() { + if (T is AccountInfoCubit) { + return accountInfoCubit as T; + } + if (T is AccountRecordCubit) { + return accountRecordCubit! as T; + } + if (T is ContactInvitationListCubit) { + return contactInvitationListCubitUpdater.bloc! as T; + } + if (T is ContactListCubit) { + return contactListCubitUpdater.bloc! as T; + } + if (T is WaitingInvitationsBlocMapCubit) { + return waitingInvitationsBlocMapCubitUpdater.bloc! as T; + } + if (T is ActiveChatCubit) { + return activeChatCubitUpdater.bloc! as T; + } + if (T is ChatListCubit) { + return chatListCubitUpdater.bloc! as T; + } + return _locator(); + } + + final Locator _locator; + final _processor = SingleStateProcessor(); + final _initWait = WaitSet(); + + // Per-account cubits regardless of login state + final AccountInfoCubit accountInfoCubit; + + // Per logged-in account cubits + AccountRecordCubit? accountRecordCubit; + StreamSubscription>? + _accountRecordSubscription; + final contactInvitationListCubitUpdater = BlocUpdater< + ContactInvitationListCubit, + (Locator, TypedKey, OwnedDHTRecordPointer)>( + create: (params) => ContactInvitationListCubit( + locator: params.$1, + accountRecordKey: params.$2, + contactInvitationListRecordPointer: params.$3, + )); + final contactListCubitUpdater = + BlocUpdater( + create: (params) => ContactListCubit( + locator: params.$1, + accountRecordKey: params.$2, + contactListRecordPointer: params.$3, + )); + final waitingInvitationsBlocMapCubitUpdater = + BlocUpdater( + create: (params) => WaitingInvitationsBlocMapCubit( + locator: params, + )); + final activeChatCubitUpdater = + BlocUpdater(create: (_) => ActiveChatCubit(null)); + final chatListCubitUpdater = + BlocUpdater( + create: (params) => ChatListCubit( + locator: params.$1, + accountRecordKey: params.$2, + chatListRecordPointer: params.$3, + )); +} diff --git a/lib/account_manager/cubits/user_logins_cubit.dart b/lib/account_manager/cubits/user_logins_cubit.dart index 75d6ad1..734ced3 100644 --- a/lib/account_manager/cubits/user_logins_cubit.dart +++ b/lib/account_manager/cubits/user_logins_cubit.dart @@ -1,17 +1,14 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; -import 'package:bloc_advanced_tools/bloc_advanced_tools.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.dart'; typedef UserLoginsState = IList; -class UserLoginsCubit extends Cubit - with StateMapFollowable { +class UserLoginsCubit extends Cubit { UserLoginsCubit(AccountRepository accountRepository) : _accountRepository = accountRepository, super(accountRepository.getUserLogins()) { @@ -34,15 +31,6 @@ class UserLoginsCubit extends Cubit await super.close(); await _accountRepositorySubscription.cancel(); } - - /// StateMapFollowable ///////////////////////// - @override - IMap getStateMap(UserLoginsState state) { - final stateValue = state; - return IMap.fromIterable(stateValue, - keyMapper: (e) => e.superIdentityRecordKey, valueMapper: (e) => e); - } - //////////////////////////////////////////////////////////////////////////// final AccountRepository _accountRepository; diff --git a/lib/account_manager/models/account_info.dart b/lib/account_manager/models/account_info.dart index 78b330e..a2a9a39 100644 --- a/lib/account_manager/models/account_info.dart +++ b/lib/account_manager/models/account_info.dart @@ -1,10 +1,12 @@ +import 'dart:convert'; + import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; +import 'package:veilid_support/veilid_support.dart'; -import 'unlocked_account_info.dart'; +import '../account_manager.dart'; enum AccountInfoStatus { - noAccount, accountInvalid, accountLocked, accountUnlocked, @@ -15,13 +17,49 @@ class AccountInfo extends Equatable { const AccountInfo({ required this.status, required this.active, - required this.unlockedAccountInfo, + required this.localAccount, + required this.userLogin, }); final AccountInfoStatus status; final bool active; - final UnlockedAccountInfo? unlockedAccountInfo; + final LocalAccount localAccount; + final UserLogin? userLogin; @override - List get props => [status, active, unlockedAccountInfo]; + List get props => [ + status, + active, + localAccount, + userLogin, + ]; +} + +extension AccountInfoExt on AccountInfo { + TypedKey get superIdentityRecordKey => localAccount.superIdentity.recordKey; + TypedKey get accountRecordKey => + userLogin!.accountRecordInfo.accountRecord.recordKey; + TypedKey get identityTypedPublicKey => + localAccount.superIdentity.currentInstance.typedPublicKey; + PublicKey get identityPublicKey => + localAccount.superIdentity.currentInstance.publicKey; + SecretKey get identitySecretKey => userLogin!.identitySecret.value; + KeyPair get identityWriter => + KeyPair(key: identityPublicKey, secret: identitySecretKey); + Future get identityCryptoSystem => + localAccount.superIdentity.currentInstance.cryptoSystem; + + 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 VeilidCryptoPrivate.fromSharedSecret( + identitySecret.kind, sharedSecret); + return messagesCrypto; + } } diff --git a/lib/account_manager/models/models.dart b/lib/account_manager/models/models.dart index bb08695..2860eec 100644 --- a/lib/account_manager/models/models.dart +++ b/lib/account_manager/models/models.dart @@ -1,6 +1,6 @@ export 'account_info.dart'; -export 'unlocked_account_info.dart'; export 'encryption_key_type.dart'; export 'local_account/local_account.dart'; export 'new_profile_spec.dart'; +export 'per_account_collection_state/per_account_collection_state.dart'; export 'user_login/user_login.dart'; diff --git a/lib/account_manager/models/per_account_collection_state/per_account_collection_state.dart b/lib/account_manager/models/per_account_collection_state/per_account_collection_state.dart new file mode 100644 index 0000000..9794784 --- /dev/null +++ b/lib/account_manager/models/per_account_collection_state/per_account_collection_state.dart @@ -0,0 +1,17 @@ +import 'package:async_tools/async_tools.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../../../contact_invitation/contact_invitation.dart'; +import '../../../proto/proto.dart' show Account; +import '../../account_manager.dart'; + +part 'per_account_collection_state.freezed.dart'; + +@freezed +class PerAccountCollectionState with _$PerAccountCollectionState { + const factory PerAccountCollectionState( + {required AccountInfo accountInfo, + required AsyncValue avAccountRecordState, + required ContactInvitationListCubit? contactInvitationListCubit}) = + _PerAccountCollectionState; +} diff --git a/lib/account_manager/models/per_account_collection_state/per_account_collection_state.freezed.dart b/lib/account_manager/models/per_account_collection_state/per_account_collection_state.freezed.dart new file mode 100644 index 0000000..d49f42a --- /dev/null +++ b/lib/account_manager/models/per_account_collection_state/per_account_collection_state.freezed.dart @@ -0,0 +1,204 @@ +// 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 'per_account_collection_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#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$PerAccountCollectionState { + AccountInfo get accountInfo => throw _privateConstructorUsedError; + AsyncValue get avAccountRecordState => + throw _privateConstructorUsedError; + ContactInvitationListCubit? get contactInvitationListCubit => + throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $PerAccountCollectionStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PerAccountCollectionStateCopyWith<$Res> { + factory $PerAccountCollectionStateCopyWith(PerAccountCollectionState value, + $Res Function(PerAccountCollectionState) then) = + _$PerAccountCollectionStateCopyWithImpl<$Res, PerAccountCollectionState>; + @useResult + $Res call( + {AccountInfo accountInfo, + AsyncValue avAccountRecordState, + ContactInvitationListCubit? contactInvitationListCubit}); + + $AsyncValueCopyWith get avAccountRecordState; +} + +/// @nodoc +class _$PerAccountCollectionStateCopyWithImpl<$Res, + $Val extends PerAccountCollectionState> + implements $PerAccountCollectionStateCopyWith<$Res> { + _$PerAccountCollectionStateCopyWithImpl(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? accountInfo = null, + Object? avAccountRecordState = null, + Object? contactInvitationListCubit = freezed, + }) { + return _then(_value.copyWith( + accountInfo: null == accountInfo + ? _value.accountInfo + : accountInfo // ignore: cast_nullable_to_non_nullable + as AccountInfo, + avAccountRecordState: null == avAccountRecordState + ? _value.avAccountRecordState + : avAccountRecordState // ignore: cast_nullable_to_non_nullable + as AsyncValue, + contactInvitationListCubit: freezed == contactInvitationListCubit + ? _value.contactInvitationListCubit + : contactInvitationListCubit // ignore: cast_nullable_to_non_nullable + as ContactInvitationListCubit?, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $AsyncValueCopyWith get avAccountRecordState { + return $AsyncValueCopyWith(_value.avAccountRecordState, + (value) { + return _then(_value.copyWith(avAccountRecordState: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$PerAccountCollectionStateImplCopyWith<$Res> + implements $PerAccountCollectionStateCopyWith<$Res> { + factory _$$PerAccountCollectionStateImplCopyWith( + _$PerAccountCollectionStateImpl value, + $Res Function(_$PerAccountCollectionStateImpl) then) = + __$$PerAccountCollectionStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {AccountInfo accountInfo, + AsyncValue avAccountRecordState, + ContactInvitationListCubit? contactInvitationListCubit}); + + @override + $AsyncValueCopyWith get avAccountRecordState; +} + +/// @nodoc +class __$$PerAccountCollectionStateImplCopyWithImpl<$Res> + extends _$PerAccountCollectionStateCopyWithImpl<$Res, + _$PerAccountCollectionStateImpl> + implements _$$PerAccountCollectionStateImplCopyWith<$Res> { + __$$PerAccountCollectionStateImplCopyWithImpl( + _$PerAccountCollectionStateImpl _value, + $Res Function(_$PerAccountCollectionStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? accountInfo = null, + Object? avAccountRecordState = null, + Object? contactInvitationListCubit = freezed, + }) { + return _then(_$PerAccountCollectionStateImpl( + accountInfo: null == accountInfo + ? _value.accountInfo + : accountInfo // ignore: cast_nullable_to_non_nullable + as AccountInfo, + avAccountRecordState: null == avAccountRecordState + ? _value.avAccountRecordState + : avAccountRecordState // ignore: cast_nullable_to_non_nullable + as AsyncValue, + contactInvitationListCubit: freezed == contactInvitationListCubit + ? _value.contactInvitationListCubit + : contactInvitationListCubit // ignore: cast_nullable_to_non_nullable + as ContactInvitationListCubit?, + )); + } +} + +/// @nodoc + +class _$PerAccountCollectionStateImpl implements _PerAccountCollectionState { + const _$PerAccountCollectionStateImpl( + {required this.accountInfo, + required this.avAccountRecordState, + required this.contactInvitationListCubit}); + + @override + final AccountInfo accountInfo; + @override + final AsyncValue avAccountRecordState; + @override + final ContactInvitationListCubit? contactInvitationListCubit; + + @override + String toString() { + return 'PerAccountCollectionState(accountInfo: $accountInfo, avAccountRecordState: $avAccountRecordState, contactInvitationListCubit: $contactInvitationListCubit)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PerAccountCollectionStateImpl && + (identical(other.accountInfo, accountInfo) || + other.accountInfo == accountInfo) && + (identical(other.avAccountRecordState, avAccountRecordState) || + other.avAccountRecordState == avAccountRecordState) && + (identical(other.contactInvitationListCubit, + contactInvitationListCubit) || + other.contactInvitationListCubit == + contactInvitationListCubit)); + } + + @override + int get hashCode => Object.hash(runtimeType, accountInfo, + avAccountRecordState, contactInvitationListCubit); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$PerAccountCollectionStateImplCopyWith<_$PerAccountCollectionStateImpl> + get copyWith => __$$PerAccountCollectionStateImplCopyWithImpl< + _$PerAccountCollectionStateImpl>(this, _$identity); +} + +abstract class _PerAccountCollectionState implements PerAccountCollectionState { + const factory _PerAccountCollectionState( + {required final AccountInfo accountInfo, + required final AsyncValue avAccountRecordState, + required final ContactInvitationListCubit? + contactInvitationListCubit}) = _$PerAccountCollectionStateImpl; + + @override + AccountInfo get accountInfo; + @override + AsyncValue get avAccountRecordState; + @override + ContactInvitationListCubit? get contactInvitationListCubit; + @override + @JsonKey(ignore: true) + _$$PerAccountCollectionStateImplCopyWith<_$PerAccountCollectionStateImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/account_manager/models/unlocked_account_info.dart b/lib/account_manager/models/unlocked_account_info.dart deleted file mode 100644 index 2aa5bdb..0000000 --- a/lib/account_manager/models/unlocked_account_info.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'dart:convert'; - -import 'package:equatable/equatable.dart'; -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 UnlockedAccountInfo extends Equatable { - const UnlockedAccountInfo({ - required this.localAccount, - required this.userLogin, - }); - - //////////////////////////////////////////////////////////////////////////// - // Public Interface - - TypedKey get superIdentityRecordKey => localAccount.superIdentity.recordKey; - TypedKey get accountRecordKey => - userLogin.accountRecordInfo.accountRecord.recordKey; - TypedKey get identityTypedPublicKey => - localAccount.superIdentity.currentInstance.typedPublicKey; - PublicKey get identityPublicKey => - localAccount.superIdentity.currentInstance.publicKey; - SecretKey get identitySecretKey => userLogin.identitySecret.value; - KeyPair get identityWriter => - KeyPair(key: identityPublicKey, secret: identitySecretKey); - Future get identityCryptoSystem => - localAccount.superIdentity.currentInstance.cryptoSystem; - - 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 VeilidCryptoPrivate.fromSharedSecret( - identitySecret.kind, sharedSecret); - return messagesCrypto; - } - - //////////////////////////////////////////////////////////////////////////// - // Fields - - final LocalAccount localAccount; - final UserLogin userLogin; - - @override - List get props => [localAccount, userLogin]; -} diff --git a/lib/account_manager/repository/account_repository.dart b/lib/account_manager/repository/account_repository.dart index ff916ac..73ece3e 100644 --- a/lib/account_manager/repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository.dart @@ -92,29 +92,15 @@ class AccountRepository { return userLogins[idx]; } - AccountInfo getAccountInfo(TypedKey? superIdentityRecordKey) { + AccountInfo? getAccountInfo(TypedKey superIdentityRecordKey) { // Get active account if we have one final activeLocalAccount = getActiveLocalAccount(); - if (superIdentityRecordKey == null) { - if (activeLocalAccount == null) { - // No user logged in - return const AccountInfo( - status: AccountInfoStatus.noAccount, - active: false, - unlockedAccountInfo: null); - } - superIdentityRecordKey = activeLocalAccount; - } final active = superIdentityRecordKey == activeLocalAccount; // Get which local account we want to fetch the profile for final localAccount = fetchLocalAccount(superIdentityRecordKey); if (localAccount == null) { - // account does not exist - return AccountInfo( - status: AccountInfoStatus.noAccount, - active: active, - unlockedAccountInfo: null); + return null; } // See if we've logged into this account or if it is locked @@ -122,17 +108,19 @@ class AccountRepository { if (userLogin == null) { // Account was locked return AccountInfo( - status: AccountInfoStatus.accountLocked, - active: active, - unlockedAccountInfo: null); + status: AccountInfoStatus.accountLocked, + active: active, + localAccount: localAccount, + userLogin: null, + ); } // Got account, decrypted and decoded return AccountInfo( status: AccountInfoStatus.accountUnlocked, active: active, - unlockedAccountInfo: - UnlockedAccountInfo(localAccount: localAccount, userLogin: userLogin), + localAccount: localAccount, + userLogin: userLogin, ); } diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index 53fef55..d064ad7 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -164,44 +164,41 @@ class HomeScreenState extends State { ], child: Builder(builder: _buildAccountReadyDeviceSpecific))); } - Widget _buildAccount(BuildContext context) { - // Get active account info status - final ( - accountInfoStatus, - accountInfoActive, - superIdentityRecordKey - ) = context - .select((c) => ( - c.state.status, - c.state.active, - c.state.unlockedAccountInfo?.superIdentityRecordKey - )); + Widget _buildAccount(BuildContext context, TypedKey superIdentityRecordKey) => + BlocProvider( + key: ValueKey(superIdentityRecordKey), + create: (context) => AccountInfoCubit( + AccountRepository.instance, superIdentityRecordKey), + child: Builder(builder: (context) { + // Get active account info status + final accountInfoStatus = + context.select( + (c) => c.state.status); - switch (accountInfoStatus) { - case AccountInfoStatus.noAccount: - return const HomeAccountMissing(); - case AccountInfoStatus.accountInvalid: - return const HomeAccountInvalid(); - case AccountInfoStatus.accountLocked: - return const HomeAccountLocked(); - case AccountInfoStatus.accountUnlocked: + switch (accountInfoStatus) { + case AccountInfoStatus.noAccount: + return const HomeAccountMissing(); + case AccountInfoStatus.accountInvalid: + return const HomeAccountInvalid(); + case AccountInfoStatus.accountLocked: + return const HomeAccountLocked(); + case AccountInfoStatus.accountUnlocked: - // Get the current active account record cubit - final activeAccountRecordCubit = - context.select( - (c) => superIdentityRecordKey == null - ? null - : c.tryOperate(superIdentityRecordKey, closure: (x) => x)); - if (activeAccountRecordCubit == null) { - return waitingPage(); - } + // Get the current active account record cubit + final activeAccountRecordCubit = context + .select( + (c) => c.tryOperate(superIdentityRecordKey, + closure: (x) => x)); + if (activeAccountRecordCubit == null) { + return waitingPage(); + } - return MultiBlocProvider(providers: [ - BlocProvider.value( - value: activeAccountRecordCubit), - ], child: Builder(builder: _buildUnlockedAccount)); - } - } + return MultiBlocProvider(providers: [ + BlocProvider.value( + value: activeAccountRecordCubit), + ], child: Builder(builder: _buildUnlockedAccount)); + } + })); Widget _buildAccountPageView(BuildContext context) { final localAccounts = context.watch().state; @@ -221,8 +218,7 @@ class HomeScreenState extends State { value.dispose(); }, child: Builder( - builder: (context) => PageView.builder( - itemCount: localAccounts.length, + builder: (context) => PageView.custom( onPageChanged: (idx) { singleFuture(this, () async { await AccountRepository.instance.switchToAccount( @@ -232,15 +228,10 @@ class HomeScreenState extends State { controller: context .read() .pageController, - itemBuilder: (context, index) { - final localAccount = localAccounts[index]; - return BlocProvider( - key: ValueKey(localAccount.superIdentity.recordKey), - create: (context) => AccountInfoCubit( - AccountRepository.instance, - localAccount.superIdentity.recordKey), - child: Builder(builder: _buildAccount)); - }))); + childrenDelegate: SliverChildListDelegate(localAccounts + .map((la) => + _buildAccount(context, la.superIdentity.recordKey)) + .toList())))); } @override From c40f835ec590b3d574fedf1251fe8388c589988f Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 18 Jun 2024 21:20:06 -0400 Subject: [PATCH 143/270] checkpoint --- .../cubits/account_info_cubit.dart | 3 +- lib/account_manager/cubits/cubits.dart | 3 +- ...er_account_collection_bloc_map_cubit.dart} | 27 ++-- .../cubits/per_account_collection_cubit.dart | 133 +++++++++++++----- lib/account_manager/models/account_info.dart | 3 - .../repository/account_repository.dart | 6 - .../views/edit_account_page.dart | 8 +- lib/app.dart | 7 +- lib/chat/cubits/chat_component_cubit.dart | 35 +++-- .../cubits/single_contact_messages_cubit.dart | 33 ++--- lib/chat/views/chat_component_widget.dart | 10 +- lib/chat_list/cubits/chat_list_cubit.dart | 22 +-- .../cubits/contact_invitation_list_cubit.dart | 38 ++--- .../cubits/contact_request_inbox_cubit.dart | 11 +- .../cubits/waiting_invitation_cubit.dart | 21 +-- .../waiting_invitations_bloc_map_cubit.dart | 21 +-- .../models/valid_contact_invitation.dart | 96 +++++-------- .../views/create_invitation_dialog.dart | 10 ++ .../views/invitation_dialog.dart | 6 +- lib/contacts/cubits/contact_list_cubit.dart | 14 +- .../active_conversations_bloc_map_cubit.dart | 31 ++-- ...ve_single_contact_chat_bloc_map_cubit.dart | 26 ++-- .../cubits/conversation_cubit.dart | 39 ++--- lib/layout/home/drawer_menu/drawer_menu.dart | 3 +- lib/layout/home/home_screen.dart | 84 ++++++----- 25 files changed, 378 insertions(+), 312 deletions(-) rename lib/account_manager/cubits/{account_records_bloc_map_cubit.dart => per_account_collection_bloc_map_cubit.dart} (60%) diff --git a/lib/account_manager/cubits/account_info_cubit.dart b/lib/account_manager/cubits/account_info_cubit.dart index d34b107..d9d93fc 100644 --- a/lib/account_manager/cubits/account_info_cubit.dart +++ b/lib/account_manager/cubits/account_info_cubit.dart @@ -8,7 +8,8 @@ import '../repository/account_repository.dart'; class AccountInfoCubit extends Cubit { AccountInfoCubit( - AccountRepository accountRepository, TypedKey superIdentityRecordKey) + {required AccountRepository accountRepository, + required TypedKey superIdentityRecordKey}) : _accountRepository = accountRepository, super(accountRepository.getAccountInfo(superIdentityRecordKey)!) { // Subscribe to streams diff --git a/lib/account_manager/cubits/cubits.dart b/lib/account_manager/cubits/cubits.dart index f507ff2..da268ae 100644 --- a/lib/account_manager/cubits/cubits.dart +++ b/lib/account_manager/cubits/cubits.dart @@ -1,6 +1,7 @@ export 'account_info_cubit.dart'; export 'account_record_cubit.dart'; -export 'account_records_bloc_map_cubit.dart'; export 'active_local_account_cubit.dart'; export 'local_accounts_cubit.dart'; +export 'per_account_collection_bloc_map_cubit.dart'; +export 'per_account_collection_cubit.dart'; export 'user_logins_cubit.dart'; diff --git a/lib/account_manager/cubits/account_records_bloc_map_cubit.dart b/lib/account_manager/cubits/per_account_collection_bloc_map_cubit.dart similarity index 60% rename from lib/account_manager/cubits/account_records_bloc_map_cubit.dart rename to lib/account_manager/cubits/per_account_collection_bloc_map_cubit.dart index 50a7e60..8810376 100644 --- a/lib/account_manager/cubits/account_records_bloc_map_cubit.dart +++ b/lib/account_manager/cubits/per_account_collection_bloc_map_cubit.dart @@ -8,26 +8,30 @@ import '../../account_manager/account_manager.dart'; typedef AccountRecordsBlocMapState = BlocMapState>; -/// Map of the logged in user accounts to their AccountRecordCubit +/// Map of the logged in user accounts to their PerAccountCollectionCubit /// Ensures there is an single account record cubit for each logged in account -class AccountRecordsBlocMapCubit extends BlocMapCubit, AccountRecordCubit> +class PerAccountCollectionBlocMapCubit extends BlocMapCubit with StateMapFollower { - AccountRecordsBlocMapCubit( - AccountRepository accountRepository, Locator locator) - : _accountRepository = accountRepository { + PerAccountCollectionBlocMapCubit({ + required Locator locator, + required AccountRepository accountRepository, + }) : _locator = locator, + _accountRepository = accountRepository { // Follow the local accounts cubit follow(locator()); } // Add account record cubit - Future _addAccountRecordCubit( + Future _addPerAccountCollectionCubit( {required TypedKey superIdentityRecordKey}) async => add(() => MapEntry( superIdentityRecordKey, - AccountRecordCubit( - accountRepository: _accountRepository, - superIdentityRecordKey: superIdentityRecordKey))); + PerAccountCollectionCubit( + locator: _locator, + accountInfoCubit: AccountInfoCubit( + accountRepository: _accountRepository, + superIdentityRecordKey: superIdentityRecordKey)))); /// StateFollower ///////////////////////// @@ -36,10 +40,11 @@ class AccountRecordsBlocMapCubit extends BlocMapCubit updateState(TypedKey key, LocalAccount value) async { - await _addAccountRecordCubit( + await _addPerAccountCollectionCubit( superIdentityRecordKey: value.superIdentity.recordKey); } //////////////////////////////////////////////////////////////////////////// final AccountRepository _accountRepository; + final Locator _locator; } diff --git a/lib/account_manager/cubits/per_account_collection_cubit.dart b/lib/account_manager/cubits/per_account_collection_cubit.dart index e76488a..b67e234 100644 --- a/lib/account_manager/cubits/per_account_collection_cubit.dart +++ b/lib/account_manager/cubits/per_account_collection_cubit.dart @@ -10,6 +10,7 @@ import '../../chat/chat.dart'; import '../../chat_list/chat_list.dart'; import '../../contact_invitation/contact_invitation.dart'; import '../../contacts/contacts.dart'; +import '../../conversation/conversation.dart'; import '../../proto/proto.dart' as proto; import '../account_manager.dart'; @@ -30,6 +31,14 @@ class PerAccountCollectionCubit extends Cubit { await _accountRecordSubscription?.cancel(); await accountRecordCubit?.close(); + await activeSingleContactChatBlocMapCubitUpdater.close(); + await activeConversationsBlocMapCubitUpdater.close(); + await activeChatCubitUpdater.close(); + await waitingInvitationsBlocMapCubitUpdater.close(); + await chatListCubitUpdater.close(); + await contactListCubitUpdater.close(); + await contactInvitationListCubitUpdater.close(); + await super.close(); } @@ -95,48 +104,72 @@ class PerAccountCollectionCubit extends Cubit { state.copyWith(avAccountRecordState: accountRecordCubit!.state); // Get bloc parameters - final accountRecordKey = nextState.accountInfo.accountRecordKey; + final accountInfo = nextState.accountInfo; // ContactInvitationListCubit final contactInvitationListRecordPointer = nextState .avAccountRecordState.asData?.value.contactInvitationRecords .toVeilid(); - contactInvitationListCubitUpdater.update( + final contactInvitationListCubit = contactInvitationListCubitUpdater.update( contactInvitationListRecordPointer == null ? null - : ( - collectionLocator, - accountRecordKey, - contactInvitationListRecordPointer - )); + : (accountInfo, contactInvitationListRecordPointer)); // ContactListCubit final contactListRecordPointer = nextState.avAccountRecordState.asData?.value.contactList.toVeilid(); - contactListCubitUpdater.update(contactListRecordPointer == null - ? null - : (collectionLocator, accountRecordKey, contactListRecordPointer)); + final contactListCubit = contactListCubitUpdater.update( + contactListRecordPointer == null + ? null + : (accountInfo, contactListRecordPointer)); // WaitingInvitationsBlocMapCubit waitingInvitationsBlocMapCubitUpdater.update( - nextState.avAccountRecordState.isData ? collectionLocator : null); + contactInvitationListCubit == null + ? null + : (accountInfo, accountRecordCubit!, contactInvitationListCubit)); // ActiveChatCubit - activeChatCubitUpdater + final activeChatCubit = activeChatCubitUpdater .update(nextState.avAccountRecordState.isData ? true : null); // ChatListCubit final chatListRecordPointer = nextState.avAccountRecordState.asData?.value.chatList.toVeilid(); - chatListCubitUpdater.update(chatListRecordPointer == null - ? null - : (collectionLocator, accountRecordKey, chatListRecordPointer)); + final chatListCubit = chatListCubitUpdater.update( + chatListRecordPointer == null || activeChatCubit == null + ? null + : (accountInfo, chatListRecordPointer, activeChatCubit)); // ActiveConversationsBlocMapCubit - // xxx + final activeConversationsBlocMapCubit = + activeConversationsBlocMapCubitUpdater.update( + accountRecordCubit == null || + chatListCubit == null || + contactListCubit == null + ? null + : ( + accountInfo, + accountRecordCubit!, + chatListCubit, + contactListCubit + )); + + // ActiveSingleContactChatBlocMapCubit + activeSingleContactChatBlocMapCubitUpdater.update( + activeConversationsBlocMapCubit == null || + chatListCubit == null || + contactListCubit == null + ? null + : ( + accountInfo, + activeConversationsBlocMapCubit, + chatListCubit, + contactListCubit + )); return nextState; } @@ -163,6 +196,12 @@ class PerAccountCollectionCubit extends Cubit { if (T is ChatListCubit) { return chatListCubitUpdater.bloc! as T; } + if (T is ActiveConversationsBlocMapCubit) { + return activeConversationsBlocMapCubitUpdater.bloc! as T; + } + if (T is ActiveSingleContactChatBlocMapCubit) { + return activeSingleContactChatBlocMapCubitUpdater.bloc! as T; + } return _locator(); } @@ -178,32 +217,52 @@ class PerAccountCollectionCubit extends Cubit { StreamSubscription>? _accountRecordSubscription; final contactInvitationListCubitUpdater = BlocUpdater< - ContactInvitationListCubit, - (Locator, TypedKey, OwnedDHTRecordPointer)>( + ContactInvitationListCubit, (AccountInfo, OwnedDHTRecordPointer)>( create: (params) => ContactInvitationListCubit( - locator: params.$1, - accountRecordKey: params.$2, - contactInvitationListRecordPointer: params.$3, + accountInfo: params.$1, + contactInvitationListRecordPointer: params.$2, )); final contactListCubitUpdater = - BlocUpdater( + BlocUpdater( create: (params) => ContactListCubit( - locator: params.$1, - accountRecordKey: params.$2, - contactListRecordPointer: params.$3, - )); - final waitingInvitationsBlocMapCubitUpdater = - BlocUpdater( - create: (params) => WaitingInvitationsBlocMapCubit( - locator: params, + accountInfo: params.$1, + contactListRecordPointer: params.$2, )); + final waitingInvitationsBlocMapCubitUpdater = BlocUpdater< + WaitingInvitationsBlocMapCubit, + (AccountInfo, AccountRecordCubit, ContactInvitationListCubit)>( + create: (params) => WaitingInvitationsBlocMapCubit( + accountInfo: params.$1, + accountRecordCubit: params.$2, + contactInvitationListCubit: params.$3)); final activeChatCubitUpdater = BlocUpdater(create: (_) => ActiveChatCubit(null)); - final chatListCubitUpdater = - BlocUpdater( - create: (params) => ChatListCubit( - locator: params.$1, - accountRecordKey: params.$2, - chatListRecordPointer: params.$3, - )); + final chatListCubitUpdater = BlocUpdater( + create: (params) => ChatListCubit( + accountInfo: params.$1, + chatListRecordPointer: params.$2, + activeChatCubit: params.$3)); + final activeConversationsBlocMapCubitUpdater = BlocUpdater< + ActiveConversationsBlocMapCubit, + (AccountInfo, AccountRecordCubit, ChatListCubit, ContactListCubit)>( + create: (params) => ActiveConversationsBlocMapCubit( + accountInfo: params.$1, + accountRecordCubit: params.$2, + chatListCubit: params.$3, + contactListCubit: params.$4)); + final activeSingleContactChatBlocMapCubitUpdater = BlocUpdater< + ActiveSingleContactChatBlocMapCubit, + ( + AccountInfo, + ActiveConversationsBlocMapCubit, + ChatListCubit, + ContactListCubit + )>( + create: (params) => ActiveSingleContactChatBlocMapCubit( + accountInfo: params.$1, + activeConversationsBlocMapCubit: params.$2, + chatListCubit: params.$3, + contactListCubit: params.$4, + )); } diff --git a/lib/account_manager/models/account_info.dart b/lib/account_manager/models/account_info.dart index a2a9a39..12ed5e1 100644 --- a/lib/account_manager/models/account_info.dart +++ b/lib/account_manager/models/account_info.dart @@ -16,20 +16,17 @@ enum AccountInfoStatus { class AccountInfo extends Equatable { const AccountInfo({ required this.status, - required this.active, required this.localAccount, required this.userLogin, }); final AccountInfoStatus status; - final bool active; final LocalAccount localAccount; final UserLogin? userLogin; @override List get props => [ status, - active, localAccount, userLogin, ]; diff --git a/lib/account_manager/repository/account_repository.dart b/lib/account_manager/repository/account_repository.dart index 73ece3e..fd15b43 100644 --- a/lib/account_manager/repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository.dart @@ -93,10 +93,6 @@ class AccountRepository { } AccountInfo? getAccountInfo(TypedKey superIdentityRecordKey) { - // Get active account if we have one - final activeLocalAccount = getActiveLocalAccount(); - final active = superIdentityRecordKey == activeLocalAccount; - // Get which local account we want to fetch the profile for final localAccount = fetchLocalAccount(superIdentityRecordKey); if (localAccount == null) { @@ -109,7 +105,6 @@ class AccountRepository { // Account was locked return AccountInfo( status: AccountInfoStatus.accountLocked, - active: active, localAccount: localAccount, userLogin: null, ); @@ -118,7 +113,6 @@ class AccountRepository { // Got account, decrypted and decoded return AccountInfo( status: AccountInfoStatus.accountUnlocked, - active: active, localAccount: localAccount, userLogin: userLogin, ); diff --git a/lib/account_manager/views/edit_account_page.dart b/lib/account_manager/views/edit_account_page.dart index 19b7acf..986fd0b 100644 --- a/lib/account_manager/views/edit_account_page.dart +++ b/lib/account_manager/views/edit_account_page.dart @@ -116,13 +116,13 @@ class _EditAccountPageState extends State { }); try { // Look up account cubit for this specific account - final accountRecordsCubit = - context.read(); - await accountRecordsCubit.operateAsync( + final perAccountCollectionCubit = + context.read(); + await perAccountCollectionCubit.operateAsync( widget.superIdentityRecordKey, closure: (c) async { // Update account profile DHT record // This triggers ConversationCubits to update - await c.updateProfile(newProfile); + await c.accountRecordCubit!.updateProfile(newProfile); }); // Update local account profile diff --git a/lib/app.dart b/lib/app.dart index 522dbc2..01d2a6d 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -131,9 +131,10 @@ class VeilidChatApp extends StatelessWidget { create: (context) => PreferencesCubit(PreferencesRepository.instance), ), - BlocProvider( - create: (context) => AccountRecordsBlocMapCubit( - AccountRepository.instance, context.read)), + BlocProvider( + create: (context) => PerAccountCollectionBlocMapCubit( + accountRepository: AccountRepository.instance, + locator: context.read)), ], child: BackgroundTicker( child: _buildShortcuts( diff --git a/lib/chat/cubits/chat_component_cubit.dart b/lib/chat/cubits/chat_component_cubit.dart index b3e609f..05c722c 100644 --- a/lib/chat/cubits/chat_component_cubit.dart +++ b/lib/chat/cubits/chat_component_cubit.dart @@ -8,7 +8,6 @@ import 'package:flutter/widgets.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:provider/provider.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:veilid_support/veilid_support.dart'; @@ -27,10 +26,12 @@ const metadataKeyAttachments = 'attachments'; class ChatComponentCubit extends Cubit { ChatComponentCubit._({ - required Locator locator, + required AccountInfo accountInfo, + required AccountRecordCubit accountRecordCubit, required List conversationCubits, required SingleContactMessagesCubit messagesCubit, - }) : _locator = locator, + }) : _accountInfo = accountInfo, + _accountRecordCubit = accountRecordCubit, _conversationCubits = conversationCubits, _messagesCubit = messagesCubit, super(ChatComponentState( @@ -48,26 +49,25 @@ class ChatComponentCubit extends Cubit { } factory ChatComponentCubit.singleContact( - {required Locator locator, + {required AccountInfo accountInfo, + required AccountRecordCubit accountRecordCubit, required ActiveConversationCubit activeConversationCubit, required SingleContactMessagesCubit messagesCubit}) => ChatComponentCubit._( - locator: locator, + accountInfo: accountInfo, + accountRecordCubit: accountRecordCubit, conversationCubits: [activeConversationCubit], messagesCubit: messagesCubit, ); Future _init() async { // Get local user info and account record cubit - final unlockedAccountInfo = - _locator().state.unlockedAccountInfo!; - _localUserIdentityKey = unlockedAccountInfo.identityTypedPublicKey; - _localUserAccountRecordCubit = _locator(); + _localUserIdentityKey = _accountInfo.identityTypedPublicKey; // Subscribe to local user info - _localUserAccountRecordSubscription = _localUserAccountRecordCubit.stream - .listen(_onChangedLocalUserAccountRecord); - _onChangedLocalUserAccountRecord(_localUserAccountRecordCubit.state); + _accountRecordSubscription = + _accountRecordCubit.stream.listen(_onChangedAccountRecord); + _onChangedAccountRecord(_accountRecordCubit.state); // Subscribe to remote user info await _updateConversationSubscriptions(); @@ -80,7 +80,7 @@ class ChatComponentCubit extends Cubit { @override Future close() async { await _initWait(); - await _localUserAccountRecordSubscription.cancel(); + await _accountRecordSubscription.cancel(); await _messagesSubscription.cancel(); await super.close(); } @@ -145,7 +145,7 @@ class ChatComponentCubit extends Cubit { //////////////////////////////////////////////////////////////////////////// // Private Implementation - void _onChangedLocalUserAccountRecord(AsyncValue avAccount) { + void _onChangedAccountRecord(AsyncValue avAccount) { final account = avAccount.asData?.value; if (account == null) { emit(state.copyWith(localUser: null)); @@ -374,15 +374,14 @@ class ChatComponentCubit extends Cubit { //////////////////////////////////////////////////////////////////////////// final _initWait = WaitSet(); - - final Locator _locator; + final AccountInfo _accountInfo; + final AccountRecordCubit _accountRecordCubit; final List _conversationCubits; final SingleContactMessagesCubit _messagesCubit; late final TypedKey _localUserIdentityKey; - late final AccountRecordCubit _localUserAccountRecordCubit; late final StreamSubscription> - _localUserAccountRecordSubscription; + _accountRecordSubscription; final Map>> _conversationSubscriptions = {}; late StreamSubscription _messagesSubscription; diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index a2fcc8d..397ac39 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -4,7 +4,6 @@ import 'package:async_tools/async_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/foundation.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'; @@ -51,13 +50,13 @@ typedef SingleContactMessagesState = AsyncValue>; // Builds the reconciled chat record from the local and remote conversation keys class SingleContactMessagesCubit extends Cubit { SingleContactMessagesCubit({ - required Locator locator, + required AccountInfo accountInfo, required TypedKey remoteIdentityPublicKey, required TypedKey localConversationRecordKey, required TypedKey localMessagesRecordKey, required TypedKey remoteConversationRecordKey, required TypedKey remoteMessagesRecordKey, - }) : _locator = locator, + }) : _accountInfo = accountInfo, _remoteIdentityPublicKey = remoteIdentityPublicKey, _localConversationRecordKey = localConversationRecordKey, _localMessagesRecordKey = localMessagesRecordKey, @@ -87,9 +86,6 @@ class SingleContactMessagesCubit extends Cubit { // Initialize everything Future _init() async { - _unlockedAccountInfo = - _locator().state.unlockedAccountInfo!; - _unsentMessagesQueue = PersistentQueue( table: 'SingleContactUnsentMessages', key: _remoteConversationRecordKey.toString(), @@ -115,15 +111,15 @@ class SingleContactMessagesCubit extends Cubit { // Make crypto Future _initCrypto() async { - _conversationCrypto = await _unlockedAccountInfo - .makeConversationCrypto(_remoteIdentityPublicKey); + _conversationCrypto = + await _accountInfo.makeConversationCrypto(_remoteIdentityPublicKey); _senderMessageIntegrity = await MessageIntegrity.create( - author: _unlockedAccountInfo.identityTypedPublicKey); + author: _accountInfo.identityTypedPublicKey); } // Open local messages key Future _initSentMessagesCubit() async { - final writer = _unlockedAccountInfo.identityWriter; + final writer = _accountInfo.identityWriter; _sentMessagesCubit = DHTLogCubit( open: () async => DHTLog.openWrite(_localMessagesRecordKey, writer, @@ -153,7 +149,7 @@ class SingleContactMessagesCubit extends Cubit { Future _makeLocalMessagesCrypto() async => VeilidCryptoPrivate.fromTypedKey( - _unlockedAccountInfo.userLogin.identitySecret, 'tabledb'); + _accountInfo.userLogin!.identitySecret, 'tabledb'); // Open reconciled chat record key Future _initReconciledMessagesCubit() async { @@ -245,9 +241,7 @@ class SingleContactMessagesCubit extends Cubit { } _reconciliation.reconcileMessages( - _unlockedAccountInfo.identityTypedPublicKey, - sentMessages, - _sentMessagesCubit!); + _accountInfo.identityTypedPublicKey, sentMessages, _sentMessagesCubit!); // Update the view _renderState(); @@ -284,7 +278,7 @@ class SingleContactMessagesCubit extends Cubit { // Now sign it await _senderMessageIntegrity.signMessage( - message, _unlockedAccountInfo.identitySecretKey); + message, _accountInfo.identitySecretKey); } // Async process to send messages in the background @@ -336,8 +330,8 @@ class SingleContactMessagesCubit extends Cubit { final renderedElements = []; for (final m in reconciledMessages.windowElements) { - final isLocal = m.content.author.toVeilid() == - _unlockedAccountInfo.identityTypedPublicKey; + final isLocal = + m.content.author.toVeilid() == _accountInfo.identityTypedPublicKey; final reconciledTimestamp = Timestamp.fromInt64(m.reconciledTime); final sm = isLocal ? sentMessagesMap[m.content.authorUniqueIdString] : null; @@ -375,7 +369,7 @@ class SingleContactMessagesCubit extends Cubit { // Add common fields // id and signature will get set by _processMessageToSend message - ..author = _unlockedAccountInfo.identityTypedPublicKey.toProto() + ..author = _accountInfo.identityTypedPublicKey.toProto() ..timestamp = Veilid.instance.now().toInt64(); // Put in the queue @@ -408,8 +402,7 @@ class SingleContactMessagesCubit extends Cubit { ///////////////////////////////////////////////////////////////////////// final WaitSet _initWait = WaitSet(); - final Locator _locator; - late final UnlockedAccountInfo _unlockedAccountInfo; + late final AccountInfo _accountInfo; final TypedKey _remoteIdentityPublicKey; final TypedKey _localConversationRecordKey; final TypedKey _localMessagesRecordKey; diff --git a/lib/chat/views/chat_component_widget.dart b/lib/chat/views/chat_component_widget.dart index 92ec587..5035969 100644 --- a/lib/chat/views/chat_component_widget.dart +++ b/lib/chat/views/chat_component_widget.dart @@ -8,6 +8,7 @@ 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 '../../conversation/conversation.dart'; import '../../theme/theme.dart'; import '../chat.dart'; @@ -21,6 +22,12 @@ class ChatComponentWidget extends StatelessWidget { static Widget builder( {required TypedKey localConversationRecordKey, Key? key}) => Builder(builder: (context) { + // Get the account info + final accountInfo = context.watch().state; + + // Get the account record cubit + final accountRecordCubit = context.read(); + // Get the active conversation cubit final activeConversationCubit = context .select( @@ -43,7 +50,8 @@ class ChatComponentWidget extends StatelessWidget { // Make chat component state return BlocProvider( create: (context) => ChatComponentCubit.singleContact( - locator: context.read, + accountInfo: accountInfo, + accountRecordCubit: accountRecordCubit, activeConversationCubit: activeConversationCubit, messagesCubit: messagesCubit, ), diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index 697297d..1b593c8 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -3,9 +3,9 @@ import 'dart:async'; import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fixnum/fixnum.dart'; -import 'package:provider/provider.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'; @@ -19,18 +19,19 @@ typedef ChatListCubitState = DHTShortArrayBusyState; class ChatListCubit extends DHTShortArrayCubit with StateMapFollowable { ChatListCubit({ - required Locator locator, - required TypedKey accountRecordKey, + required AccountInfo accountInfo, required OwnedDHTRecordPointer chatListRecordPointer, - }) : _locator = locator, + required ActiveChatCubit activeChatCubit, + }) : _activeChatCubit = activeChatCubit, super( - open: () => _open(locator, accountRecordKey, chatListRecordPointer), + open: () => _open(accountInfo, chatListRecordPointer), decodeElement: proto.Chat.fromBuffer); - static Future _open(Locator locator, TypedKey accountRecordKey, + static Future _open(AccountInfo accountInfo, OwnedDHTRecordPointer chatListRecordPointer) async { final dhtRecord = await DHTShortArray.openOwned(chatListRecordPointer, - debugName: 'ChatListCubit::_open::ChatList', parent: accountRecordKey); + debugName: 'ChatListCubit::_open::ChatList', + parent: accountInfo.accountRecordKey); return dhtRecord; } @@ -95,9 +96,8 @@ class ChatListCubit extends DHTShortArrayCubit final deletedItem = // Ensure followers get their changes before we return await syncFollowers(() => operateWrite((writer) async { - final activeChatCubit = _locator(); - if (activeChatCubit.state == localConversationRecordKey) { - activeChatCubit.setActiveChat(null); + if (_activeChatCubit.state == localConversationRecordKey) { + _activeChatCubit.setActiveChat(null); } for (var i = 0; i < writer.length; i++) { final c = await writer.getProtobuf(proto.Chat.fromBuffer, i); @@ -139,5 +139,5 @@ class ChatListCubit extends DHTShortArrayCubit //////////////////////////////////////////////////////////////////////////// - final Locator _locator; + 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 2cd0a49..6a850e4 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -4,7 +4,6 @@ import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; -import 'package:provider/provider.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; @@ -37,14 +36,12 @@ class ContactInvitationListCubit StateMapFollowable { ContactInvitationListCubit({ - required Locator locator, - required TypedKey accountRecordKey, + required AccountInfo accountInfo, required OwnedDHTRecordPointer contactInvitationListRecordPointer, - }) : _locator = locator, - _accountRecordKey = accountRecordKey, + }) : _accountInfo = accountInfo, super( - open: () => - _open(accountRecordKey, contactInvitationListRecordPointer), + open: () => _open(accountInfo.accountRecordKey, + contactInvitationListRecordPointer), decodeElement: proto.ContactInvitationRecord.fromBuffer); static Future _open(TypedKey accountRecordKey, @@ -58,7 +55,8 @@ class ContactInvitationListCubit } Future createInvitation( - {required EncryptionKeyType encryptionKeyType, + {required proto.Profile profile, + required EncryptionKeyType encryptionKeyType, required String encryptionKey, required String message, required Timestamp? expiration}) async { @@ -68,12 +66,8 @@ class ContactInvitationListCubit final crcs = await pool.veilid.bestCryptoSystem(); final contactRequestWriter = await crcs.generateKeyPair(); - final activeAccountInfo = - _locator().state.unlockedAccountInfo!; - final profile = _locator().state.asData!.value.profile; - - final idcs = await activeAccountInfo.identityCryptoSystem; - final identityWriter = activeAccountInfo.identityWriter; + final idcs = await _accountInfo.identityCryptoSystem; + final identityWriter = _accountInfo.identityWriter; // Encrypt the writer secret with the encryption key final encryptedSecret = await encryptionKeyType.encryptSecretToBytes( @@ -91,7 +85,7 @@ class ContactInvitationListCubit await (await pool.createRecord( debugName: 'ContactInvitationListCubit::createInvitation::' 'LocalConversation', - parent: _accountRecordKey, + parent: _accountInfo.accountRecordKey, schema: DHTSchema.smpl( oCnt: 0, members: [DHTSchemaMember(mKey: identityWriter.key, mCnt: 1)]))) @@ -101,8 +95,7 @@ class ContactInvitationListCubit final crpriv = proto.ContactRequestPrivate() ..writerKey = contactRequestWriter.key.toProto() ..profile = profile - ..superIdentityRecordKey = - activeAccountInfo.userLogin.superIdentityRecordKey.toProto() + ..superIdentityRecordKey = _accountInfo.superIdentityRecordKey.toProto() ..chatRecordKey = localConversation.key.toProto() ..expiration = expiration?.toInt64() ?? Int64.ZERO; final crprivbytes = crpriv.writeToBuffer(); @@ -120,7 +113,7 @@ class ContactInvitationListCubit await (await pool.createRecord( debugName: 'ContactInvitationListCubit::createInvitation::' 'ContactRequestInbox', - parent: _accountRecordKey, + parent: _accountInfo.accountRecordKey, schema: DHTSchema.smpl(oCnt: 1, members: [ DHTSchemaMember(mCnt: 1, mKey: contactRequestWriter.key) ]), @@ -197,7 +190,7 @@ class ContactInvitationListCubit await (await pool.openRecordOwned(contactRequestInbox, debugName: 'ContactInvitationListCubit::deleteInvitation::' 'ContactRequestInbox', - parent: _accountRecordKey)) + parent: _accountInfo.accountRecordKey)) .scope((contactRequestInbox) async { // Wipe out old invitation so it shows up as invalid await contactRequestInbox.tryWriteBytes(Uint8List(0)); @@ -248,7 +241,7 @@ class ContactInvitationListCubit debugName: 'ContactInvitationListCubit::validateInvitation::' 'ContactRequestInbox', parent: pool.getParentRecordKey(contactRequestInboxKey) ?? - _accountRecordKey)) + _accountInfo.accountRecordKey)) .maybeDeleteScope(!isSelf, (contactRequestInbox) async { // final contactRequest = await contactRequestInbox @@ -293,7 +286,7 @@ class ContactInvitationListCubit secret: writerSecret); out = ValidContactInvitation( - locator: _locator, + accountInfo: _accountInfo, contactRequestInboxKey: contactRequestInboxKey, contactRequestPrivate: contactRequestPrivate, contactSuperIdentity: contactSuperIdentity, @@ -317,6 +310,5 @@ class ContactInvitationListCubit } // - final Locator _locator; - final TypedKey _accountRecordKey; + final AccountInfo _accountInfo; } diff --git a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart index 64f9d45..714201b 100644 --- a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart +++ b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart @@ -1,4 +1,3 @@ -import 'package:provider/provider.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; @@ -8,23 +7,21 @@ import '../../proto/proto.dart' as proto; class ContactRequestInboxCubit extends DefaultDHTRecordCubit { ContactRequestInboxCubit( - {required Locator locator, required this.contactInvitationRecord}) + {required AccountInfo accountInfo, required this.contactInvitationRecord}) : super( open: () => _open( - locator: locator, + accountInfo: accountInfo, contactInvitationRecord: contactInvitationRecord), decodeState: (buf) => buf.isEmpty ? null : proto.SignedContactResponse.fromBuffer(buf)); static Future _open( - {required Locator locator, + {required AccountInfo accountInfo, required proto.ContactInvitationRecord contactInvitationRecord}) async { final pool = DHTRecordPool.instance; - final unlockedAccountInfo = - locator().state.unlockedAccountInfo!; - final accountRecordKey = unlockedAccountInfo.accountRecordKey; + final accountRecordKey = accountInfo.accountRecordKey; final writerSecret = contactInvitationRecord.writerSecret.toVeilid(); final recordKey = diff --git a/lib/contact_invitation/cubits/waiting_invitation_cubit.dart b/lib/contact_invitation/cubits/waiting_invitation_cubit.dart index 4930e4d..a955978 100644 --- a/lib/contact_invitation/cubits/waiting_invitation_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitation_cubit.dart @@ -4,9 +4,9 @@ import 'package:async_tools/async_tools.dart'; import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; -import 'package:provider/provider.dart'; import 'package:veilid_support/veilid_support.dart'; +import '../../account_manager/account_manager.dart'; import '../../conversation/conversation.dart'; import '../../proto/proto.dart' as proto; import '../../tools/tools.dart'; @@ -24,18 +24,22 @@ class InvitationStatus extends Equatable { class WaitingInvitationCubit extends AsyncTransformerCubit { - WaitingInvitationCubit(ContactRequestInboxCubit super.input, - {required Locator locator, - required proto.ContactInvitationRecord contactInvitationRecord}) - : super( + WaitingInvitationCubit( + ContactRequestInboxCubit super.input, { + required AccountInfo accountInfo, + required AccountRecordCubit accountRecordCubit, + required proto.ContactInvitationRecord contactInvitationRecord, + }) : super( transform: (signedContactResponse) => _transform( signedContactResponse, - locator: locator, + accountInfo: accountInfo, + accountRecordCubit: accountRecordCubit, contactInvitationRecord: contactInvitationRecord)); static Future> _transform( proto.SignedContactResponse? signedContactResponse, - {required Locator locator, + {required AccountInfo accountInfo, + required AccountRecordCubit accountRecordCubit, required proto.ContactInvitationRecord contactInvitationRecord}) async { if (signedContactResponse == null) { return const AsyncValue.loading(); @@ -69,7 +73,7 @@ class WaitingInvitationCubit extends AsyncTransformerCubit AsyncValue.data(InvitationStatus( acceptedContact: AcceptedContact( 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 3fa8388..ff7f701 100644 --- a/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart @@ -1,8 +1,8 @@ import 'package:async_tools/async_tools.dart'; import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; -import 'package:provider/provider.dart'; import 'package:veilid_support/veilid_support.dart'; +import '../../account_manager/account_manager.dart'; import '../../proto/proto.dart' as proto; import 'cubits.dart'; @@ -17,11 +17,14 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit, TypedKey, proto.ContactInvitationRecord> { - WaitingInvitationsBlocMapCubit({ - required Locator locator, - }) : _locator = locator { + WaitingInvitationsBlocMapCubit( + {required AccountInfo accountInfo, + required AccountRecordCubit accountRecordCubit, + required ContactInvitationListCubit contactInvitationListCubit}) + : _accountInfo = accountInfo, + _accountRecordCubit = accountRecordCubit { // Follow the contact invitation list cubit - follow(locator()); + follow(contactInvitationListCubit); } Future _addWaitingInvitation( @@ -31,9 +34,10 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit _contactRequestPrivate.profile; - Future accept() async { + Future accept(proto.Profile profile) async { final pool = DHTRecordPool.instance; try { - final unlockedAccountInfo = - _locator().state.unlockedAccountInfo!; - final accountRecordKey = unlockedAccountInfo.accountRecordKey; - final identityPublicKey = unlockedAccountInfo.identityPublicKey; - // 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 = - _contactSuperIdentity.currentInstance.publicKey == identityPublicKey; + final isSelf = _contactSuperIdentity.currentInstance.publicKey == + _accountInfo.identityPublicKey; return (await pool.openRecordWrite(_contactRequestInboxKey, _writer, debugName: 'ValidContactInvitation::accept::' 'ContactRequestInbox', parent: pool.getParentRecordKey(_contactRequestInboxKey) ?? - accountRecordKey)) + _accountInfo.accountRecordKey)) // ignore: prefer_expression_function_bodies .maybeDeleteScope(!isSelf, (contactRequestInbox) async { // Create local conversation key for this // contact and send via contact response final conversation = ConversationCubit( - locator: _locator, + accountInfo: _accountInfo, remoteIdentityPublicKey: _contactSuperIdentity.currentInstance.typedPublicKey); return conversation.initLocalConversation( + profile: profile, callback: (localConversation) async { - final contactResponse = proto.ContactResponse() - ..accept = true - ..remoteConversationRecordKey = localConversation.key.toProto() - ..superIdentityRecordKey = - unlockedAccountInfo.superIdentityRecordKey.toProto(); - final contactResponseBytes = contactResponse.writeToBuffer(); + final contactResponse = proto.ContactResponse() + ..accept = true + ..remoteConversationRecordKey = localConversation.key.toProto() + ..superIdentityRecordKey = + _accountInfo.superIdentityRecordKey.toProto(); + final contactResponseBytes = contactResponse.writeToBuffer(); - final cs = - await pool.veilid.getCryptoSystem(_contactRequestInboxKey.kind); + final cs = await _accountInfo.identityCryptoSystem; + final identitySignature = await cs.signWithKeyPair( + _accountInfo.identityWriter, contactResponseBytes); - final identitySignature = await cs.sign( - unlockedAccountInfo.identityWriter.key, - unlockedAccountInfo.identityWriter.secret, - contactResponseBytes); + final signedContactResponse = proto.SignedContactResponse() + ..contactResponse = contactResponseBytes + ..identitySignature = identitySignature.toProto(); - final signedContactResponse = proto.SignedContactResponse() - ..contactResponse = contactResponseBytes - ..identitySignature = identitySignature.toProto(); + // Write the acceptance to the inbox + await contactRequestInbox + .eventualWriteProtobuf(signedContactResponse, subkey: 1); - // Write the acceptance to the inbox - await contactRequestInbox.eventualWriteProtobuf(signedContactResponse, - subkey: 1); - - return AcceptedContact( - remoteProfile: _contactRequestPrivate.profile, - remoteIdentity: _contactSuperIdentity, - remoteConversationRecordKey: - _contactRequestPrivate.chatRecordKey.toVeilid(), - localConversationRecordKey: localConversation.key, - ); - }); + return AcceptedContact( + remoteProfile: _contactRequestPrivate.profile, + remoteIdentity: _contactSuperIdentity, + remoteConversationRecordKey: + _contactRequestPrivate.chatRecordKey.toVeilid(), + localConversationRecordKey: localConversation.key, + ); + }); }); } on Exception catch (e) { log.debug('exception: $e', e); @@ -96,33 +87,24 @@ class ValidContactInvitation { Future reject() async { final pool = DHTRecordPool.instance; - final unlockedAccountInfo = - _locator().state.unlockedAccountInfo!; - final accountRecordKey = unlockedAccountInfo.accountRecordKey; - final identityPublicKey = unlockedAccountInfo.identityPublicKey; - // Ensure we don't delete this if we're trying to chat to self - final isSelf = - _contactSuperIdentity.currentInstance.publicKey == identityPublicKey; + final isSelf = _contactSuperIdentity.currentInstance.publicKey == + _accountInfo.identityPublicKey; return (await pool.openRecordWrite(_contactRequestInboxKey, _writer, debugName: 'ValidContactInvitation::reject::' 'ContactRequestInbox', - parent: accountRecordKey)) + parent: _accountInfo.accountRecordKey)) .maybeDeleteScope(!isSelf, (contactRequestInbox) async { - final cs = - await pool.veilid.getCryptoSystem(_contactRequestInboxKey.kind); - final contactResponse = proto.ContactResponse() ..accept = false ..superIdentityRecordKey = - unlockedAccountInfo.superIdentityRecordKey.toProto(); + _accountInfo.superIdentityRecordKey.toProto(); final contactResponseBytes = contactResponse.writeToBuffer(); - final identitySignature = await cs.sign( - unlockedAccountInfo.identityWriter.key, - unlockedAccountInfo.identityWriter.secret, - contactResponseBytes); + final cs = await _accountInfo.identityCryptoSystem; + final identitySignature = await cs.signWithKeyPair( + _accountInfo.identityWriter, contactResponseBytes); final signedContactResponse = proto.SignedContactResponse() ..contactResponse = contactResponseBytes @@ -136,7 +118,7 @@ class ValidContactInvitation { } // - final Locator _locator; + final AccountInfo _accountInfo; final TypedKey _contactRequestInboxKey; final SuperIdentity _contactSuperIdentity; final KeyPair _writer; diff --git a/lib/contact_invitation/views/create_invitation_dialog.dart b/lib/contact_invitation/views/create_invitation_dialog.dart index ace71d5..93b5796 100644 --- a/lib/contact_invitation/views/create_invitation_dialog.dart +++ b/lib/contact_invitation/views/create_invitation_dialog.dart @@ -140,8 +140,18 @@ class CreateInvitationDialogState extends State { // Start generation final contactInvitationListCubit = widget.modalContext.read(); + final profile = widget.modalContext + .read() + .state + .asData + ?.value + .profile; + if (profile == null) { + return; + } final generator = contactInvitationListCubit.createInvitation( + profile: profile, encryptionKeyType: _encryptionKeyType, encryptionKey: _encryptionKey, message: _messageTextController.text, diff --git a/lib/contact_invitation/views/invitation_dialog.dart b/lib/contact_invitation/views/invitation_dialog.dart index e619360..cd962ab 100644 --- a/lib/contact_invitation/views/invitation_dialog.dart +++ b/lib/contact_invitation/views/invitation_dialog.dart @@ -76,17 +76,19 @@ class InvitationDialogState extends State { final navigator = Navigator.of(context); final accountInfo = widget._locator().state; final contactList = widget._locator(); + final profile = + widget._locator().state.asData!.value.profile; setState(() { _isAccepting = true; }); final validInvitation = _validInvitation; if (validInvitation != null) { - final acceptedContact = await validInvitation.accept(); + final acceptedContact = await validInvitation.accept(profile); if (acceptedContact != null) { // initiator when accept is received will create // contact in the case of a 'note to self' - final isSelf = accountInfo.unlockedAccountInfo!.identityPublicKey == + final isSelf = accountInfo.identityPublicKey == acceptedContact.remoteIdentity.currentInstance.publicKey; if (!isSelf) { await contactList.createContact( diff --git a/lib/contacts/cubits/contact_list_cubit.dart b/lib/contacts/cubits/contact_list_cubit.dart index 3b66815..47433c7 100644 --- a/lib/contacts/cubits/contact_list_cubit.dart +++ b/lib/contacts/cubits/contact_list_cubit.dart @@ -3,9 +3,9 @@ import 'dart:convert'; import 'package:async_tools/async_tools.dart'; import 'package:protobuf/protobuf.dart'; -import 'package:provider/provider.dart'; import 'package:veilid_support/veilid_support.dart'; +import '../../account_manager/account_manager.dart'; import '../../conversation/conversation.dart'; import '../../proto/proto.dart' as proto; import '../../tools/tools.dart'; @@ -15,12 +15,12 @@ import '../../tools/tools.dart'; class ContactListCubit extends DHTShortArrayCubit { ContactListCubit({ - required Locator locator, - required TypedKey accountRecordKey, + required AccountInfo accountInfo, required OwnedDHTRecordPointer contactListRecordPointer, - }) : _locator = locator, + }) : _accountInfo = accountInfo, super( - open: () => _open(accountRecordKey, contactListRecordPointer), + open: () => + _open(accountInfo.accountRecordKey, contactListRecordPointer), decodeElement: proto.Contact.fromBuffer); static Future _open(TypedKey accountRecordKey, @@ -126,7 +126,7 @@ class ContactListCubit extends DHTShortArrayCubit { try { // Make a conversation cubit to manipulate the conversation final conversationCubit = ConversationCubit( - locator: _locator, + accountInfo: _accountInfo, remoteIdentityPublicKey: deletedItem.identityPublicKey.toVeilid(), localConversationRecordKey: deletedItem.localConversationRecordKey.toVeilid(), @@ -144,5 +144,5 @@ class ContactListCubit extends DHTShortArrayCubit { final _contactProfileUpdateMap = SingleStateProcessorMap(); - final Locator _locator; + final AccountInfo _accountInfo; } diff --git a/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart b/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart index aa93bfc..ae527e4 100644 --- a/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart +++ b/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart @@ -2,7 +2,6 @@ import 'package:async_tools/async_tools.dart'; import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; -import 'package:provider/provider.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; @@ -45,10 +44,15 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit, ActiveConversationCubit> with StateMapFollower { ActiveConversationsBlocMapCubit({ - required Locator locator, - }) : _locator = locator { + required AccountInfo accountInfo, + required AccountRecordCubit accountRecordCubit, + required ChatListCubit chatListCubit, + required ContactListCubit contactListCubit, + }) : _accountInfo = accountInfo, + _accountRecordCubit = accountRecordCubit, + _contactListCubit = contactListCubit { // Follow the chat list cubit - follow(locator()); + follow(chatListCubit); } //////////////////////////////////////////////////////////////////////////// @@ -69,20 +73,15 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit(); - conversationCubit.watchAccountChanges( - accountRecordCubit.stream, accountRecordCubit.state); - // When remote conversation changes its profile, // update our local contact - _locator().followContactProfileChanges( + _contactListCubit.followContactProfileChanges( localConversationRecordKey, conversationCubit.stream.map((x) => x.map( data: (d) => d.value.remoteConversation?.profile, @@ -90,6 +89,10 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit null)), conversationCubit.state.asData?.value.remoteConversation?.profile); + // When our local account profile changes, send it to the conversation + conversationCubit.watchAccountChanges( + _accountRecordCubit.stream, _accountRecordCubit.state); + // Transformer that only passes through completed/active conversations // along with the contact that corresponds to the completed // conversation @@ -119,7 +122,7 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit updateState(TypedKey key, proto.Chat value) async { - final contactList = _locator().state.state.asData?.value; + final contactList = _contactListCubit.state.state.asData?.value; if (contactList == null) { await addState(key, const AsyncValue.loading()); return; @@ -136,5 +139,7 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit> { - ActiveSingleContactChatBlocMapCubit({required Locator locator}) - : _locator = locator { + ActiveSingleContactChatBlocMapCubit( + {required AccountInfo accountInfo, + required ActiveConversationsBlocMapCubit activeConversationsBlocMapCubit, + required ContactListCubit contactListCubit, + required ChatListCubit chatListCubit}) + : _accountInfo = accountInfo, + _contactListCubit = contactListCubit, + _chatListCubit = chatListCubit { // Follow the active conversations bloc map cubit - follow(locator()); + follow(activeConversationsBlocMapCubit); } Future _addConversationMessages( @@ -33,7 +40,7 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit MapEntry( contact.localConversationRecordKey.toVeilid(), SingleContactMessagesCubit( - locator: _locator, + accountInfo: _accountInfo, remoteIdentityPublicKey: contact.identityPublicKey.toVeilid(), localConversationRecordKey: contact.localConversationRecordKey.toVeilid(), @@ -52,7 +59,7 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit updateState( TypedKey key, AsyncValue value) async { // Get the contact object for this single contact chat - final contactList = _locator().state.state.asData?.value; + final contactList = _contactListCubit.state.state.asData?.value; if (contactList == null) { await addState(key, const AsyncValue.loading()); return; @@ -67,7 +74,7 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit().state.state.asData?.value; + final chatList = _chatListCubit.state.state.asData?.value; if (chatList == null) { await addState(key, const AsyncValue.loading()); return; @@ -92,6 +99,7 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit> { ConversationCubit( - {required Locator locator, + {required AccountInfo accountInfo, required TypedKey remoteIdentityPublicKey, TypedKey? localConversationRecordKey, TypedKey? remoteConversationRecordKey}) - : _locator = locator, + : _accountInfo = accountInfo, _localConversationRecordKey = localConversationRecordKey, _remoteIdentityPublicKey = remoteIdentityPublicKey, _remoteConversationRecordKey = remoteConversationRecordKey, super(const AsyncValue.loading()) { - final unlockedAccountInfo = - _locator().state.unlockedAccountInfo!; - _accountRecordKey = unlockedAccountInfo.accountRecordKey; - _identityWriter = unlockedAccountInfo.identityWriter; + _identityWriter = _accountInfo.identityWriter; if (_localConversationRecordKey != null) { _initWait.add(() async { @@ -60,7 +56,7 @@ class ConversationCubit extends Cubit> { final record = await pool.openRecordWrite( _localConversationRecordKey!, writer, debugName: 'ConversationCubit::LocalConversation', - parent: _accountRecordKey, + parent: accountInfo.accountRecordKey, crypto: crypto); return record; @@ -77,7 +73,7 @@ class ConversationCubit extends Cubit> { final record = await pool.openRecordRead(_remoteConversationRecordKey, debugName: 'ConversationCubit::RemoteConversation', parent: pool.getParentRecordKey(_remoteConversationRecordKey) ?? - _accountRecordKey, + accountInfo.accountRecordKey, crypto: crypto); return record; }); @@ -108,7 +104,8 @@ class ConversationCubit extends Cubit> { /// The callback allows for more initialization to occur and for /// cleanup to delete records upon failure of the callback Future initLocalConversation( - {required FutureOr Function(DHTRecord) callback, + {required proto.Profile profile, + required FutureOr Function(DHTRecord) callback, TypedKey? existingConversationRecordKey}) async { assert(_localConversationRecordKey == null, 'must not have a local conversation yet'); @@ -116,11 +113,8 @@ class ConversationCubit extends Cubit> { final pool = DHTRecordPool.instance; final crypto = await _cachedConversationCrypto(); - final account = _locator().state.asData!.value; - final unlockedAccountInfo = - _locator().state.unlockedAccountInfo!; - final accountRecordKey = unlockedAccountInfo.accountRecordKey; - final writer = unlockedAccountInfo.identityWriter; + final accountRecordKey = _accountInfo.accountRecordKey; + final writer = _accountInfo.identityWriter; // Open with SMPL scheme for identity writer late final DHTRecord localConversationRecord; @@ -150,9 +144,9 @@ class ConversationCubit extends Cubit> { callback: (messages) async { // Create initial local conversation key contents final conversation = proto.Conversation() - ..profile = account.profile - ..superIdentityJson = jsonEncode( - unlockedAccountInfo.localAccount.superIdentity.toJson()) + ..profile = profile + ..superIdentityJson = + jsonEncode(_accountInfo.localAccount.superIdentity.toJson()) ..messages = messages.recordKey.toProto(); // Write initial conversation to record @@ -359,10 +353,8 @@ class ConversationCubit extends Cubit> { if (conversationCrypto != null) { return conversationCrypto; } - final unlockedAccountInfo = - _locator().state.unlockedAccountInfo!; - conversationCrypto = await unlockedAccountInfo - .makeConversationCrypto(_remoteIdentityPublicKey); + conversationCrypto = + await _accountInfo.makeConversationCrypto(_remoteIdentityPublicKey); _conversationCrypto = conversationCrypto; return conversationCrypto; } @@ -371,8 +363,7 @@ class ConversationCubit extends Cubit> { // Fields TypedKey get remoteIdentityPublicKey => _remoteIdentityPublicKey; - final Locator _locator; - late final TypedKey _accountRecordKey; + final AccountInfo _accountInfo; late final KeyPair _identityWriter; final TypedKey _remoteIdentityPublicKey; TypedKey? _localConversationRecordKey; diff --git a/lib/layout/home/drawer_menu/drawer_menu.dart b/lib/layout/home/drawer_menu/drawer_menu.dart index ea5b722..f68a277 100644 --- a/lib/layout/home/drawer_menu/drawer_menu.dart +++ b/lib/layout/home/drawer_menu/drawer_menu.dart @@ -233,7 +233,8 @@ class _DrawerMenuState extends State { final scale = theme.extension()!; //final textTheme = theme.textTheme; final localAccounts = context.watch().state; - final accountRecords = context.watch().state; + final accountRecords = + context.watch().state; final activeLocalAccount = context.watch().state; final gradient = LinearGradient( begin: Alignment.topLeft, diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index d064ad7..ddbb99e 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -164,45 +164,44 @@ class HomeScreenState extends State { ], child: Builder(builder: _buildAccountReadyDeviceSpecific))); } - Widget _buildAccount(BuildContext context, TypedKey superIdentityRecordKey) => - BlocProvider( + Widget _buildAccount(BuildContext context, TypedKey superIdentityRecordKey, + PerAccountCollectionCubit perAccountCollectionCubit) => + BlocBuilder( key: ValueKey(superIdentityRecordKey), - create: (context) => AccountInfoCubit( - AccountRepository.instance, superIdentityRecordKey), - child: Builder(builder: (context) { - // Get active account info status - final accountInfoStatus = - context.select( - (c) => c.state.status); + bloc: perAccountCollectionCubit, + builder: (context, state) { - switch (accountInfoStatus) { - case AccountInfoStatus.noAccount: - return const HomeAccountMissing(); - case AccountInfoStatus.accountInvalid: - return const HomeAccountInvalid(); - case AccountInfoStatus.accountLocked: - return const HomeAccountLocked(); - case AccountInfoStatus.accountUnlocked: - // Get the current active account record cubit - final activeAccountRecordCubit = context - .select( - (c) => c.tryOperate(superIdentityRecordKey, - closure: (x) => x)); - if (activeAccountRecordCubit == null) { - return waitingPage(); - } + switch (state.accountInfo.status) { + case AccountInfoStatus.accountInvalid: + return const HomeAccountInvalid(); + case AccountInfoStatus.accountLocked: + return const HomeAccountLocked(); + case AccountInfoStatus.accountUnlocked: - return MultiBlocProvider(providers: [ - BlocProvider.value( - value: activeAccountRecordCubit), - ], child: Builder(builder: _buildUnlockedAccount)); - } - })); + // Get the current active account record cubit + final activeAccountRecordCubit = context.select< + PerAccountCollectionBlocMapCubit, + AccountRecordCubit?>( + (c) => c.tryOperate(superIdentityRecordKey, + closure: (x) => x)); + if (activeAccountRecordCubit == null) { + return waitingPage(); + } + + return MultiBlocProvider(providers: [ + BlocProvider.value( + value: activeAccountRecordCubit), + ], child: Builder(builder: _buildUnlockedAccount)); + } + }); + }; Widget _buildAccountPageView(BuildContext context) { final localAccounts = context.watch().state; - final activeLocalAccountCubit = context.read(); + final activeLocalAccountCubit = context.watch(); + final perAccountCollectionBlocMapCubit = + context.watch(); final activeIndex = localAccounts.indexWhere( (x) => x.superIdentity.recordKey == activeLocalAccountCubit.state); @@ -218,7 +217,7 @@ class HomeScreenState extends State { value.dispose(); }, child: Builder( - builder: (context) => PageView.custom( + builder: (context) => PageView.builder( onPageChanged: (idx) { singleFuture(this, () async { await AccountRepository.instance.switchToAccount( @@ -228,10 +227,21 @@ class HomeScreenState extends State { controller: context .read() .pageController, - childrenDelegate: SliverChildListDelegate(localAccounts - .map((la) => - _buildAccount(context, la.superIdentity.recordKey)) - .toList())))); + itemCount: localAccounts.length, + itemBuilder: (context, index) { + final superIdentityRecordKey = + localAccounts[index].superIdentity.recordKey; + final perAccountCollectionCubit = + perAccountCollectionBlocMapCubit.tryOperate( + superIdentityRecordKey, + closure: (c) => c); + if (perAccountCollectionCubit == null) { + return HomeAccountMissing( + key: ValueKey(superIdentityRecordKey)); + } + return _buildAccount(context, superIdentityRecordKey, + perAccountCollectionCubit); + }))); } @override From aea196deb34239f9f64c37d65c459d90f806b933 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 19 Jun 2024 11:35:51 -0400 Subject: [PATCH 144/270] checkpoint --- ...per_account_collection_bloc_map_cubit.dart | 5 +- .../cubits/per_account_collection_cubit.dart | 77 ++++-- .../per_account_collection_state.dart | 52 +++- .../per_account_collection_state.freezed.dart | 256 ++++++++++++++++-- .../views/edit_account_page.dart | 13 +- lib/layout/home/drawer_menu/drawer_menu.dart | 15 +- lib/layout/home/home_screen.dart | 130 ++------- 7 files changed, 371 insertions(+), 177 deletions(-) diff --git a/lib/account_manager/cubits/per_account_collection_bloc_map_cubit.dart b/lib/account_manager/cubits/per_account_collection_bloc_map_cubit.dart index 8810376..f1df6e6 100644 --- a/lib/account_manager/cubits/per_account_collection_bloc_map_cubit.dart +++ b/lib/account_manager/cubits/per_account_collection_bloc_map_cubit.dart @@ -1,12 +1,11 @@ -import 'package:async_tools/async_tools.dart'; import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:provider/provider.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; -typedef AccountRecordsBlocMapState - = BlocMapState>; +typedef PerAccountCollectionBlocMapState + = BlocMapState; /// Map of the logged in user accounts to their PerAccountCollectionCubit /// Ensures there is an single account record cubit for each logged in account diff --git a/lib/account_manager/cubits/per_account_collection_cubit.dart b/lib/account_manager/cubits/per_account_collection_cubit.dart index b67e234..0678f4b 100644 --- a/lib/account_manager/cubits/per_account_collection_cubit.dart +++ b/lib/account_manager/cubits/per_account_collection_cubit.dart @@ -55,11 +55,26 @@ class PerAccountCollectionCubit extends Cubit { PerAccountCollectionState( accountInfo: accountInfoCubit.state, avAccountRecordState: const AsyncValue.loading(), - contactInvitationListCubit: null); + contactInvitationListCubit: null, + accountInfoCubit: null, + accountRecordCubit: null, + contactListCubit: null, + waitingInvitationsBlocMapCubit: null, + activeChatCubit: null, + chatListCubit: null, + activeConversationsBlocMapCubit: null, + activeSingleContactChatBlocMapCubit: null); Future _followAccountInfoState(AccountInfo accountInfo) async { + // If state hasn't changed just return + if (state.accountInfo == accountInfo) { + return; + } + + // Get the next state var nextState = state.copyWith(accountInfo: accountInfo); + // Update AccountRecordCubit if (accountInfo.userLogin == null) { /////////////// Not logged in ///////////////// @@ -67,7 +82,7 @@ class PerAccountCollectionCubit extends Cubit { await _accountRecordSubscription?.cancel(); _accountRecordSubscription = null; - // Update state + // Update state to 'loading' nextState = _updateAccountRecordState(nextState, const AsyncValue.loading()); emit(nextState); @@ -78,12 +93,12 @@ class PerAccountCollectionCubit extends Cubit { } else { ///////////////// Logged in /////////////////// - // AccountRecordCubit + // Create AccountRecordCubit accountRecordCubit ??= AccountRecordCubit( localAccount: accountInfo.localAccount, userLogin: accountInfo.userLogin!); - // Update State + // Update state to value nextState = _updateAccountRecordState(nextState, accountRecordCubit!.state); emit(nextState); @@ -98,17 +113,17 @@ class PerAccountCollectionCubit extends Cubit { PerAccountCollectionState _updateAccountRecordState( PerAccountCollectionState prevState, - AsyncValue avAccountRecordState) { + AsyncValue? avAccountRecordState) { // Get next state final nextState = - state.copyWith(avAccountRecordState: accountRecordCubit!.state); + state.copyWith(avAccountRecordState: avAccountRecordState); // Get bloc parameters final accountInfo = nextState.accountInfo; // ContactInvitationListCubit final contactInvitationListRecordPointer = nextState - .avAccountRecordState.asData?.value.contactInvitationRecords + .avAccountRecordState?.asData?.value.contactInvitationRecords .toVeilid(); final contactInvitationListCubit = contactInvitationListCubitUpdater.update( @@ -118,7 +133,7 @@ class PerAccountCollectionCubit extends Cubit { // ContactListCubit final contactListRecordPointer = - nextState.avAccountRecordState.asData?.value.contactList.toVeilid(); + nextState.avAccountRecordState?.asData?.value.contactList.toVeilid(); final contactListCubit = contactListCubitUpdater.update( contactListRecordPointer == null @@ -126,18 +141,18 @@ class PerAccountCollectionCubit extends Cubit { : (accountInfo, contactListRecordPointer)); // WaitingInvitationsBlocMapCubit - waitingInvitationsBlocMapCubitUpdater.update( - contactInvitationListCubit == null + final waitingInvitationsBlocMapCubit = waitingInvitationsBlocMapCubitUpdater + .update(contactInvitationListCubit == null ? null : (accountInfo, accountRecordCubit!, contactInvitationListCubit)); // ActiveChatCubit - final activeChatCubit = activeChatCubitUpdater - .update(nextState.avAccountRecordState.isData ? true : null); + final activeChatCubit = activeChatCubitUpdater.update( + (nextState.avAccountRecordState?.isData ?? false) ? true : null); // ChatListCubit final chatListRecordPointer = - nextState.avAccountRecordState.asData?.value.chatList.toVeilid(); + nextState.avAccountRecordState?.asData?.value.chatList.toVeilid(); final chatListCubit = chatListCubitUpdater.update( chatListRecordPointer == null || activeChatCubit == null @@ -159,19 +174,31 @@ class PerAccountCollectionCubit extends Cubit { )); // ActiveSingleContactChatBlocMapCubit - activeSingleContactChatBlocMapCubitUpdater.update( - activeConversationsBlocMapCubit == null || - chatListCubit == null || - contactListCubit == null - ? null - : ( - accountInfo, - activeConversationsBlocMapCubit, - chatListCubit, - contactListCubit - )); + final activeSingleContactChatBlocMapCubit = + activeSingleContactChatBlocMapCubitUpdater.update( + activeConversationsBlocMapCubit == null || + chatListCubit == null || + contactListCubit == null + ? null + : ( + accountInfo, + activeConversationsBlocMapCubit, + chatListCubit, + contactListCubit + )); - return nextState; + // Update available blocs in our state + return nextState.copyWith( + contactInvitationListCubit: contactInvitationListCubit, + accountInfoCubit: accountInfoCubit, + accountRecordCubit: accountRecordCubit, + contactListCubit: contactListCubit, + waitingInvitationsBlocMapCubit: waitingInvitationsBlocMapCubit, + activeChatCubit: activeChatCubit, + chatListCubit: chatListCubit, + activeConversationsBlocMapCubit: activeConversationsBlocMapCubit, + activeSingleContactChatBlocMapCubit: + activeSingleContactChatBlocMapCubit); } T collectionLocator() { diff --git a/lib/account_manager/models/per_account_collection_state/per_account_collection_state.dart b/lib/account_manager/models/per_account_collection_state/per_account_collection_state.dart index 9794784..7fc8f0d 100644 --- a/lib/account_manager/models/per_account_collection_state/per_account_collection_state.dart +++ b/lib/account_manager/models/per_account_collection_state/per_account_collection_state.dart @@ -1,7 +1,13 @@ import 'package:async_tools/async_tools.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import '../../../chat/chat.dart'; +import '../../../chat_list/chat_list.dart'; import '../../../contact_invitation/contact_invitation.dart'; +import '../../../contacts/contacts.dart'; +import '../../../conversation/conversation.dart'; import '../../../proto/proto.dart' show Account; import '../../account_manager.dart'; @@ -9,9 +15,45 @@ part 'per_account_collection_state.freezed.dart'; @freezed class PerAccountCollectionState with _$PerAccountCollectionState { - const factory PerAccountCollectionState( - {required AccountInfo accountInfo, - required AsyncValue avAccountRecordState, - required ContactInvitationListCubit? contactInvitationListCubit}) = - _PerAccountCollectionState; + const factory PerAccountCollectionState({ + required AccountInfo accountInfo, + required AsyncValue? avAccountRecordState, + required AccountInfoCubit? accountInfoCubit, + required AccountRecordCubit? accountRecordCubit, + required ContactInvitationListCubit? contactInvitationListCubit, + required ContactListCubit? contactListCubit, + required WaitingInvitationsBlocMapCubit? waitingInvitationsBlocMapCubit, + required ActiveChatCubit? activeChatCubit, + required ChatListCubit? chatListCubit, + required ActiveConversationsBlocMapCubit? activeConversationsBlocMapCubit, + required ActiveSingleContactChatBlocMapCubit? + activeSingleContactChatBlocMapCubit, + }) = _PerAccountCollectionState; +} + +extension PerAccountCollectionStateExt on PerAccountCollectionState { + bool get isReady => + avAccountRecordState != null && + avAccountRecordState!.isData && + accountInfoCubit != null && + accountRecordCubit != null && + contactInvitationListCubit != null && + contactListCubit != null && + waitingInvitationsBlocMapCubit != null && + activeChatCubit != null && + chatListCubit != null && + activeConversationsBlocMapCubit != null && + activeSingleContactChatBlocMapCubit != null; + + Widget provide({required Widget child}) => MultiBlocProvider(providers: [ + BlocProvider.value(value: accountInfoCubit!), + BlocProvider.value(value: accountRecordCubit!), + BlocProvider.value(value: contactInvitationListCubit!), + BlocProvider.value(value: contactListCubit!), + BlocProvider.value(value: waitingInvitationsBlocMapCubit!), + BlocProvider.value(value: activeChatCubit!), + BlocProvider.value(value: chatListCubit!), + BlocProvider.value(value: activeConversationsBlocMapCubit!), + BlocProvider.value(value: activeSingleContactChatBlocMapCubit!), + ], child: child); } diff --git a/lib/account_manager/models/per_account_collection_state/per_account_collection_state.freezed.dart b/lib/account_manager/models/per_account_collection_state/per_account_collection_state.freezed.dart index d49f42a..8dcc549 100644 --- a/lib/account_manager/models/per_account_collection_state/per_account_collection_state.freezed.dart +++ b/lib/account_manager/models/per_account_collection_state/per_account_collection_state.freezed.dart @@ -17,10 +17,23 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$PerAccountCollectionState { AccountInfo get accountInfo => throw _privateConstructorUsedError; - AsyncValue get avAccountRecordState => + AsyncValue? get avAccountRecordState => + throw _privateConstructorUsedError; + AccountInfoCubit? get accountInfoCubit => throw _privateConstructorUsedError; + AccountRecordCubit? get accountRecordCubit => throw _privateConstructorUsedError; ContactInvitationListCubit? get contactInvitationListCubit => throw _privateConstructorUsedError; + ContactListCubit? get contactListCubit => throw _privateConstructorUsedError; + WaitingInvitationsBlocMapCubit? get waitingInvitationsBlocMapCubit => + throw _privateConstructorUsedError; + ActiveChatCubit? get activeChatCubit => throw _privateConstructorUsedError; + ChatListCubit? get chatListCubit => throw _privateConstructorUsedError; + ActiveConversationsBlocMapCubit? get activeConversationsBlocMapCubit => + throw _privateConstructorUsedError; + ActiveSingleContactChatBlocMapCubit? + get activeSingleContactChatBlocMapCubit => + throw _privateConstructorUsedError; @JsonKey(ignore: true) $PerAccountCollectionStateCopyWith get copyWith => @@ -35,10 +48,19 @@ abstract class $PerAccountCollectionStateCopyWith<$Res> { @useResult $Res call( {AccountInfo accountInfo, - AsyncValue avAccountRecordState, - ContactInvitationListCubit? contactInvitationListCubit}); + AsyncValue? avAccountRecordState, + AccountInfoCubit? accountInfoCubit, + AccountRecordCubit? accountRecordCubit, + ContactInvitationListCubit? contactInvitationListCubit, + ContactListCubit? contactListCubit, + WaitingInvitationsBlocMapCubit? waitingInvitationsBlocMapCubit, + ActiveChatCubit? activeChatCubit, + ChatListCubit? chatListCubit, + ActiveConversationsBlocMapCubit? activeConversationsBlocMapCubit, + ActiveSingleContactChatBlocMapCubit? + activeSingleContactChatBlocMapCubit}); - $AsyncValueCopyWith get avAccountRecordState; + $AsyncValueCopyWith? get avAccountRecordState; } /// @nodoc @@ -56,29 +78,75 @@ class _$PerAccountCollectionStateCopyWithImpl<$Res, @override $Res call({ Object? accountInfo = null, - Object? avAccountRecordState = null, + Object? avAccountRecordState = freezed, + Object? accountInfoCubit = freezed, + Object? accountRecordCubit = freezed, Object? contactInvitationListCubit = freezed, + Object? contactListCubit = freezed, + Object? waitingInvitationsBlocMapCubit = freezed, + Object? activeChatCubit = freezed, + Object? chatListCubit = freezed, + Object? activeConversationsBlocMapCubit = freezed, + Object? activeSingleContactChatBlocMapCubit = freezed, }) { return _then(_value.copyWith( accountInfo: null == accountInfo ? _value.accountInfo : accountInfo // ignore: cast_nullable_to_non_nullable as AccountInfo, - avAccountRecordState: null == avAccountRecordState + avAccountRecordState: freezed == avAccountRecordState ? _value.avAccountRecordState : avAccountRecordState // ignore: cast_nullable_to_non_nullable - as AsyncValue, + as AsyncValue?, + accountInfoCubit: freezed == accountInfoCubit + ? _value.accountInfoCubit + : accountInfoCubit // ignore: cast_nullable_to_non_nullable + as AccountInfoCubit?, + accountRecordCubit: freezed == accountRecordCubit + ? _value.accountRecordCubit + : accountRecordCubit // ignore: cast_nullable_to_non_nullable + as AccountRecordCubit?, contactInvitationListCubit: freezed == contactInvitationListCubit ? _value.contactInvitationListCubit : contactInvitationListCubit // ignore: cast_nullable_to_non_nullable as ContactInvitationListCubit?, + contactListCubit: freezed == contactListCubit + ? _value.contactListCubit + : contactListCubit // ignore: cast_nullable_to_non_nullable + as ContactListCubit?, + waitingInvitationsBlocMapCubit: freezed == waitingInvitationsBlocMapCubit + ? _value.waitingInvitationsBlocMapCubit + : waitingInvitationsBlocMapCubit // ignore: cast_nullable_to_non_nullable + as WaitingInvitationsBlocMapCubit?, + activeChatCubit: freezed == activeChatCubit + ? _value.activeChatCubit + : activeChatCubit // ignore: cast_nullable_to_non_nullable + as ActiveChatCubit?, + chatListCubit: freezed == chatListCubit + ? _value.chatListCubit + : chatListCubit // ignore: cast_nullable_to_non_nullable + as ChatListCubit?, + activeConversationsBlocMapCubit: freezed == + activeConversationsBlocMapCubit + ? _value.activeConversationsBlocMapCubit + : activeConversationsBlocMapCubit // ignore: cast_nullable_to_non_nullable + as ActiveConversationsBlocMapCubit?, + activeSingleContactChatBlocMapCubit: freezed == + activeSingleContactChatBlocMapCubit + ? _value.activeSingleContactChatBlocMapCubit + : activeSingleContactChatBlocMapCubit // ignore: cast_nullable_to_non_nullable + as ActiveSingleContactChatBlocMapCubit?, ) as $Val); } @override @pragma('vm:prefer-inline') - $AsyncValueCopyWith get avAccountRecordState { - return $AsyncValueCopyWith(_value.avAccountRecordState, + $AsyncValueCopyWith? get avAccountRecordState { + if (_value.avAccountRecordState == null) { + return null; + } + + return $AsyncValueCopyWith(_value.avAccountRecordState!, (value) { return _then(_value.copyWith(avAccountRecordState: value) as $Val); }); @@ -96,11 +164,20 @@ abstract class _$$PerAccountCollectionStateImplCopyWith<$Res> @useResult $Res call( {AccountInfo accountInfo, - AsyncValue avAccountRecordState, - ContactInvitationListCubit? contactInvitationListCubit}); + AsyncValue? avAccountRecordState, + AccountInfoCubit? accountInfoCubit, + AccountRecordCubit? accountRecordCubit, + ContactInvitationListCubit? contactInvitationListCubit, + ContactListCubit? contactListCubit, + WaitingInvitationsBlocMapCubit? waitingInvitationsBlocMapCubit, + ActiveChatCubit? activeChatCubit, + ChatListCubit? chatListCubit, + ActiveConversationsBlocMapCubit? activeConversationsBlocMapCubit, + ActiveSingleContactChatBlocMapCubit? + activeSingleContactChatBlocMapCubit}); @override - $AsyncValueCopyWith get avAccountRecordState; + $AsyncValueCopyWith? get avAccountRecordState; } /// @nodoc @@ -117,22 +194,64 @@ class __$$PerAccountCollectionStateImplCopyWithImpl<$Res> @override $Res call({ Object? accountInfo = null, - Object? avAccountRecordState = null, + Object? avAccountRecordState = freezed, + Object? accountInfoCubit = freezed, + Object? accountRecordCubit = freezed, Object? contactInvitationListCubit = freezed, + Object? contactListCubit = freezed, + Object? waitingInvitationsBlocMapCubit = freezed, + Object? activeChatCubit = freezed, + Object? chatListCubit = freezed, + Object? activeConversationsBlocMapCubit = freezed, + Object? activeSingleContactChatBlocMapCubit = freezed, }) { return _then(_$PerAccountCollectionStateImpl( accountInfo: null == accountInfo ? _value.accountInfo : accountInfo // ignore: cast_nullable_to_non_nullable as AccountInfo, - avAccountRecordState: null == avAccountRecordState + avAccountRecordState: freezed == avAccountRecordState ? _value.avAccountRecordState : avAccountRecordState // ignore: cast_nullable_to_non_nullable - as AsyncValue, + as AsyncValue?, + accountInfoCubit: freezed == accountInfoCubit + ? _value.accountInfoCubit + : accountInfoCubit // ignore: cast_nullable_to_non_nullable + as AccountInfoCubit?, + accountRecordCubit: freezed == accountRecordCubit + ? _value.accountRecordCubit + : accountRecordCubit // ignore: cast_nullable_to_non_nullable + as AccountRecordCubit?, contactInvitationListCubit: freezed == contactInvitationListCubit ? _value.contactInvitationListCubit : contactInvitationListCubit // ignore: cast_nullable_to_non_nullable as ContactInvitationListCubit?, + contactListCubit: freezed == contactListCubit + ? _value.contactListCubit + : contactListCubit // ignore: cast_nullable_to_non_nullable + as ContactListCubit?, + waitingInvitationsBlocMapCubit: freezed == waitingInvitationsBlocMapCubit + ? _value.waitingInvitationsBlocMapCubit + : waitingInvitationsBlocMapCubit // ignore: cast_nullable_to_non_nullable + as WaitingInvitationsBlocMapCubit?, + activeChatCubit: freezed == activeChatCubit + ? _value.activeChatCubit + : activeChatCubit // ignore: cast_nullable_to_non_nullable + as ActiveChatCubit?, + chatListCubit: freezed == chatListCubit + ? _value.chatListCubit + : chatListCubit // ignore: cast_nullable_to_non_nullable + as ChatListCubit?, + activeConversationsBlocMapCubit: freezed == + activeConversationsBlocMapCubit + ? _value.activeConversationsBlocMapCubit + : activeConversationsBlocMapCubit // ignore: cast_nullable_to_non_nullable + as ActiveConversationsBlocMapCubit?, + activeSingleContactChatBlocMapCubit: freezed == + activeSingleContactChatBlocMapCubit + ? _value.activeSingleContactChatBlocMapCubit + : activeSingleContactChatBlocMapCubit // ignore: cast_nullable_to_non_nullable + as ActiveSingleContactChatBlocMapCubit?, )); } } @@ -143,18 +262,43 @@ class _$PerAccountCollectionStateImpl implements _PerAccountCollectionState { const _$PerAccountCollectionStateImpl( {required this.accountInfo, required this.avAccountRecordState, - required this.contactInvitationListCubit}); + required this.accountInfoCubit, + required this.accountRecordCubit, + required this.contactInvitationListCubit, + required this.contactListCubit, + required this.waitingInvitationsBlocMapCubit, + required this.activeChatCubit, + required this.chatListCubit, + required this.activeConversationsBlocMapCubit, + required this.activeSingleContactChatBlocMapCubit}); @override final AccountInfo accountInfo; @override - final AsyncValue avAccountRecordState; + final AsyncValue? avAccountRecordState; + @override + final AccountInfoCubit? accountInfoCubit; + @override + final AccountRecordCubit? accountRecordCubit; @override final ContactInvitationListCubit? contactInvitationListCubit; + @override + final ContactListCubit? contactListCubit; + @override + final WaitingInvitationsBlocMapCubit? waitingInvitationsBlocMapCubit; + @override + final ActiveChatCubit? activeChatCubit; + @override + final ChatListCubit? chatListCubit; + @override + final ActiveConversationsBlocMapCubit? activeConversationsBlocMapCubit; + @override + final ActiveSingleContactChatBlocMapCubit? + activeSingleContactChatBlocMapCubit; @override String toString() { - return 'PerAccountCollectionState(accountInfo: $accountInfo, avAccountRecordState: $avAccountRecordState, contactInvitationListCubit: $contactInvitationListCubit)'; + return 'PerAccountCollectionState(accountInfo: $accountInfo, avAccountRecordState: $avAccountRecordState, accountInfoCubit: $accountInfoCubit, accountRecordCubit: $accountRecordCubit, contactInvitationListCubit: $contactInvitationListCubit, contactListCubit: $contactListCubit, waitingInvitationsBlocMapCubit: $waitingInvitationsBlocMapCubit, activeChatCubit: $activeChatCubit, chatListCubit: $chatListCubit, activeConversationsBlocMapCubit: $activeConversationsBlocMapCubit, activeSingleContactChatBlocMapCubit: $activeSingleContactChatBlocMapCubit)'; } @override @@ -166,15 +310,48 @@ class _$PerAccountCollectionStateImpl implements _PerAccountCollectionState { other.accountInfo == accountInfo) && (identical(other.avAccountRecordState, avAccountRecordState) || other.avAccountRecordState == avAccountRecordState) && + (identical(other.accountInfoCubit, accountInfoCubit) || + other.accountInfoCubit == accountInfoCubit) && + (identical(other.accountRecordCubit, accountRecordCubit) || + other.accountRecordCubit == accountRecordCubit) && (identical(other.contactInvitationListCubit, contactInvitationListCubit) || other.contactInvitationListCubit == - contactInvitationListCubit)); + contactInvitationListCubit) && + (identical(other.contactListCubit, contactListCubit) || + other.contactListCubit == contactListCubit) && + (identical(other.waitingInvitationsBlocMapCubit, + waitingInvitationsBlocMapCubit) || + other.waitingInvitationsBlocMapCubit == + waitingInvitationsBlocMapCubit) && + (identical(other.activeChatCubit, activeChatCubit) || + other.activeChatCubit == activeChatCubit) && + (identical(other.chatListCubit, chatListCubit) || + other.chatListCubit == chatListCubit) && + (identical(other.activeConversationsBlocMapCubit, + activeConversationsBlocMapCubit) || + other.activeConversationsBlocMapCubit == + activeConversationsBlocMapCubit) && + (identical(other.activeSingleContactChatBlocMapCubit, + activeSingleContactChatBlocMapCubit) || + other.activeSingleContactChatBlocMapCubit == + activeSingleContactChatBlocMapCubit)); } @override - int get hashCode => Object.hash(runtimeType, accountInfo, - avAccountRecordState, contactInvitationListCubit); + int get hashCode => Object.hash( + runtimeType, + accountInfo, + avAccountRecordState, + accountInfoCubit, + accountRecordCubit, + contactInvitationListCubit, + contactListCubit, + waitingInvitationsBlocMapCubit, + activeChatCubit, + chatListCubit, + activeConversationsBlocMapCubit, + activeSingleContactChatBlocMapCubit); @JsonKey(ignore: true) @override @@ -186,18 +363,45 @@ class _$PerAccountCollectionStateImpl implements _PerAccountCollectionState { abstract class _PerAccountCollectionState implements PerAccountCollectionState { const factory _PerAccountCollectionState( - {required final AccountInfo accountInfo, - required final AsyncValue avAccountRecordState, - required final ContactInvitationListCubit? - contactInvitationListCubit}) = _$PerAccountCollectionStateImpl; + {required final AccountInfo accountInfo, + required final AsyncValue? avAccountRecordState, + required final AccountInfoCubit? accountInfoCubit, + required final AccountRecordCubit? accountRecordCubit, + required final ContactInvitationListCubit? contactInvitationListCubit, + required final ContactListCubit? contactListCubit, + required final WaitingInvitationsBlocMapCubit? + waitingInvitationsBlocMapCubit, + required final ActiveChatCubit? activeChatCubit, + required final ChatListCubit? chatListCubit, + required final ActiveConversationsBlocMapCubit? + activeConversationsBlocMapCubit, + required final ActiveSingleContactChatBlocMapCubit? + activeSingleContactChatBlocMapCubit}) = + _$PerAccountCollectionStateImpl; @override AccountInfo get accountInfo; @override - AsyncValue get avAccountRecordState; + AsyncValue? get avAccountRecordState; + @override + AccountInfoCubit? get accountInfoCubit; + @override + AccountRecordCubit? get accountRecordCubit; @override ContactInvitationListCubit? get contactInvitationListCubit; @override + ContactListCubit? get contactListCubit; + @override + WaitingInvitationsBlocMapCubit? get waitingInvitationsBlocMapCubit; + @override + ActiveChatCubit? get activeChatCubit; + @override + ChatListCubit? get chatListCubit; + @override + ActiveConversationsBlocMapCubit? get activeConversationsBlocMapCubit; + @override + ActiveSingleContactChatBlocMapCubit? get activeSingleContactChatBlocMapCubit; + @override @JsonKey(ignore: true) _$$PerAccountCollectionStateImplCopyWith<_$PerAccountCollectionStateImpl> get copyWith => throw _privateConstructorUsedError; diff --git a/lib/account_manager/views/edit_account_page.dart b/lib/account_manager/views/edit_account_page.dart index 986fd0b..468bf48 100644 --- a/lib/account_manager/views/edit_account_page.dart +++ b/lib/account_manager/views/edit_account_page.dart @@ -116,14 +116,11 @@ class _EditAccountPageState extends State { }); try { // Look up account cubit for this specific account - final perAccountCollectionCubit = - context.read(); - await perAccountCollectionCubit.operateAsync( - widget.superIdentityRecordKey, closure: (c) async { - // Update account profile DHT record - // This triggers ConversationCubits to update - await c.accountRecordCubit!.updateProfile(newProfile); - }); + final accountRecordCubit = context.read(); + + // Update account profile DHT record + // This triggers ConversationCubits to update + await accountRecordCubit.updateProfile(newProfile); // Update local account profile await AccountRepository.instance.editAccountProfile( diff --git a/lib/layout/home/drawer_menu/drawer_menu.dart b/lib/layout/home/drawer_menu/drawer_menu.dart index f68a277..1e56c4b 100644 --- a/lib/layout/home/drawer_menu/drawer_menu.dart +++ b/lib/layout/home/drawer_menu/drawer_menu.dart @@ -105,7 +105,8 @@ class _DrawerMenuState extends State { Widget _getAccountList( {required IList localAccounts, required TypedKey? activeLocalAccount, - required AccountRecordsBlocMapState accountRecords}) { + required PerAccountCollectionBlocMapState + perAccountCollectionBlocMapState}) { final theme = Theme.of(context); final scaleScheme = theme.extension()!; @@ -116,11 +117,13 @@ class _DrawerMenuState extends State { final superIdentityRecordKey = la.superIdentity.recordKey; // See if this account is logged in - final acctRecord = accountRecords.get(superIdentityRecordKey); - if (acctRecord != null) { + final avAccountRecordState = perAccountCollectionBlocMapState + .get(superIdentityRecordKey) + ?.avAccountRecordState; + if (avAccountRecordState != null) { // Account is logged in final scale = theme.extension()!.tertiaryScale; - final loggedInAccount = acctRecord.when( + final loggedInAccount = avAccountRecordState.when( data: (value) => _makeAccountWidget( name: value.profile.name, scale: scale, @@ -233,7 +236,7 @@ class _DrawerMenuState extends State { final scale = theme.extension()!; //final textTheme = theme.textTheme; final localAccounts = context.watch().state; - final accountRecords = + final perAccountCollectionBlocMapState = context.watch().state; final activeLocalAccount = context.watch().state; final gradient = LinearGradient( @@ -278,7 +281,7 @@ class _DrawerMenuState extends State { _getAccountList( localAccounts: localAccounts, activeLocalAccount: activeLocalAccount, - accountRecords: accountRecords), + perAccountCollectionBlocMapState: perAccountCollectionBlocMapState), _getBottomButtons(), const Spacer(), Row(children: [ diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index ddbb99e..16d484d 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -9,11 +9,8 @@ import 'package:veilid_support/veilid_support.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 '../../conversation/conversation.dart'; -import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; import '../../tools/tools.dart'; import 'active_account_page_controller_wrapper.dart'; @@ -98,113 +95,39 @@ class HomeScreenState extends State { return const HomeAccountReadyMain(); } - Widget _buildUnlockedAccount(BuildContext context) { - final accountRecordKey = context.select( - (c) => c.state.unlockedAccountInfo!.accountRecordKey); - final contactListRecordPointer = - context.select( - (c) => c.state.asData?.value.contactList.toVeilid()); - final contactInvitationListRecordPointer = - context.select( - (c) => c.state.asData?.value.contactInvitationRecords.toVeilid()); - final chatListRecordPointer = - context.select( - (c) => c.state.asData?.value.chatList.toVeilid()); + Widget _buildAccount(BuildContext context, TypedKey superIdentityRecordKey, + PerAccountCollectionState perAccountCollectionState) { + switch (perAccountCollectionState.accountInfo.status) { + case AccountInfoStatus.accountInvalid: + return const HomeAccountInvalid(); + case AccountInfoStatus.accountLocked: + return const HomeAccountLocked(); + case AccountInfoStatus.accountUnlocked: + // Are we ready to render? + if (!perAccountCollectionState.isReady) { + return waitingPage(); + } - if (contactListRecordPointer == null || - contactInvitationListRecordPointer == null || - chatListRecordPointer == null) { - return waitingPage(); - } - - return MultiBlocProvider( - providers: [ - // Contact Cubits - BlocProvider( - create: (context) => ContactInvitationListCubit( - locator: context.read, - accountRecordKey: accountRecordKey, - contactInvitationListRecordPointer: - contactInvitationListRecordPointer, - )), - BlocProvider( - create: (context) => ContactListCubit( - locator: context.read, - accountRecordKey: accountRecordKey, - contactListRecordPointer: contactListRecordPointer)), - BlocProvider( - create: (context) => WaitingInvitationsBlocMapCubit( - locator: context.read, - )), - // Chat Cubits - BlocProvider( - create: (context) => ActiveChatCubit( - null, - )), - BlocProvider( - create: (context) => ChatListCubit( - locator: context.read, - accountRecordKey: accountRecordKey, - chatListRecordPointer: chatListRecordPointer)), - // Conversation Cubits - BlocProvider( - create: (context) => ActiveConversationsBlocMapCubit( - locator: context.read, - )), - BlocProvider( - create: (context) => ActiveSingleContactChatBlocMapCubit( - locator: context.read, - )), - ], - child: MultiBlocListener(listeners: [ + // Re-export all ready blocs to the account display subtree + return perAccountCollectionState.provide( + child: MultiBlocListener(listeners: [ BlocListener( listener: _invitationStatusListener, ) ], child: Builder(builder: _buildAccountReadyDeviceSpecific))); + } } - Widget _buildAccount(BuildContext context, TypedKey superIdentityRecordKey, - PerAccountCollectionCubit perAccountCollectionCubit) => - BlocBuilder( - key: ValueKey(superIdentityRecordKey), - bloc: perAccountCollectionCubit, - builder: (context, state) { - - - switch (state.accountInfo.status) { - case AccountInfoStatus.accountInvalid: - return const HomeAccountInvalid(); - case AccountInfoStatus.accountLocked: - return const HomeAccountLocked(); - case AccountInfoStatus.accountUnlocked: - - // Get the current active account record cubit - final activeAccountRecordCubit = context.select< - PerAccountCollectionBlocMapCubit, - AccountRecordCubit?>( - (c) => c.tryOperate(superIdentityRecordKey, - closure: (x) => x)); - if (activeAccountRecordCubit == null) { - return waitingPage(); - } - - return MultiBlocProvider(providers: [ - BlocProvider.value( - value: activeAccountRecordCubit), - ], child: Builder(builder: _buildUnlockedAccount)); - } - }); - }; - Widget _buildAccountPageView(BuildContext context) { final localAccounts = context.watch().state; - final activeLocalAccountCubit = context.watch(); - final perAccountCollectionBlocMapCubit = - context.watch(); + final activeLocalAccountCubit = + context.watch().state; + final perAccountCollectionBlocMapState = + context.watch().state; final activeIndex = localAccounts.indexWhere( - (x) => x.superIdentity.recordKey == activeLocalAccountCubit.state); + (x) => x.superIdentity.recordKey == activeLocalAccountCubit); if (activeIndex == -1) { return const HomeNoActive(); } @@ -231,16 +154,15 @@ class HomeScreenState extends State { itemBuilder: (context, index) { final superIdentityRecordKey = localAccounts[index].superIdentity.recordKey; - final perAccountCollectionCubit = - perAccountCollectionBlocMapCubit.tryOperate( - superIdentityRecordKey, - closure: (c) => c); - if (perAccountCollectionCubit == null) { + final perAccountCollectionState = + perAccountCollectionBlocMapState + .get(superIdentityRecordKey); + if (perAccountCollectionState == null) { return HomeAccountMissing( key: ValueKey(superIdentityRecordKey)); } return _buildAccount(context, superIdentityRecordKey, - perAccountCollectionCubit); + perAccountCollectionState); }))); } From 3f8b4d2a41d556330e29ab2c95b84682d0a7ae56 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 19 Jun 2024 13:44:22 -0400 Subject: [PATCH 145/270] clean up --- .../cubits/per_account_collection_cubit.dart | 43 ++++++++++--------- lib/layout/home/home_account_missing.dart | 10 ----- 2 files changed, 23 insertions(+), 30 deletions(-) diff --git a/lib/account_manager/cubits/per_account_collection_cubit.dart b/lib/account_manager/cubits/per_account_collection_cubit.dart index 0678f4b..8b8600b 100644 --- a/lib/account_manager/cubits/per_account_collection_cubit.dart +++ b/lib/account_manager/cubits/per_account_collection_cubit.dart @@ -26,6 +26,8 @@ class PerAccountCollectionCubit extends Cubit { @override Future close() async { + await _initWait(); + await _processor.close(); await accountInfoCubit.close(); await _accountRecordSubscription?.cancel(); @@ -43,8 +45,6 @@ class PerAccountCollectionCubit extends Cubit { } Future _init() async { - await _initWait(); - // subscribe to accountInfo changes _processor.follow(accountInfoCubit.stream, accountInfoCubit.state, _followAccountInfoState); @@ -66,11 +66,6 @@ class PerAccountCollectionCubit extends Cubit { activeSingleContactChatBlocMapCubit: null); Future _followAccountInfoState(AccountInfo accountInfo) async { - // If state hasn't changed just return - if (state.accountInfo == accountInfo) { - return; - } - // Get the next state var nextState = state.copyWith(accountInfo: accountInfo); @@ -83,8 +78,7 @@ class PerAccountCollectionCubit extends Cubit { _accountRecordSubscription = null; // Update state to 'loading' - nextState = - _updateAccountRecordState(nextState, const AsyncValue.loading()); + nextState = _updateAccountRecordState(nextState, null); emit(nextState); // Close AccountRecordCubit @@ -116,7 +110,7 @@ class PerAccountCollectionCubit extends Cubit { AsyncValue? avAccountRecordState) { // Get next state final nextState = - state.copyWith(avAccountRecordState: avAccountRecordState); + prevState.copyWith(avAccountRecordState: avAccountRecordState); // Get bloc parameters final accountInfo = nextState.accountInfo; @@ -127,7 +121,8 @@ class PerAccountCollectionCubit extends Cubit { .toVeilid(); final contactInvitationListCubit = contactInvitationListCubitUpdater.update( - contactInvitationListRecordPointer == null + accountInfo.userLogin == null || + contactInvitationListRecordPointer == null ? null : (accountInfo, contactInvitationListRecordPointer)); @@ -136,26 +131,33 @@ class PerAccountCollectionCubit extends Cubit { nextState.avAccountRecordState?.asData?.value.contactList.toVeilid(); final contactListCubit = contactListCubitUpdater.update( - contactListRecordPointer == null + accountInfo.userLogin == null || contactListRecordPointer == null ? null : (accountInfo, contactListRecordPointer)); // WaitingInvitationsBlocMapCubit - final waitingInvitationsBlocMapCubit = waitingInvitationsBlocMapCubitUpdater - .update(contactInvitationListCubit == null - ? null - : (accountInfo, accountRecordCubit!, contactInvitationListCubit)); + final waitingInvitationsBlocMapCubit = + waitingInvitationsBlocMapCubitUpdater.update( + accountInfo.userLogin == null || contactInvitationListCubit == null + ? null + : ( + accountInfo, + accountRecordCubit!, + contactInvitationListCubit + )); // ActiveChatCubit - final activeChatCubit = activeChatCubitUpdater.update( - (nextState.avAccountRecordState?.isData ?? false) ? true : null); + final activeChatCubit = activeChatCubitUpdater + .update((accountInfo.userLogin == null) ? null : true); // ChatListCubit final chatListRecordPointer = nextState.avAccountRecordState?.asData?.value.chatList.toVeilid(); final chatListCubit = chatListCubitUpdater.update( - chatListRecordPointer == null || activeChatCubit == null + accountInfo.userLogin == null || + chatListRecordPointer == null || + activeChatCubit == null ? null : (accountInfo, chatListRecordPointer, activeChatCubit)); @@ -176,7 +178,8 @@ class PerAccountCollectionCubit extends Cubit { // ActiveSingleContactChatBlocMapCubit final activeSingleContactChatBlocMapCubit = activeSingleContactChatBlocMapCubitUpdater.update( - activeConversationsBlocMapCubit == null || + accountInfo.userLogin == null || + activeConversationsBlocMapCubit == null || chatListCubit == null || contactListCubit == null ? null diff --git a/lib/layout/home/home_account_missing.dart b/lib/layout/home/home_account_missing.dart index d9c0aad..a2e4db4 100644 --- a/lib/layout/home/home_account_missing.dart +++ b/lib/layout/home/home_account_missing.dart @@ -21,13 +21,3 @@ class HomeAccountMissingState extends State { @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 From 17211f3515f1f0ded8bfd30d9bf19fba6b9a3deb Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 20 Jun 2024 19:04:39 -0400 Subject: [PATCH 146/270] checkpoint --- ...per_account_collection_bloc_map_cubit.dart | 18 +- .../cubits/per_account_collection_cubit.dart | 45 ++-- .../views/edit_account_page.dart | 9 +- lib/chat/cubits/chat_component_cubit.dart | 46 +++- lib/chat/views/chat_component_widget.dart | 10 +- lib/chat_list/cubits/chat_list_cubit.dart | 42 +++- lib/chat_list/views/chat_list_widget.dart | 96 ++++++++ .../chat_single_contact_list_widget.dart | 76 ------- lib/chat_list/views/views.dart | 2 +- .../cubits/waiting_invitation_cubit.dart | 7 +- .../waiting_invitations_bloc_map_cubit.dart | 66 +++++- .../active_conversations_bloc_map_cubit.dart | 91 +++++--- ...ve_single_contact_chat_bloc_map_cubit.dart | 134 +++++------ .../main_pager/chats_page.dart | 2 +- lib/layout/home/home_screen.dart | 61 +---- lib/proto/extensions.dart | 13 ++ lib/proto/veilidchat.pb.dart | 209 +++++++++++++++--- lib/proto/veilidchat.pbjson.dart | 73 ++++-- lib/proto/veilidchat.proto | 30 ++- .../src/dht_log/dht_log_spine.dart | 2 +- .../dht_record/default_dht_record_cubit.dart | 8 - .../src/dht_record/dht_record_cubit.dart | 14 -- 22 files changed, 701 insertions(+), 353 deletions(-) create mode 100644 lib/chat_list/views/chat_list_widget.dart delete mode 100644 lib/chat_list/views/chat_single_contact_list_widget.dart diff --git a/lib/account_manager/cubits/per_account_collection_bloc_map_cubit.dart b/lib/account_manager/cubits/per_account_collection_bloc_map_cubit.dart index f1df6e6..f5334c1 100644 --- a/lib/account_manager/cubits/per_account_collection_bloc_map_cubit.dart +++ b/lib/account_manager/cubits/per_account_collection_bloc_map_cubit.dart @@ -38,9 +38,23 @@ class PerAccountCollectionBlocMapCubit extends BlocMapCubit removeFromState(TypedKey key) => remove(key); @override - Future updateState(TypedKey key, LocalAccount value) async { + Future updateState( + TypedKey key, LocalAccount? oldValue, LocalAccount newValue) async { + // Don't replace unless this is a totally different account + // The sub-cubit's subscription will update our state later + if (oldValue != null) { + if (oldValue.superIdentity.recordKey != + newValue.superIdentity.recordKey) { + throw StateError( + 'should remove LocalAccount and make a new one, not change it, if ' + 'the superidentity record key has changed'); + } + // This never changes anything that should result in rebuildin the + // sub-cubit + return; + } await _addPerAccountCollectionCubit( - superIdentityRecordKey: value.superIdentity.recordKey); + superIdentityRecordKey: newValue.superIdentity.recordKey); } //////////////////////////////////////////////////////////////////////////// diff --git a/lib/account_manager/cubits/per_account_collection_cubit.dart b/lib/account_manager/cubits/per_account_collection_cubit.dart index 8b8600b..6cb8d5d 100644 --- a/lib/account_manager/cubits/per_account_collection_cubit.dart +++ b/lib/account_manager/cubits/per_account_collection_cubit.dart @@ -136,15 +136,17 @@ class PerAccountCollectionCubit extends Cubit { : (accountInfo, contactListRecordPointer)); // WaitingInvitationsBlocMapCubit - final waitingInvitationsBlocMapCubit = - waitingInvitationsBlocMapCubitUpdater.update( - accountInfo.userLogin == null || contactInvitationListCubit == null - ? null - : ( - accountInfo, - accountRecordCubit!, - contactInvitationListCubit - )); + final waitingInvitationsBlocMapCubit = waitingInvitationsBlocMapCubitUpdater + .update(accountInfo.userLogin == null || + contactInvitationListCubit == null || + contactListCubit == null + ? null + : ( + accountInfo, + accountRecordCubit!, + contactInvitationListCubit, + contactListCubit, + )); // ActiveChatCubit final activeChatCubit = activeChatCubitUpdater @@ -179,15 +181,11 @@ class PerAccountCollectionCubit extends Cubit { final activeSingleContactChatBlocMapCubit = activeSingleContactChatBlocMapCubitUpdater.update( accountInfo.userLogin == null || - activeConversationsBlocMapCubit == null || - chatListCubit == null || - contactListCubit == null + activeConversationsBlocMapCubit == null ? null : ( accountInfo, activeConversationsBlocMapCubit, - chatListCubit, - contactListCubit )); // Update available blocs in our state @@ -260,11 +258,18 @@ class PerAccountCollectionCubit extends Cubit { )); final waitingInvitationsBlocMapCubitUpdater = BlocUpdater< WaitingInvitationsBlocMapCubit, - (AccountInfo, AccountRecordCubit, ContactInvitationListCubit)>( + ( + AccountInfo, + AccountRecordCubit, + ContactInvitationListCubit, + ContactListCubit + )>( create: (params) => WaitingInvitationsBlocMapCubit( - accountInfo: params.$1, - accountRecordCubit: params.$2, - contactInvitationListCubit: params.$3)); + accountInfo: params.$1, + accountRecordCubit: params.$2, + contactInvitationListCubit: params.$3, + contactListCubit: params.$4, + )); final activeChatCubitUpdater = BlocUpdater(create: (_) => ActiveChatCubit(null)); final chatListCubitUpdater = BlocUpdater { ( AccountInfo, ActiveConversationsBlocMapCubit, - ChatListCubit, - ContactListCubit )>( create: (params) => ActiveSingleContactChatBlocMapCubit( accountInfo: params.$1, activeConversationsBlocMapCubit: params.$2, - chatListCubit: params.$3, - contactListCubit: params.$4, )); } diff --git a/lib/account_manager/views/edit_account_page.dart b/lib/account_manager/views/edit_account_page.dart index 468bf48..0e57d77 100644 --- a/lib/account_manager/views/edit_account_page.dart +++ b/lib/account_manager/views/edit_account_page.dart @@ -116,7 +116,14 @@ class _EditAccountPageState extends State { }); try { // Look up account cubit for this specific account - final accountRecordCubit = context.read(); + final perAccountCollectionBlocMapCubit = + context.read(); + final accountRecordCubit = await perAccountCollectionBlocMapCubit + .operate(widget.superIdentityRecordKey, + closure: (c) async => c.accountRecordCubit); + if (accountRecordCubit == null) { + return; + } // Update account profile DHT record // This triggers ConversationCubits to update diff --git a/lib/chat/cubits/chat_component_cubit.dart b/lib/chat/cubits/chat_component_cubit.dart index 05c722c..cd0f724 100644 --- a/lib/chat/cubits/chat_component_cubit.dart +++ b/lib/chat/cubits/chat_component_cubit.dart @@ -12,6 +12,7 @@ import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; +import '../../contacts/contacts.dart'; import '../../conversation/conversation.dart'; import '../../proto/proto.dart' as proto; import '../models/chat_component_state.dart'; @@ -28,10 +29,12 @@ class ChatComponentCubit extends Cubit { ChatComponentCubit._({ required AccountInfo accountInfo, required AccountRecordCubit accountRecordCubit, + required ContactListCubit contactListCubit, required List conversationCubits, required SingleContactMessagesCubit messagesCubit, }) : _accountInfo = accountInfo, _accountRecordCubit = accountRecordCubit, + _contactListCubit = contactListCubit, _conversationCubits = conversationCubits, _messagesCubit = messagesCubit, super(ChatComponentState( @@ -51,11 +54,13 @@ class ChatComponentCubit extends Cubit { factory ChatComponentCubit.singleContact( {required AccountInfo accountInfo, required AccountRecordCubit accountRecordCubit, + required ContactListCubit contactListCubit, required ActiveConversationCubit activeConversationCubit, required SingleContactMessagesCubit messagesCubit}) => ChatComponentCubit._( accountInfo: accountInfo, accountRecordCubit: accountRecordCubit, + contactListCubit: contactListCubit, conversationCubits: [activeConversationCubit], messagesCubit: messagesCubit, ); @@ -82,6 +87,7 @@ class ChatComponentCubit extends Cubit { await _initWait(); await _accountRecordSubscription.cancel(); await _messagesSubscription.cancel(); + await _conversationSubscriptions.values.map((v) => v.cancel()).wait; await super.close(); } @@ -146,12 +152,12 @@ class ChatComponentCubit extends Cubit { // Private Implementation void _onChangedAccountRecord(AsyncValue avAccount) { + // Update local 'User' final account = avAccount.asData?.value; if (account == null) { emit(state.copyWith(localUser: null)); return; } - // Make local 'User' final localUser = types.User( id: _localUserIdentityKey.toString(), firstName: account.profile.name, @@ -168,15 +174,40 @@ class ChatComponentCubit extends Cubit { TypedKey remoteIdentityPublicKey, AsyncValue avConversationState, ) { - // + // Update remote 'User' + final activeConversationState = avConversationState.asData?.value; + if (activeConversationState == null) { + // Don't change user information on loading state + return; + } + emit(state.copyWith( + remoteUsers: state.remoteUsers.add( + remoteIdentityPublicKey, + _convertRemoteUser( + remoteIdentityPublicKey, activeConversationState)))); } types.User _convertRemoteUser(TypedKey remoteIdentityPublicKey, - ActiveConversationState activeConversationState) => - types.User( - id: remoteIdentityPublicKey.toString(), - firstName: activeConversationState.contact.displayName, - metadata: {metadataKeyIdentityPublicKey: remoteIdentityPublicKey}); + ActiveConversationState activeConversationState) { + // See if we have a contact for this remote user + final contacts = _contactListCubit.state.state.asData?.value; + if (contacts != null) { + final contactIdx = contacts.indexWhere((x) => + x.value.identityPublicKey.toVeilid() == remoteIdentityPublicKey); + if (contactIdx != -1) { + final contact = contacts[contactIdx].value; + return types.User( + id: remoteIdentityPublicKey.toString(), + firstName: contact.displayName, + metadata: {metadataKeyIdentityPublicKey: remoteIdentityPublicKey}); + } + } + + return types.User( + id: remoteIdentityPublicKey.toString(), + firstName: activeConversationState.remoteConversation.profile.name, + metadata: {metadataKeyIdentityPublicKey: remoteIdentityPublicKey}); + } types.User _convertUnknownUser(TypedKey remoteIdentityPublicKey) => types.User( @@ -376,6 +407,7 @@ class ChatComponentCubit extends Cubit { final _initWait = WaitSet(); final AccountInfo _accountInfo; final AccountRecordCubit _accountRecordCubit; + final ContactListCubit _contactListCubit; final List _conversationCubits; final SingleContactMessagesCubit _messagesCubit; diff --git a/lib/chat/views/chat_component_widget.dart b/lib/chat/views/chat_component_widget.dart index 5035969..6d0fa73 100644 --- a/lib/chat/views/chat_component_widget.dart +++ b/lib/chat/views/chat_component_widget.dart @@ -9,6 +9,7 @@ import 'package:flutter_chat_ui/flutter_chat_ui.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; +import '../../contacts/contacts.dart'; import '../../conversation/conversation.dart'; import '../../theme/theme.dart'; import '../chat.dart'; @@ -28,10 +29,13 @@ class ChatComponentWidget extends StatelessWidget { // Get the account record cubit final accountRecordCubit = context.read(); + // Get the contact list cubit + final contactListCubit = context.watch(); + // Get the active conversation cubit final activeConversationCubit = context .select( - (x) => x.tryOperate(localConversationRecordKey, + (x) => x.tryOperateSync(localConversationRecordKey, closure: (cubit) => cubit)); if (activeConversationCubit == null) { return waitingPage(); @@ -41,7 +45,7 @@ class ChatComponentWidget extends StatelessWidget { final messagesCubit = context.select< ActiveSingleContactChatBlocMapCubit, SingleContactMessagesCubit?>( - (x) => x.tryOperate(localConversationRecordKey, + (x) => x.tryOperateSync(localConversationRecordKey, closure: (cubit) => cubit)); if (messagesCubit == null) { return waitingPage(); @@ -49,9 +53,11 @@ class ChatComponentWidget extends StatelessWidget { // Make chat component state return BlocProvider( + key: key, create: (context) => ChatComponentCubit.singleContact( accountInfo: accountInfo, accountRecordCubit: accountRecordCubit, + contactListCubit: contactListCubit, activeConversationCubit: activeConversationCubit, messagesCubit: messagesCubit, ), diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index 1b593c8..fa262f6 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -54,6 +54,7 @@ class ChatListCubit extends DHTShortArrayCubit // Make local copy so we don't share the buffer final localConversationRecordKey = contact.localConversationRecordKey.toVeilid(); + final remoteIdentityPublicKey = contact.identityPublicKey.toVeilid(); final remoteConversationRecordKey = contact.remoteConversationRecordKey.toVeilid(); @@ -67,18 +68,38 @@ class ChatListCubit extends DHTShortArrayCubit throw Exception('Failed to get chat'); } final c = proto.Chat.fromBuffer(cbuf); - if (c.localConversationRecordKey == - contact.localConversationRecordKey) { - // Nothing to do here - return; + + switch (c.whichKind()) { + case proto.Chat_Kind.direct: + if (c.direct.localConversationRecordKey == + contact.localConversationRecordKey) { + // Nothing to do here + return; + } + break; + case proto.Chat_Kind.group: + if (c.group.localConversationRecordKey == + contact.localConversationRecordKey) { + throw StateError('direct conversation record key should' + ' not be used for group chats!'); + } + break; + case proto.Chat_Kind.notSet: + throw StateError('unknown chat kind'); } } // Create 1:1 conversation type Chat - final chat = proto.Chat() + final chatMember = proto.ChatMember() + ..remoteIdentityPublicKey = remoteIdentityPublicKey.toProto() + ..remoteConversationRecordKey = remoteConversationRecordKey.toProto(); + + final directChat = proto.DirectChat() ..settings = await getDefaultChatSettings(contact) ..localConversationRecordKey = localConversationRecordKey.toProto() - ..remoteConversationRecordKey = remoteConversationRecordKey.toProto(); + ..remoteMember = chatMember; + + final chat = proto.Chat()..direct = directChat; // Add chat await writer.add(chat.writeToBuffer()); @@ -88,9 +109,6 @@ class ChatListCubit extends DHTShortArrayCubit /// Delete a chat Future deleteChat( {required TypedKey localConversationRecordKey}) async { - final localConversationRecordKeyProto = - localConversationRecordKey.toProto(); - // Remove Chat from account's list // if this fails, don't keep retrying, user can try again later final deletedItem = @@ -104,9 +122,9 @@ class ChatListCubit extends DHTShortArrayCubit if (c == null) { throw Exception('Failed to get chat'); } + if (c.localConversationRecordKey == - localConversationRecordKeyProto) { - // Found the right chat + localConversationRecordKey) { await writer.remove(i); return c; } @@ -133,7 +151,7 @@ class ChatListCubit extends DHTShortArrayCubit return IMap(); } return IMap.fromIterable(stateValue, - keyMapper: (e) => e.value.localConversationRecordKey.toVeilid(), + keyMapper: (e) => e.value.localConversationRecordKey, valueMapper: (e) => e.value); } diff --git a/lib/chat_list/views/chat_list_widget.dart b/lib/chat_list/views/chat_list_widget.dart new file mode 100644 index 0000000..e91cbba --- /dev/null +++ b/lib/chat_list/views/chat_list_widget.dart @@ -0,0 +1,96 @@ +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:searchable_listview/searchable_listview.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../contacts/contacts.dart'; +import '../../proto/proto.dart' as proto; +import '../../proto/proto.dart'; +import '../../theme/theme.dart'; +import '../chat_list.dart'; + +class ChatListWidget extends StatelessWidget { + const ChatListWidget({super.key}); + + Widget _itemBuilderDirect(proto.DirectChat direct, + IMap contactMap, bool busy) { + final contact = contactMap[direct.localConversationRecordKey]; + if (contact == null) { + return const Text('...'); + } + return ChatSingleContactItemWidget(contact: contact, disabled: busy) + .paddingLTRB(0, 4, 0, 0); + } + + List _itemFilter(IMap contactMap, + IList> chatList, String filter) { + final lowerValue = filter.toLowerCase(); + return chatList.map((x) => x.value).where((c) { + switch (c.whichKind()) { + case proto.Chat_Kind.direct: + final contact = contactMap[c.direct.localConversationRecordKey]; + if (contact == null) { + return false; + } + return contact.nickname.toLowerCase().contains(lowerValue) || + contact.profile.name.toLowerCase().contains(lowerValue) || + contact.profile.pronouns.toLowerCase().contains(lowerValue); + case proto.Chat_Kind.group: + // xxx: how to filter group chats + return true; + case proto.Chat_Kind.notSet: + throw StateError('unknown chat kind'); + } + }).toList(); + } + + @override + // ignore: prefer_expression_function_bodies + Widget build(BuildContext context) { + final contactListV = context.watch().state; + + return contactListV.builder((context, contactList) { + final contactMap = IMap.fromIterable(contactList, + keyMapper: (c) => c.value.localConversationRecordKey, + valueMapper: (c) => c.value); + + 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( + initialList: chatList.map((x) => x.value).toList(), + itemBuilder: (c) { + switch (c.whichKind()) { + case proto.Chat_Kind.direct: + return _itemBuilderDirect( + c.direct, + contactMap, + contactListV.busy || chatListV.busy); + case proto.Chat_Kind.group: + return const Text( + 'group chats not yet supported!'); + case proto.Chat_Kind.notSet: + throw StateError('unknown chat kind'); + } + }, + filter: (value) => + _itemFilter(contactMap, chatList, value), + spaceBetweenSearchAndList: 4, + inputDecoration: InputDecoration( + labelText: translate('chat_list.search'), + ), + ), + ).paddingAll(8)))) + .paddingLTRB(8, 0, 8, 8); + }); + } +} diff --git a/lib/chat_list/views/chat_single_contact_list_widget.dart b/lib/chat_list/views/chat_single_contact_list_widget.dart deleted file mode 100644 index 605ade0..0000000 --- a/lib/chat_list/views/chat_single_contact_list_widget.dart +++ /dev/null @@ -1,76 +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_bloc/flutter_bloc.dart'; -import 'package:flutter_translate/flutter_translate.dart'; -import 'package:searchable_listview/searchable_listview.dart'; - -import '../../contacts/contacts.dart'; -import '../../proto/proto.dart' as proto; -import '../../theme/theme.dart'; -import '../chat_list.dart'; - -class ChatSingleContactListWidget extends StatelessWidget { - const ChatSingleContactListWidget({super.key}); - - @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - final contactListV = context.watch().state; - - return contactListV.builder((context, contactList) { - final contactMap = IMap.fromIterable(contactList, - keyMapper: (c) => c.value.localConversationRecordKey, - valueMapper: (c) => c.value); - - 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( - initialList: chatList.map((x) => x.value).toList(), - itemBuilder: (c) { - final contact = - contactMap[c.localConversationRecordKey]; - if (contact == null) { - return const Text('...'); - } - return ChatSingleContactItemWidget( - contact: contact, - disabled: contactListV.busy) - .paddingLTRB(0, 4, 0, 0); - }, - filter: (value) { - final lowerValue = value.toLowerCase(); - return chatList.map((x) => x.value).where((c) { - final contact = - contactMap[c.localConversationRecordKey]; - if (contact == null) { - return false; - } - return contact.nickname - .toLowerCase() - .contains(lowerValue) || - contact.profile.name - .toLowerCase() - .contains(lowerValue) || - contact.profile.pronouns - .toLowerCase() - .contains(lowerValue); - }).toList(); - }, - spaceBetweenSearchAndList: 4, - inputDecoration: InputDecoration( - labelText: translate('chat_list.search'), - ), - ), - ).paddingAll(8)))) - .paddingLTRB(8, 0, 8, 8); - }); - } -} diff --git a/lib/chat_list/views/views.dart b/lib/chat_list/views/views.dart index 311d02e..1420794 100644 --- a/lib/chat_list/views/views.dart +++ b/lib/chat_list/views/views.dart @@ -1,3 +1,3 @@ +export 'chat_list_widget.dart'; 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/cubits/waiting_invitation_cubit.dart b/lib/contact_invitation/cubits/waiting_invitation_cubit.dart index a955978..47addc2 100644 --- a/lib/contact_invitation/cubits/waiting_invitation_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitation_cubit.dart @@ -59,8 +59,11 @@ class WaitingInvitationCubit extends AsyncTransformerCubit close() async { + await _singleInvitationStatusProcessor.unfollow(); + await super.close(); + } + Future _addWaitingInvitation( {required proto.ContactInvitationRecord contactInvitationRecord}) async => @@ -40,16 +54,60 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit _invitationStatusListener( + WaitingInvitationsBlocMapState newState) async { + for (final entry in newState.entries) { + final contactRequestInboxRecordKey = entry.key; + final invStatus = entry.value.asData?.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( + profile: acceptedContact.remoteProfile, + remoteSuperIdentity: acceptedContact.remoteIdentity, + remoteConversationRecordKey: + acceptedContact.remoteConversationRecordKey, + localConversationRecordKey: + acceptedContact.localConversationRecordKey, + ); + } else { + // Reject + await _contactInvitationListCubit.deleteInvitation( + accepted: false, + contactRequestInboxRecordKey: contactRequestInboxRecordKey); + } + } + } + /// StateFollower ///////////////////////// @override Future removeFromState(TypedKey key) => remove(key); @override - Future updateState(TypedKey key, proto.ContactInvitationRecord value) => - _addWaitingInvitation(contactInvitationRecord: value); + Future updateState( + TypedKey key, + proto.ContactInvitationRecord? oldValue, + proto.ContactInvitationRecord newValue) async { + await _addWaitingInvitation(contactInvitationRecord: newValue); + } //// final AccountInfo _accountInfo; final AccountRecordCubit _accountRecordCubit; + final ContactInvitationListCubit _contactInvitationListCubit; + final ContactListCubit _contactListCubit; + final _singleInvitationStatusProcessor = + SingleStateProcessor(); } diff --git a/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart b/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart index ae527e4..b983265 100644 --- a/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart +++ b/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart @@ -13,17 +13,27 @@ import '../conversation.dart'; @immutable class ActiveConversationState extends Equatable { const ActiveConversationState({ - required this.contact, + required this.remoteIdentityPublicKey, + required this.localConversationRecordKey, + required this.remoteConversationRecordKey, required this.localConversation, required this.remoteConversation, }); - final proto.Contact contact; + final TypedKey remoteIdentityPublicKey; + final TypedKey localConversationRecordKey; + final TypedKey remoteConversationRecordKey; final proto.Conversation localConversation; final proto.Conversation remoteConversation; @override - List get props => [contact, localConversation, remoteConversation]; + List get props => [ + remoteIdentityPublicKey, + localConversationRecordKey, + remoteConversationRecordKey, + localConversation, + remoteConversation + ]; } typedef ActiveConversationCubit = TransformerCubit< @@ -37,9 +47,11 @@ typedef ActiveConversationsBlocMapState // Map of localConversationRecordKey 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. +// +// TODO: Polling contacts for new inactive chats is yet to be done +// class ActiveConversationsBlocMapCubit extends BlocMapCubit, ActiveConversationCubit> with StateMapFollower { @@ -62,14 +74,11 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit _addConversation({required proto.Contact contact}) async => + Future _addDirectConversation( + {required TypedKey remoteIdentityPublicKey, + required TypedKey localConversationRecordKey, + required TypedKey remoteConversationRecordKey}) async => add(() { - final remoteIdentityPublicKey = contact.identityPublicKey.toVeilid(); - final localConversationRecordKey = - contact.localConversationRecordKey.toVeilid(); - final remoteConversationRecordKey = - contact.remoteConversationRecordKey.toVeilid(); - // Conversation cubit the tracks the state between the local // and remote halves of a contact's relationship with this account final conversationCubit = ConversationCubit( @@ -105,14 +114,16 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit removeFromState(TypedKey key) => remove(key); @override - Future updateState(TypedKey key, proto.Chat value) async { - final contactList = _contactListCubit.state.state.asData?.value; - if (contactList == null) { - await addState(key, const AsyncValue.loading()); - return; + Future updateState( + TypedKey key, proto.Chat? oldValue, proto.Chat newValue) async { + switch (newValue.whichKind()) { + case proto.Chat_Kind.notSet: + throw StateError('unknown chat kind'); + case proto.Chat_Kind.direct: + final localConversationRecordKey = + newValue.direct.localConversationRecordKey.toVeilid(); + final remoteIdentityPublicKey = + newValue.direct.remoteMember.remoteIdentityPublicKey.toVeilid(); + final remoteConversationRecordKey = + newValue.direct.remoteMember.remoteConversationRecordKey.toVeilid(); + + if (oldValue != null) { + final oldLocalConversationRecordKey = + oldValue.direct.localConversationRecordKey.toVeilid(); + final oldRemoteIdentityPublicKey = + oldValue.direct.remoteMember.remoteIdentityPublicKey.toVeilid(); + final oldRemoteConversationRecordKey = oldValue + .direct.remoteMember.remoteConversationRecordKey + .toVeilid(); + + if (oldLocalConversationRecordKey == localConversationRecordKey && + oldRemoteIdentityPublicKey == remoteIdentityPublicKey && + oldRemoteConversationRecordKey == remoteConversationRecordKey) { + return; + } + } + + await _addDirectConversation( + remoteIdentityPublicKey: remoteIdentityPublicKey, + localConversationRecordKey: localConversationRecordKey, + remoteConversationRecordKey: remoteConversationRecordKey); + + break; + case proto.Chat_Kind.group: + break; } - final contactIndex = contactList.indexWhere( - (c) => c.value.localConversationRecordKey.toVeilid() == key); - if (contactIndex == -1) { - await addState(key, AsyncValue.error('Contact not found')); - return; - } - final contact = contactList[contactIndex]; - await _addConversation(contact: contact.value); } //// diff --git a/lib/conversation/cubits/active_single_contact_chat_bloc_map_cubit.dart b/lib/conversation/cubits/active_single_contact_chat_bloc_map_cubit.dart index c1d170d..6c6d4b8 100644 --- a/lib/conversation/cubits/active_single_contact_chat_bloc_map_cubit.dart +++ b/lib/conversation/cubits/active_single_contact_chat_bloc_map_cubit.dart @@ -2,16 +2,42 @@ import 'dart:async'; import 'package:async_tools/async_tools.dart'; import 'package:bloc_advanced_tools/bloc_advanced_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 '../../chat/chat.dart'; -import '../../chat_list/cubits/chat_list_cubit.dart'; -import '../../contacts/contacts.dart'; import '../../proto/proto.dart' as proto; import '../conversation.dart'; import 'active_conversations_bloc_map_cubit.dart'; +@immutable +class _SingleContactChatState extends Equatable { + const _SingleContactChatState( + {required this.remoteIdentityPublicKey, + required this.localConversationRecordKey, + required this.remoteConversationRecordKey, + required this.localMessagesRecordKey, + required this.remoteMessagesRecordKey}); + + final TypedKey remoteIdentityPublicKey; + final TypedKey localConversationRecordKey; + final TypedKey remoteConversationRecordKey; + final TypedKey localMessagesRecordKey; + final TypedKey remoteMessagesRecordKey; + + @override + // TODO: implement props + List get props => [ + remoteIdentityPublicKey, + localConversationRecordKey, + remoteConversationRecordKey, + localMessagesRecordKey, + remoteMessagesRecordKey + ]; +} + // Map of localConversationRecordKey to MessagesCubit // Wraps a MessagesCubit to stream the latest messages to the state // Automatically follows the state of a ActiveConversationsBlocMapCubit. @@ -20,36 +46,42 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit> { - ActiveSingleContactChatBlocMapCubit( - {required AccountInfo accountInfo, - required ActiveConversationsBlocMapCubit activeConversationsBlocMapCubit, - required ContactListCubit contactListCubit, - required ChatListCubit chatListCubit}) - : _accountInfo = accountInfo, - _contactListCubit = contactListCubit, - _chatListCubit = chatListCubit { + ActiveSingleContactChatBlocMapCubit({ + required AccountInfo accountInfo, + required ActiveConversationsBlocMapCubit activeConversationsBlocMapCubit, + }) : _accountInfo = accountInfo { // Follow the active conversations bloc map cubit follow(activeConversationsBlocMapCubit); } - Future _addConversationMessages( - {required proto.Contact contact, - required proto.Chat chat, - required proto.Conversation localConversation, - required proto.Conversation remoteConversation}) async => + Future _addConversationMessages(_SingleContactChatState state) async => add(() => MapEntry( - contact.localConversationRecordKey.toVeilid(), + state.localConversationRecordKey, SingleContactMessagesCubit( accountInfo: _accountInfo, - remoteIdentityPublicKey: contact.identityPublicKey.toVeilid(), - localConversationRecordKey: - contact.localConversationRecordKey.toVeilid(), - remoteConversationRecordKey: - contact.remoteConversationRecordKey.toVeilid(), - localMessagesRecordKey: localConversation.messages.toVeilid(), - remoteMessagesRecordKey: remoteConversation.messages.toVeilid(), + remoteIdentityPublicKey: state.remoteIdentityPublicKey, + localConversationRecordKey: state.localConversationRecordKey, + remoteConversationRecordKey: state.remoteConversationRecordKey, + localMessagesRecordKey: state.localMessagesRecordKey, + remoteMessagesRecordKey: state.remoteMessagesRecordKey, ))); + _SingleContactChatState? _mapStateValue( + AsyncValue avInputState) { + final inputState = avInputState.asData?.value; + if (inputState == null) { + return null; + } + return _SingleContactChatState( + remoteIdentityPublicKey: inputState.remoteIdentityPublicKey, + localConversationRecordKey: inputState.localConversationRecordKey, + remoteConversationRecordKey: inputState.remoteConversationRecordKey, + localMessagesRecordKey: + inputState.localConversation.messages.toVeilid(), + remoteMessagesRecordKey: + inputState.remoteConversation.messages.toVeilid()); + } + /// StateFollower ///////////////////////// @override @@ -57,49 +89,27 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit updateState( - TypedKey key, AsyncValue value) async { - // Get the contact object for this single contact chat - final contactList = _contactListCubit.state.state.asData?.value; - if (contactList == null) { + TypedKey key, + AsyncValue? oldValue, + AsyncValue newValue) async { + final newState = _mapStateValue(newValue); + if (oldValue != null) { + final oldState = _mapStateValue(oldValue); + if (oldState == newState) { + return; + } + } + if (newState != null) { + await _addConversationMessages(newState); + } else if (newValue.isLoading) { await addState(key, const AsyncValue.loading()); - return; + } else { + final (error, stackTrace) = + (newValue.asError!.error, newValue.asError!.stackTrace); + await addState(key, AsyncValue.error(error, stackTrace)); } - final contactIndex = contactList.indexWhere( - (c) => c.value.localConversationRecordKey.toVeilid() == key); - if (contactIndex == -1) { - await addState( - key, AsyncValue.error('Contact not found for conversation')); - return; - } - final contact = contactList[contactIndex].value; - - // Get the chat object for this single contact chat - final chatList = _chatListCubit.state.state.asData?.value; - if (chatList == null) { - await addState(key, const AsyncValue.loading()); - return; - } - final chatIndex = chatList.indexWhere( - (c) => c.value.localConversationRecordKey.toVeilid() == key); - if (contactIndex == -1) { - await addState(key, AsyncValue.error('Chat not found for conversation')); - return; - } - final chat = chatList[chatIndex].value; - - 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 AccountInfo _accountInfo; - final ContactListCubit _contactListCubit; - final ChatListCubit _chatListCubit; } 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 bdea8e3..8811607 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 @@ -25,7 +25,7 @@ class ChatsPageState extends State { // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return Column(children: [ - const ChatSingleContactListWidget().expanded(), + const ChatListWidget().expanded(), ]); } } diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index 16d484d..8ade6f5 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -2,15 +2,12 @@ import 'dart:math'; import 'package:async_tools/async_tools.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart'; import 'package:provider/provider.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../chat/chat.dart'; -import '../../contact_invitation/contact_invitation.dart'; -import '../../contacts/contacts.dart'; import '../../theme/theme.dart'; import '../../tools/tools.dart'; import 'active_account_page_controller_wrapper.dart'; @@ -39,48 +36,6 @@ class HomeScreenState extends State { super.dispose(); } - // Process all accepted or rejected invitations - void _invitationStatusListener( - BuildContext context, WaitingInvitationsBlocMapState state) { - _singleInvitationStatusProcessor.updateState(state, (newState) async { - final contactListCubit = context.read(); - final contactInvitationListCubit = - context.read(); - - for (final entry in newState.entries) { - final contactRequestInboxRecordKey = entry.key; - final invStatus = entry.value.asData?.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( - profile: acceptedContact.remoteProfile, - remoteSuperIdentity: acceptedContact.remoteIdentity, - remoteConversationRecordKey: - acceptedContact.remoteConversationRecordKey, - localConversationRecordKey: - acceptedContact.localConversationRecordKey, - ); - } else { - // Reject - await contactInvitationListCubit.deleteInvitation( - accepted: false, - contactRequestInboxRecordKey: contactRequestInboxRecordKey); - } - } - }); - } - Widget _buildAccountReadyDeviceSpecific(BuildContext context) { final hasActiveChat = context.watch().state != null; if (responsiveVisibility( @@ -110,24 +65,18 @@ class HomeScreenState extends State { // Re-export all ready blocs to the account display subtree return perAccountCollectionState.provide( - child: MultiBlocListener(listeners: [ - BlocListener( - listener: _invitationStatusListener, - ) - ], child: Builder(builder: _buildAccountReadyDeviceSpecific))); + child: Builder(builder: _buildAccountReadyDeviceSpecific)); } } Widget _buildAccountPageView(BuildContext context) { final localAccounts = context.watch().state; - final activeLocalAccountCubit = - context.watch().state; + final activeLocalAccount = context.watch().state; final perAccountCollectionBlocMapState = context.watch().state; - final activeIndex = localAccounts.indexWhere( - (x) => x.superIdentity.recordKey == activeLocalAccountCubit); + final activeIndex = localAccounts + .indexWhere((x) => x.superIdentity.recordKey == activeLocalAccount); if (activeIndex == -1) { return const HomeNoActive(); } @@ -208,6 +157,4 @@ class HomeScreenState extends State { } final _zoomDrawerController = ZoomDrawerController(); - final _singleInvitationStatusProcessor = - SingleStateProcessor(); } diff --git a/lib/proto/extensions.dart b/lib/proto/extensions.dart index a5e212e..4491f89 100644 --- a/lib/proto/extensions.dart +++ b/lib/proto/extensions.dart @@ -34,3 +34,16 @@ extension ContactExt on proto.Contact { String get displayName => nickname.isNotEmpty ? '$nickname (${profile.name})' : profile.name; } + +extension ChatExt on proto.Chat { + TypedKey get localConversationRecordKey { + switch (whichKind()) { + case proto.Chat_Kind.direct: + return direct.localConversationRecordKey.toVeilid(); + case proto.Chat_Kind.group: + return group.localConversationRecordKey.toVeilid(); + case proto.Chat_Kind.notSet: + throw StateError('unknown chat kind'); + } + } +} diff --git a/lib/proto/veilidchat.pb.dart b/lib/proto/veilidchat.pb.dart index f2b43d9..63bd910 100644 --- a/lib/proto/veilidchat.pb.dart +++ b/lib/proto/veilidchat.pb.dart @@ -1255,16 +1255,15 @@ class Conversation extends $pb.GeneratedMessage { $0.TypedKey ensureMessages() => $_ensure(2); } -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); +class ChatMember extends $pb.GeneratedMessage { + factory ChatMember() => create(); + ChatMember._() : super(); + factory ChatMember.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory ChatMember.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) - ..aOM(1, _omitFieldNames ? '' : 'settings', subBuilder: ChatSettings.create) - ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create) - ..aOM<$0.TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create) + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ChatMember', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOM<$0.TypedKey>(1, _omitFieldNames ? '' : 'remoteIdentityPublicKey', subBuilder: $0.TypedKey.create) + ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create) ..hasRequiredFields = false ; @@ -1272,22 +1271,79 @@ class Chat extends $pb.GeneratedMessage { '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); + ChatMember clone() => ChatMember()..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; + ChatMember copyWith(void Function(ChatMember) updates) => super.copyWith((message) => updates(message as ChatMember)) as ChatMember; $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') - static Chat create() => Chat._(); - Chat createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); + static ChatMember create() => ChatMember._(); + ChatMember createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static Chat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static Chat? _defaultInstance; + static ChatMember getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ChatMember? _defaultInstance; + + @$pb.TagNumber(1) + $0.TypedKey get remoteIdentityPublicKey => $_getN(0); + @$pb.TagNumber(1) + set remoteIdentityPublicKey($0.TypedKey v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasRemoteIdentityPublicKey() => $_has(0); + @$pb.TagNumber(1) + void clearRemoteIdentityPublicKey() => clearField(1); + @$pb.TagNumber(1) + $0.TypedKey ensureRemoteIdentityPublicKey() => $_ensure(0); + + @$pb.TagNumber(2) + $0.TypedKey get remoteConversationRecordKey => $_getN(1); + @$pb.TagNumber(2) + set remoteConversationRecordKey($0.TypedKey v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasRemoteConversationRecordKey() => $_has(1); + @$pb.TagNumber(2) + void clearRemoteConversationRecordKey() => clearField(2); + @$pb.TagNumber(2) + $0.TypedKey ensureRemoteConversationRecordKey() => $_ensure(1); +} + +class DirectChat extends $pb.GeneratedMessage { + factory DirectChat() => create(); + DirectChat._() : super(); + factory DirectChat.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory DirectChat.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DirectChat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'settings', subBuilder: ChatSettings.create) + ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create) + ..aOM(3, _omitFieldNames ? '' : 'remoteMember', subBuilder: ChatMember.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') + DirectChat clone() => DirectChat()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + DirectChat copyWith(void Function(DirectChat) updates) => super.copyWith((message) => updates(message as DirectChat)) as DirectChat; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DirectChat create() => DirectChat._(); + DirectChat createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DirectChat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static DirectChat? _defaultInstance; @$pb.TagNumber(1) ChatSettings get settings => $_getN(0); @@ -1312,15 +1368,15 @@ class Chat extends $pb.GeneratedMessage { $0.TypedKey ensureLocalConversationRecordKey() => $_ensure(1); @$pb.TagNumber(3) - $0.TypedKey get remoteConversationRecordKey => $_getN(2); + ChatMember get remoteMember => $_getN(2); @$pb.TagNumber(3) - set remoteConversationRecordKey($0.TypedKey v) { setField(3, v); } + set remoteMember(ChatMember v) { setField(3, v); } @$pb.TagNumber(3) - $core.bool hasRemoteConversationRecordKey() => $_has(2); + $core.bool hasRemoteMember() => $_has(2); @$pb.TagNumber(3) - void clearRemoteConversationRecordKey() => clearField(3); + void clearRemoteMember() => clearField(3); @$pb.TagNumber(3) - $0.TypedKey ensureRemoteConversationRecordKey() => $_ensure(2); + ChatMember ensureRemoteMember() => $_ensure(2); } class GroupChat extends $pb.GeneratedMessage { @@ -1331,8 +1387,10 @@ class GroupChat extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'GroupChat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) ..aOM(1, _omitFieldNames ? '' : 'settings', subBuilder: ChatSettings.create) - ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create) - ..pc<$0.TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationRecordKeys', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create) + ..aOM(2, _omitFieldNames ? '' : 'membership', subBuilder: Membership.create) + ..aOM(3, _omitFieldNames ? '' : 'permissions', subBuilder: Permissions.create) + ..aOM<$0.TypedKey>(4, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create) + ..pc(5, _omitFieldNames ? '' : 'remoteMembers', $pb.PbFieldType.PM, subBuilder: ChatMember.create) ..hasRequiredFields = false ; @@ -1369,18 +1427,111 @@ class GroupChat extends $pb.GeneratedMessage { ChatSettings ensureSettings() => $_ensure(0); @$pb.TagNumber(2) - $0.TypedKey get localConversationRecordKey => $_getN(1); + Membership get membership => $_getN(1); @$pb.TagNumber(2) - set localConversationRecordKey($0.TypedKey v) { setField(2, v); } + set membership(Membership v) { setField(2, v); } @$pb.TagNumber(2) - $core.bool hasLocalConversationRecordKey() => $_has(1); + $core.bool hasMembership() => $_has(1); @$pb.TagNumber(2) - void clearLocalConversationRecordKey() => clearField(2); + void clearMembership() => clearField(2); @$pb.TagNumber(2) - $0.TypedKey ensureLocalConversationRecordKey() => $_ensure(1); + Membership ensureMembership() => $_ensure(1); @$pb.TagNumber(3) - $core.List<$0.TypedKey> get remoteConversationRecordKeys => $_getList(2); + Permissions get permissions => $_getN(2); + @$pb.TagNumber(3) + set permissions(Permissions v) { setField(3, v); } + @$pb.TagNumber(3) + $core.bool hasPermissions() => $_has(2); + @$pb.TagNumber(3) + void clearPermissions() => clearField(3); + @$pb.TagNumber(3) + Permissions ensurePermissions() => $_ensure(2); + + @$pb.TagNumber(4) + $0.TypedKey get localConversationRecordKey => $_getN(3); + @$pb.TagNumber(4) + set localConversationRecordKey($0.TypedKey v) { setField(4, v); } + @$pb.TagNumber(4) + $core.bool hasLocalConversationRecordKey() => $_has(3); + @$pb.TagNumber(4) + void clearLocalConversationRecordKey() => clearField(4); + @$pb.TagNumber(4) + $0.TypedKey ensureLocalConversationRecordKey() => $_ensure(3); + + @$pb.TagNumber(5) + $core.List get remoteMembers => $_getList(4); +} + +enum Chat_Kind { + direct, + group, + notSet +} + +class Chat extends $pb.GeneratedMessage { + factory Chat() => create(); + Chat._() : super(); + factory Chat.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Chat.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static const $core.Map<$core.int, Chat_Kind> _Chat_KindByTag = { + 1 : Chat_Kind.direct, + 2 : Chat_Kind.group, + 0 : Chat_Kind.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Chat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..oo(0, [1, 2]) + ..aOM(1, _omitFieldNames ? '' : 'direct', subBuilder: DirectChat.create) + ..aOM(2, _omitFieldNames ? '' : 'group', subBuilder: GroupChat.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Chat clone() => Chat()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Chat copyWith(void Function(Chat) updates) => super.copyWith((message) => updates(message as Chat)) as Chat; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Chat create() => Chat._(); + Chat createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Chat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Chat? _defaultInstance; + + Chat_Kind whichKind() => _Chat_KindByTag[$_whichOneof(0)]!; + void clearKind() => clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + DirectChat get direct => $_getN(0); + @$pb.TagNumber(1) + set direct(DirectChat v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasDirect() => $_has(0); + @$pb.TagNumber(1) + void clearDirect() => clearField(1); + @$pb.TagNumber(1) + DirectChat ensureDirect() => $_ensure(0); + + @$pb.TagNumber(2) + GroupChat get group => $_getN(1); + @$pb.TagNumber(2) + set group(GroupChat v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasGroup() => $_has(1); + @$pb.TagNumber(2) + void clearGroup() => clearField(2); + @$pb.TagNumber(2) + GroupChat ensureGroup() => $_ensure(1); } class Profile extends $pb.GeneratedMessage { diff --git a/lib/proto/veilidchat.pbjson.dart b/lib/proto/veilidchat.pbjson.dart index d59b75c..fe6cac3 100644 --- a/lib/proto/veilidchat.pbjson.dart +++ b/lib/proto/veilidchat.pbjson.dart @@ -365,41 +365,76 @@ final $typed_data.Uint8List conversationDescriptor = $convert.base64Decode( 'JvZmlsZRIuChNzdXBlcl9pZGVudGl0eV9qc29uGAIgASgJUhFzdXBlcklkZW50aXR5SnNvbhIs' 'CghtZXNzYWdlcxgDIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIIbWVzc2FnZXM='); -@$core.Deprecated('Use chatDescriptor instead') -const Chat$json = { - '1': 'Chat', +@$core.Deprecated('Use chatMemberDescriptor instead') +const ChatMember$json = { + '1': 'ChatMember', '2': [ - {'1': 'settings', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.ChatSettings', '10': 'settings'}, - {'1': 'local_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'}, - {'1': 'remote_conversation_record_key', '3': 3, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'}, + {'1': 'remote_identity_public_key', '3': 1, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteIdentityPublicKey'}, + {'1': 'remote_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'}, ], }; -/// Descriptor for `Chat`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List chatDescriptor = $convert.base64Decode( - 'CgRDaGF0EjQKCHNldHRpbmdzGAEgASgLMhgudmVpbGlkY2hhdC5DaGF0U2V0dGluZ3NSCHNldH' - 'RpbmdzElMKHWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAIgASgLMhAudmVpbGlkLlR5' - 'cGVkS2V5Uhpsb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRJVCh5yZW1vdGVfY29udmVyc2F0aW' - '9uX3JlY29yZF9rZXkYAyABKAsyEC52ZWlsaWQuVHlwZWRLZXlSG3JlbW90ZUNvbnZlcnNhdGlv' - 'blJlY29yZEtleQ=='); +/// Descriptor for `ChatMember`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List chatMemberDescriptor = $convert.base64Decode( + 'CgpDaGF0TWVtYmVyEk0KGnJlbW90ZV9pZGVudGl0eV9wdWJsaWNfa2V5GAEgASgLMhAudmVpbG' + 'lkLlR5cGVkS2V5UhdyZW1vdGVJZGVudGl0eVB1YmxpY0tleRJVCh5yZW1vdGVfY29udmVyc2F0' + 'aW9uX3JlY29yZF9rZXkYAiABKAsyEC52ZWlsaWQuVHlwZWRLZXlSG3JlbW90ZUNvbnZlcnNhdG' + 'lvblJlY29yZEtleQ=='); + +@$core.Deprecated('Use directChatDescriptor instead') +const DirectChat$json = { + '1': 'DirectChat', + '2': [ + {'1': 'settings', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.ChatSettings', '10': 'settings'}, + {'1': 'local_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'}, + {'1': 'remote_member', '3': 3, '4': 1, '5': 11, '6': '.veilidchat.ChatMember', '10': 'remoteMember'}, + ], +}; + +/// Descriptor for `DirectChat`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List directChatDescriptor = $convert.base64Decode( + 'CgpEaXJlY3RDaGF0EjQKCHNldHRpbmdzGAEgASgLMhgudmVpbGlkY2hhdC5DaGF0U2V0dGluZ3' + 'NSCHNldHRpbmdzElMKHWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAIgASgLMhAudmVp' + 'bGlkLlR5cGVkS2V5Uhpsb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRI7Cg1yZW1vdGVfbWVtYm' + 'VyGAMgASgLMhYudmVpbGlkY2hhdC5DaGF0TWVtYmVyUgxyZW1vdGVNZW1iZXI='); @$core.Deprecated('Use groupChatDescriptor instead') const GroupChat$json = { '1': 'GroupChat', '2': [ {'1': 'settings', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.ChatSettings', '10': 'settings'}, - {'1': 'local_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'}, - {'1': 'remote_conversation_record_keys', '3': 3, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKeys'}, + {'1': 'membership', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.Membership', '10': 'membership'}, + {'1': 'permissions', '3': 3, '4': 1, '5': 11, '6': '.veilidchat.Permissions', '10': 'permissions'}, + {'1': 'local_conversation_record_key', '3': 4, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'}, + {'1': 'remote_members', '3': 5, '4': 3, '5': 11, '6': '.veilidchat.ChatMember', '10': 'remoteMembers'}, ], }; /// Descriptor for `GroupChat`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List groupChatDescriptor = $convert.base64Decode( 'CglHcm91cENoYXQSNAoIc2V0dGluZ3MYASABKAsyGC52ZWlsaWRjaGF0LkNoYXRTZXR0aW5nc1' - 'IIc2V0dGluZ3MSUwodbG9jYWxfY29udmVyc2F0aW9uX3JlY29yZF9rZXkYAiABKAsyEC52ZWls' - 'aWQuVHlwZWRLZXlSGmxvY2FsQ29udmVyc2F0aW9uUmVjb3JkS2V5ElcKH3JlbW90ZV9jb252ZX' - 'JzYXRpb25fcmVjb3JkX2tleXMYAyADKAsyEC52ZWlsaWQuVHlwZWRLZXlSHHJlbW90ZUNvbnZl' - 'cnNhdGlvblJlY29yZEtleXM='); + 'IIc2V0dGluZ3MSNgoKbWVtYmVyc2hpcBgCIAEoCzIWLnZlaWxpZGNoYXQuTWVtYmVyc2hpcFIK' + 'bWVtYmVyc2hpcBI5CgtwZXJtaXNzaW9ucxgDIAEoCzIXLnZlaWxpZGNoYXQuUGVybWlzc2lvbn' + 'NSC3Blcm1pc3Npb25zElMKHWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAQgASgLMhAu' + 'dmVpbGlkLlR5cGVkS2V5Uhpsb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRI9Cg5yZW1vdGVfbW' + 'VtYmVycxgFIAMoCzIWLnZlaWxpZGNoYXQuQ2hhdE1lbWJlclINcmVtb3RlTWVtYmVycw=='); + +@$core.Deprecated('Use chatDescriptor instead') +const Chat$json = { + '1': 'Chat', + '2': [ + {'1': 'direct', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.DirectChat', '9': 0, '10': 'direct'}, + {'1': 'group', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.GroupChat', '9': 0, '10': 'group'}, + ], + '8': [ + {'1': 'kind'}, + ], +}; + +/// Descriptor for `Chat`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List chatDescriptor = $convert.base64Decode( + 'CgRDaGF0EjAKBmRpcmVjdBgBIAEoCzIWLnZlaWxpZGNoYXQuRGlyZWN0Q2hhdEgAUgZkaXJlY3' + 'QSLQoFZ3JvdXAYAiABKAsyFS52ZWlsaWRjaGF0Lkdyb3VwQ2hhdEgAUgVncm91cEIGCgRraW5k'); @$core.Deprecated('Use profileDescriptor instead') const Profile$json = { diff --git a/lib/proto/veilidchat.proto b/lib/proto/veilidchat.proto index 0c00db7..794cef8 100644 --- a/lib/proto/veilidchat.proto +++ b/lib/proto/veilidchat.proto @@ -267,15 +267,23 @@ message Conversation { veilid.TypedKey messages = 3; } -// Either a 1-1 conversation or a group chat +// A member of chat which may or may not be associated with a contact +message ChatMember { + // The identity public key most recently associated with the chat member + veilid.TypedKey remote_identity_public_key = 1; + // Conversation key for the other party + veilid.TypedKey remote_conversation_record_key = 2; +} + +// A 1-1 chat // Privately encrypted, this is the local user's copy of the chat -message Chat { +message DirectChat { // Settings ChatSettings settings = 1; // Conversation key for this user veilid.TypedKey local_conversation_record_key = 2; // Conversation key for the other party - veilid.TypedKey remote_conversation_record_key = 3; + ChatMember remote_member = 3; } // A group chat @@ -283,10 +291,22 @@ message Chat { message GroupChat { // Settings ChatSettings settings = 1; + // Membership + Membership membership = 2; + // Permissions + Permissions permissions = 3; // Conversation key for this user - veilid.TypedKey local_conversation_record_key = 2; + veilid.TypedKey local_conversation_record_key = 4; // Conversation keys for the other parties - repeated veilid.TypedKey remote_conversation_record_keys = 3; + repeated ChatMember remote_members = 5; +} + +// Some kind of chat +message Chat { + oneof kind { + DirectChat direct = 1; + GroupChat group = 2; + } } //////////////////////////////////////////////////////////////////////////////////// diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index 5d59190..ca0074f 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -609,7 +609,7 @@ class _DHTLogSpine { // Don't watch for local changes because this class already handles // notifying listeners and knows when it makes local changes _subscription ??= - await _spineRecord.listen(localChanges: false, _onSpineChanged); + await _spineRecord.listen(localChanges: true, _onSpineChanged); } on Exception { // If anything fails, try to cancel the watches await cancelWatch(); diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart index a333160..5ea6761 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart @@ -12,14 +12,6 @@ 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()); - static InitialStateFunction _makeInitialStateFunction( T Function(List data) decodeState) => (record) async { 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 1cfcfcd..54d1dec 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 @@ -29,20 +29,6 @@ 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); - // }); - // } - Future _init( InitialStateFunction initialStateFunction, StateFunction stateFunction, From c42736ce24a9e9697914a909a4e248ae287f931f Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 20 Jun 2024 21:38:59 -0400 Subject: [PATCH 147/270] fix titles --- lib/chat/cubits/chat_component_cubit.dart | 64 ++++++++++++------- .../cubits/conversation_cubit.dart | 2 + 2 files changed, 43 insertions(+), 23 deletions(-) diff --git a/lib/chat/cubits/chat_component_cubit.dart b/lib/chat/cubits/chat_component_cubit.dart index cd0f724..83e3d21 100644 --- a/lib/chat/cubits/chat_component_cubit.dart +++ b/lib/chat/cubits/chat_component_cubit.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:async_tools/async_tools.dart'; +import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/widgets.dart'; @@ -24,6 +25,7 @@ const metadataKeyIdentityPublicKey = 'identityPublicKey'; const metadataKeyExpirationDuration = 'expiration'; const metadataKeyViewLimit = 'view_limit'; const metadataKeyAttachments = 'attachments'; +const _sfChangedContacts = 'changedContacts'; class ChatComponentCubit extends Cubit { ChatComponentCubit._({ @@ -80,11 +82,17 @@ class ChatComponentCubit extends Cubit { // Subscribe to messages _messagesSubscription = _messagesCubit.stream.listen(_onChangedMessages); _onChangedMessages(_messagesCubit.state); + + // Subscribe to contact list changes + _contactListSubscription = + _contactListCubit.stream.listen(_onChangedContacts); + _onChangedContacts(_contactListCubit.state); } @override Future close() async { await _initWait(); + await _contactListSubscription.cancel(); await _accountRecordSubscription.cancel(); await _messagesSubscription.cancel(); await _conversationSubscriptions.values.map((v) => v.cancel()).wait; @@ -170,6 +178,13 @@ class ChatComponentCubit extends Cubit { emit(_convertMessages(state, avMessagesState)); } + void _onChangedContacts( + BlocBusyState>>> + bavContacts) { + // Rewrite users when contacts change + singleFuture((this, _sfChangedContacts), _updateConversationSubscriptions); + } + void _onChangedConversation( TypedKey remoteIdentityPublicKey, AsyncValue avConversationState, @@ -180,11 +195,23 @@ class ChatComponentCubit extends Cubit { // Don't change user information on loading state return; } - emit(state.copyWith( + emit(_updateTitle(state.copyWith( remoteUsers: state.remoteUsers.add( remoteIdentityPublicKey, _convertRemoteUser( - remoteIdentityPublicKey, activeConversationState)))); + remoteIdentityPublicKey, activeConversationState))))); + } + + static ChatComponentState _updateTitle(ChatComponentState currentState) { + if (currentState.remoteUsers.length == 0) { + return currentState.copyWith(title: 'Empty Chat'); + } + if (currentState.remoteUsers.length == 1) { + final remoteUser = currentState.remoteUsers.values.first; + return currentState.copyWith(title: remoteUser.firstName ?? ''); + } + return currentState.copyWith( + title: ''); } types.User _convertRemoteUser(TypedKey remoteIdentityPublicKey, @@ -227,18 +254,18 @@ class ChatComponentCubit extends Cubit { // If the cubit is already being listened to we have nothing to do if (existing.remove(remoteIdentityPublicKey)) { - continue; + // If the cubit is not already being listened to we should do that + _conversationSubscriptions[remoteIdentityPublicKey] = cc.stream.listen( + (avConv) => + _onChangedConversation(remoteIdentityPublicKey, avConv)); } - // If the cubit is not already being listened to we should do that - _conversationSubscriptions[remoteIdentityPublicKey] = cc.stream.listen( - (avConv) => _onChangedConversation(remoteIdentityPublicKey, avConv)); final activeConversationState = cc.state.asData?.value; if (activeConversationState != null) { - final remoteUser = _convertRemoteUser( - remoteIdentityPublicKey, activeConversationState); - currentRemoteUsersState = - currentRemoteUsersState.add(remoteIdentityPublicKey, remoteUser); + currentRemoteUsersState = currentRemoteUsersState.add( + remoteIdentityPublicKey, + _convertRemoteUser( + remoteIdentityPublicKey, activeConversationState)); } } // Purge remote users we didn't see in the cubit list any more @@ -253,18 +280,6 @@ class ChatComponentCubit extends Cubit { emit(_updateTitle(state.copyWith(remoteUsers: currentRemoteUsersState))); } - ChatComponentState _updateTitle(ChatComponentState currentState) { - if (currentState.remoteUsers.length == 0) { - return currentState.copyWith(title: 'Empty Chat'); - } - if (currentState.remoteUsers.length == 1) { - final remoteUser = currentState.remoteUsers.values.first; - return currentState.copyWith(title: remoteUser.firstName ?? ''); - } - return currentState.copyWith( - title: ''); - } - (ChatComponentState, types.Message?) _messageStateToChatMessage( ChatComponentState currentState, MessageState message) { final authorIdentityPublicKey = message.content.author.toVeilid(); @@ -417,6 +432,9 @@ class ChatComponentCubit extends Cubit { final Map>> _conversationSubscriptions = {}; late StreamSubscription _messagesSubscription; - + late StreamSubscription< + BlocBusyState< + AsyncValue>>>> + _contactListSubscription; double scrollOffset = 0; } diff --git a/lib/conversation/cubits/conversation_cubit.dart b/lib/conversation/cubits/conversation_cubit.dart index 45bc251..728489f 100644 --- a/lib/conversation/cubits/conversation_cubit.dart +++ b/lib/conversation/cubits/conversation_cubit.dart @@ -320,6 +320,7 @@ class ConversationCubit extends Cubit> { open: open, decodeState: proto.Conversation.fromBuffer); _localSubscription = _localConversationCubit!.stream.listen(_updateLocalConversationState); + _updateLocalConversationState(_localConversationCubit!.state); } // Open remote converation key @@ -330,6 +331,7 @@ class ConversationCubit extends Cubit> { open: open, decodeState: proto.Conversation.fromBuffer); _remoteSubscription = _remoteConversationCubit!.stream.listen(_updateRemoteConversationState); + _updateRemoteConversationState(_remoteConversationCubit!.state); } // Initialize local messages From adaa2951c20c79e2f595d55261fc426463df5a60 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 20 Jun 2024 23:00:10 -0400 Subject: [PATCH 148/270] more cleanup --- lib/chat/cubits/single_contact_messages_cubit.dart | 13 ++++++++++--- .../cubits/contact_invitation_list_cubit.dart | 7 ++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 397ac39..515ed03 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -4,6 +4,7 @@ import 'package:async_tools/async_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:protobuf/protobuf.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; @@ -302,8 +303,8 @@ class SingleContactMessagesCubit extends Cubit { _reconciledMessagesCubit?.state.state.asData?.value; // Get all sent messages final sentMessages = _sentMessagesCubit?.state.state.asData?.value; - // Get all items in the unsent queue - // final unsentMessages = _unsentMessagesQueue.queue; + //Get all items in the unsent queue + final unsentMessages = _unsentMessagesQueue.queue; // If we aren't ready to render a state, say we're loading if (reconciledMessages == null || sentMessages == null) { @@ -315,7 +316,7 @@ class SingleContactMessagesCubit extends Cubit { // final reconciledMessagesMap = // IMap.fromValues( // keyMapper: (x) => x.content.authorUniqueIdString, - // values: reconciledMessages.elements, + // values: reconciledMessages.windowElements, // ); final sentMessagesMap = IMap>.fromValues( @@ -346,6 +347,12 @@ class SingleContactMessagesCubit extends Cubit { sentOffline: sentOffline, )); } + for (final m in unsentMessages) { + renderedElements.add(RenderStateElement( + message: (m.deepCopy())..id = m.timestamp.toBytes(), + isLocal: true, + )); + } // Render the state final messages = renderedElements diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index 6a850e4..cac3d6c 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -232,7 +232,12 @@ 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.state.asData!.value.indexWhere((cir) => + final contactInvitationList = state.state.asData?.value; + if (contactInvitationList == null) { + return null; + } + + final isSelf = contactInvitationList.indexWhere((cir) => cir.value.contactRequestInbox.recordKey.toVeilid() == contactRequestInboxKey) != -1; From 7b400ed08bde219f739c346cbdac2f36cd02f50f Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 20 Jun 2024 23:26:55 -0400 Subject: [PATCH 149/270] watch anomaly --- .../lib/dht_support/src/dht_record/dht_record_pool.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 8b65d41..3efd8a7 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 @@ -673,7 +673,7 @@ class DHTRecordPool with TableDBBackedJson { /// Handle the DHT record updates coming from Veilid void processRemoteValueChange(VeilidUpdateValueChange updateValueChange) { - if (updateValueChange.subkeys.isNotEmpty) { + if (updateValueChange.subkeys.isNotEmpty && updateValueChange.count != 0) { // Change for (final kv in _opened.entries) { if (kv.key == updateValueChange.key) { @@ -691,7 +691,7 @@ class DHTRecordPool with TableDBBackedJson { final openedRecordInfo = entry.value; if (openedKey == updateValueChange.key) { - // Renew watch state for each opened recrod + // Renew watch state for each opened record 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 From 152c8bdff439b47cf1752888cf113304c7bdfa24 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 21 Jun 2024 22:44:35 -0400 Subject: [PATCH 150/270] ui cleanup --- .../cubits/single_contact_messages_cubit.dart | 44 +- lib/chat_list/cubits/chat_list_cubit.dart | 73 +- .../cubits/contact_invitation_list_cubit.dart | 3 +- .../views/contact_invitation_display.dart | 8 +- lib/contacts/cubits/contact_list_cubit.dart | 27 +- lib/contacts/views/contact_item_widget.dart | 8 +- ...ve_single_contact_chat_bloc_map_cubit.dart | 1 - .../cubits/conversation_cubit.dart | 54 +- lib/layout/home/drawer_menu/drawer_menu.dart | 38 +- lib/theme/models/scale_scheme.dart | 4 +- lib/theme/views/widget_helpers.dart | 75 +- .../src/dht_record/dht_record.dart | 16 +- .../src/dht_record/dht_record_pool.dart | 1168 ++++++++--------- .../dht_record/dht_record_pool_private.dart | 77 ++ .../lib/src/persistent_queue.dart | 3 +- 15 files changed, 827 insertions(+), 772 deletions(-) create mode 100644 packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool_private.dart diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 515ed03..9854535 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -4,7 +4,6 @@ import 'package:async_tools/async_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:protobuf/protobuf.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; @@ -82,6 +81,16 @@ class SingleContactMessagesCubit extends Cubit { await _sentMessagesCubit?.close(); await _rcvdMessagesCubit?.close(); await _reconciledMessagesCubit?.close(); + + // If the local conversation record is gone, then delete the reconciled + // messages table as well + final conversationDead = await DHTRecordPool.instance + .isDeletedRecordKey(_localConversationRecordKey); + if (conversationDead) { + await SingleContactMessagesCubit.cleanupAndDeleteMessages( + localConversationRecordKey: _localConversationRecordKey); + } + await super.close(); } @@ -292,8 +301,14 @@ class SingleContactMessagesCubit extends Cubit { previousMessage = message; } + // _sendingMessages = messages; + + // _renderState(); + await _sentMessagesCubit!.operateAppendEventual((writer) => writer.addAll(messages.map((m) => m.writeToBuffer()).toList())); + + // _sendingMessages = const IList.empty(); } // Produce a state for this cubit from the input cubits and queues @@ -304,7 +319,7 @@ class SingleContactMessagesCubit extends Cubit { // Get all sent messages final sentMessages = _sentMessagesCubit?.state.state.asData?.value; //Get all items in the unsent queue - final unsentMessages = _unsentMessagesQueue.queue; + //final unsentMessages = _unsentMessagesQueue.queue; // If we aren't ready to render a state, say we're loading if (reconciledMessages == null || sentMessages == null) { @@ -329,7 +344,7 @@ class SingleContactMessagesCubit extends Cubit { // ); final renderedElements = []; - + final renderedIds = {}; for (final m in reconciledMessages.windowElements) { final isLocal = m.content.author.toVeilid() == _accountInfo.identityTypedPublicKey; @@ -346,13 +361,22 @@ class SingleContactMessagesCubit extends Cubit { sent: sent, sentOffline: sentOffline, )); + + renderedIds.add(m.content.authorUniqueIdString); } - for (final m in unsentMessages) { - renderedElements.add(RenderStateElement( - message: (m.deepCopy())..id = m.timestamp.toBytes(), - isLocal: true, - )); - } + + // Render in-flight messages at the bottom + // for (final m in _sendingMessages) { + // if (renderedIds.contains(m.authorUniqueIdString)) { + // continue; + // } + // renderedElements.add(RenderStateElement( + // message: m, + // isLocal: true, + // sent: true, + // sentOffline: true, + // )); + // } // Render the state final messages = renderedElements @@ -426,7 +450,7 @@ class SingleContactMessagesCubit extends Cubit { late final MessageReconciliation _reconciliation; late final PersistentQueue _unsentMessagesQueue; - + // IList _sendingMessages = const IList.empty(); StreamSubscription>? _sentSubscription; StreamSubscription>? _rcvdSubscription; StreamSubscription>? diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index fa262f6..6bb88c1 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -8,7 +8,6 @@ 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'; ////////////////////////////////////////////////// @@ -58,9 +57,20 @@ class ChatListCubit extends DHTShortArrayCubit final remoteConversationRecordKey = contact.remoteConversationRecordKey.toVeilid(); + // Create 1:1 conversation type Chat + final chatMember = proto.ChatMember() + ..remoteIdentityPublicKey = remoteIdentityPublicKey.toProto() + ..remoteConversationRecordKey = remoteConversationRecordKey.toProto(); + + final directChat = proto.DirectChat() + ..settings = await getDefaultChatSettings(contact) + ..localConversationRecordKey = localConversationRecordKey.toProto() + ..remoteMember = chatMember; + + final chat = proto.Chat()..direct = directChat; + // Add Chat to account's list - // if this fails, don't keep retrying, user can try again later - await operateWrite((writer) async { + await operateWriteEventual((writer) async { // See if we have added this chat already for (var i = 0; i < writer.length; i++) { final cbuf = await writer.get(i); @@ -89,18 +99,6 @@ class ChatListCubit extends DHTShortArrayCubit } } - // Create 1:1 conversation type Chat - final chatMember = proto.ChatMember() - ..remoteIdentityPublicKey = remoteIdentityPublicKey.toProto() - ..remoteConversationRecordKey = remoteConversationRecordKey.toProto(); - - final directChat = proto.DirectChat() - ..settings = await getDefaultChatSettings(contact) - ..localConversationRecordKey = localConversationRecordKey.toProto() - ..remoteMember = chatMember; - - final chat = proto.Chat()..direct = directChat; - // Add chat await writer.add(chat.writeToBuffer()); }); @@ -110,37 +108,22 @@ class ChatListCubit extends DHTShortArrayCubit Future deleteChat( {required TypedKey localConversationRecordKey}) async { // Remove Chat from account's list - // if this fails, don't keep retrying, user can try again later - final deletedItem = - // Ensure followers get their changes before we return - await syncFollowers(() => operateWrite((writer) async { - if (_activeChatCubit.state == localConversationRecordKey) { - _activeChatCubit.setActiveChat(null); - } - for (var i = 0; i < writer.length; i++) { - final c = await writer.getProtobuf(proto.Chat.fromBuffer, i); - if (c == null) { - throw Exception('Failed to get chat'); - } - - if (c.localConversationRecordKey == - localConversationRecordKey) { - await writer.remove(i); - return c; - } - } - return null; - })); - // Since followers are synced, we can safetly remove the reconciled - // chat record now - if (deletedItem != null) { - try { - await SingleContactMessagesCubit.cleanupAndDeleteMessages( - localConversationRecordKey: localConversationRecordKey); - } on Exception catch (e) { - log.debug('error removing reconciled chat table: $e', e); + await operateWriteEventual((writer) async { + if (_activeChatCubit.state == localConversationRecordKey) { + _activeChatCubit.setActiveChat(null); } - } + for (var i = 0; i < writer.length; i++) { + final c = await writer.getProtobuf(proto.Chat.fromBuffer, i); + if (c == null) { + throw Exception('Failed to get chat'); + } + + if (c.localConversationRecordKey == localConversationRecordKey) { + await writer.remove(i); + return; + } + } + }); } /// StateMapFollowable ///////////////////////// diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index cac3d6c..9bd589e 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -152,8 +152,7 @@ class ContactInvitationListCubit ..message = message; // Add ContactInvitationRecord to account's list - // if this fails, don't keep retrying, user can try again later - await operateWrite((writer) async { + await operateWriteEventual((writer) async { await writer.add(cinvrec.writeToBuffer()); }); }); diff --git a/lib/contact_invitation/views/contact_invitation_display.dart b/lib/contact_invitation/views/contact_invitation_display.dart index 374a309..2e4acad 100644 --- a/lib/contact_invitation/views/contact_invitation_display.dart +++ b/lib/contact_invitation/views/contact_invitation_display.dart @@ -46,7 +46,6 @@ class ContactInvitationDisplayDialog extends StatelessWidget { // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { final theme = Theme.of(context); - //final scale = theme.extension()!; final textTheme = theme.textTheme; final signedContactInvitationBytesV = @@ -58,6 +57,9 @@ class ContactInvitationDisplayDialog extends StatelessWidget { return PopControl( dismissible: !signedContactInvitationBytesV.isLoading, child: Dialog( + shape: RoundedRectangleBorder( + side: const BorderSide(width: 2), + borderRadius: BorderRadius.circular(16)), backgroundColor: Colors.white, child: ConstrainedBox( constraints: BoxConstraints( @@ -90,6 +92,10 @@ class ContactInvitationDisplayDialog extends StatelessWidget { .paddingAll(8), ElevatedButton.icon( icon: const Icon(Icons.copy), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.black, + backgroundColor: Colors.white, + side: const BorderSide()), label: Text(translate( 'create_invitation_dialog.copy_invitation')), onPressed: () async { diff --git a/lib/contacts/cubits/contact_list_cubit.dart b/lib/contacts/cubits/contact_list_cubit.dart index 47433c7..aaecca4 100644 --- a/lib/contacts/cubits/contact_list_cubit.dart +++ b/lib/contacts/cubits/contact_list_cubit.dart @@ -6,7 +6,6 @@ import 'package:protobuf/protobuf.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; -import '../../conversation/conversation.dart'; import '../../proto/proto.dart' as proto; import '../../tools/tools.dart'; @@ -17,8 +16,7 @@ class ContactListCubit extends DHTShortArrayCubit { ContactListCubit({ required AccountInfo accountInfo, required OwnedDHTRecordPointer contactListRecordPointer, - }) : _accountInfo = accountInfo, - super( + }) : super( open: () => _open(accountInfo.accountRecordKey, contactListRecordPointer), decodeElement: proto.Contact.fromBuffer); @@ -98,8 +96,7 @@ class ContactListCubit extends DHTShortArrayCubit { ..showAvailability = false; // Add Contact to account's list - // if this fails, don't keep retrying, user can try again later - await operateWrite((writer) async { + await operateWriteEventual((writer) async { await writer.add(contact.writeToBuffer()); }); } @@ -107,7 +104,7 @@ class ContactListCubit extends DHTShortArrayCubit { Future deleteContact( {required TypedKey localConversationRecordKey}) async { // Remove Contact from account's list - final deletedItem = await operateWrite((writer) async { + final deletedItem = await operateWriteEventual((writer) async { for (var i = 0; i < writer.length; i++) { final item = await writer.getProtobuf(proto.Contact.fromBuffer, i); if (item == null) { @@ -124,18 +121,11 @@ class ContactListCubit extends DHTShortArrayCubit { if (deletedItem != null) { try { - // Make a conversation cubit to manipulate the conversation - final conversationCubit = ConversationCubit( - accountInfo: _accountInfo, - remoteIdentityPublicKey: deletedItem.identityPublicKey.toVeilid(), - localConversationRecordKey: - deletedItem.localConversationRecordKey.toVeilid(), - remoteConversationRecordKey: - deletedItem.remoteConversationRecordKey.toVeilid(), - ); - - // Delete the local and remote conversation records - await conversationCubit.delete(); + // Mark the conversation records for deletion + await DHTRecordPool.instance + .deleteRecord(deletedItem.localConversationRecordKey.toVeilid()); + await DHTRecordPool.instance + .deleteRecord(deletedItem.remoteConversationRecordKey.toVeilid()); } on Exception catch (e) { log.debug('error deleting conversation records: $e', e); } @@ -144,5 +134,4 @@ class ContactListCubit extends DHTShortArrayCubit { final _contactProfileUpdateMap = SingleStateProcessorMap(); - final AccountInfo _accountInfo; } diff --git a/lib/contacts/views/contact_item_widget.dart b/lib/contacts/views/contact_item_widget.dart index a7c2836..a7441e9 100644 --- a/lib/contacts/views/contact_item_widget.dart +++ b/lib/contacts/views/contact_item_widget.dart @@ -74,13 +74,13 @@ class ContactItemWidget extends StatelessWidget { final contactListCubit = context.read(); final chatListCubit = context.read(); - // Remove any chats for this contact - await chatListCubit.deleteChat( - localConversationRecordKey: localConversationRecordKey); - // Delete the contact itself await contactListCubit.deleteContact( localConversationRecordKey: localConversationRecordKey); + + // Remove any chats for this contact + await chatListCubit.deleteChat( + localConversationRecordKey: localConversationRecordKey); }) ], ); diff --git a/lib/conversation/cubits/active_single_contact_chat_bloc_map_cubit.dart b/lib/conversation/cubits/active_single_contact_chat_bloc_map_cubit.dart index 6c6d4b8..88860c4 100644 --- a/lib/conversation/cubits/active_single_contact_chat_bloc_map_cubit.dart +++ b/lib/conversation/cubits/active_single_contact_chat_bloc_map_cubit.dart @@ -28,7 +28,6 @@ class _SingleContactChatState extends Equatable { final TypedKey remoteMessagesRecordKey; @override - // TODO: implement props List get props => [ remoteIdentityPublicKey, localConversationRecordKey, diff --git a/lib/conversation/cubits/conversation_cubit.dart b/lib/conversation/cubits/conversation_cubit.dart index 728489f..1947504 100644 --- a/lib/conversation/cubits/conversation_cubit.dart +++ b/lib/conversation/cubits/conversation_cubit.dart @@ -14,7 +14,6 @@ import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../proto/proto.dart' as proto; -import '../../tools/tools.dart'; const _sfUpdateAccountChange = 'updateAccountChange'; @@ -116,7 +115,7 @@ class ConversationCubit extends Cubit> { final accountRecordKey = _accountInfo.accountRecordKey; final writer = _accountInfo.identityWriter; - // Open with SMPL scheme for identity writer + // Open with SMPL schema for identity writer late final DHTRecord localConversationRecord; if (existingConversationRecordKey != null) { localConversationRecord = await pool.openRecordWrite( @@ -171,57 +170,6 @@ class ConversationCubit extends Cubit> { return out; } - /// Delete the conversation keys associated with this conversation - Future delete() async { - final pool = DHTRecordPool.instance; - - await _initWait(); - final localConversationCubit = _localConversationCubit; - final remoteConversationCubit = _remoteConversationCubit; - - final deleteSet = DelayedWaitSet(); - - if (localConversationCubit != null) { - final data = localConversationCubit.state.asData; - if (data == null) { - log.warning('could not delete local conversation'); - return false; - } - - deleteSet.add(() async { - _localConversationCubit = null; - await localConversationCubit.close(); - final conversation = data.value; - final messagesKey = conversation.messages.toVeilid(); - await pool.deleteRecord(messagesKey); - await pool.deleteRecord(_localConversationRecordKey!); - _localConversationRecordKey = null; - }); - } - - if (remoteConversationCubit != null) { - final data = remoteConversationCubit.state.asData; - if (data == null) { - log.warning('could not delete remote conversation'); - return false; - } - - deleteSet.add(() async { - _remoteConversationCubit = null; - await remoteConversationCubit.close(); - final conversation = data.value; - final messagesKey = conversation.messages.toVeilid(); - await pool.deleteRecord(messagesKey); - await pool.deleteRecord(_remoteConversationRecordKey!); - }); - } - - // Commit the delete futures - await deleteSet(); - - return true; - } - /// Force refresh of conversation keys Future refresh() async { await _initWait(); diff --git a/lib/layout/home/drawer_menu/drawer_menu.dart b/lib/layout/home/drawer_menu/drawer_menu.dart index 1e56c4b..218d7ed 100644 --- a/lib/layout/home/drawer_menu/drawer_menu.dart +++ b/lib/layout/home/drawer_menu/drawer_menu.dart @@ -71,11 +71,22 @@ class _DrawerMenuState extends State { shortname = abbrev; } - final avatar = AvatarImage( - size: 32, - backgroundColor: loggedIn ? scale.primary : scale.elementBackground, - foregroundColor: loggedIn ? scale.primaryText : scale.subtleText, - child: Text(shortname, style: theme.textTheme.titleLarge)); + final avatar = Container( + height: 34, + width: 34, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: loggedIn ? scale.border : scale.subtleBorder, + width: 2, + strokeAlign: BorderSide.strokeAlignOutside), + color: Colors.blue, + ), + child: AvatarImage( + //size: 32, + backgroundColor: loggedIn ? scale.primary : scale.elementBackground, + foregroundColor: loggedIn ? scale.primaryText : scale.subtleText, + child: Text(shortname, style: theme.textTheme.titleLarge))); return AnimatedPadding( padding: EdgeInsets.fromLTRB(selected ? 0 : 0, 0, selected ? 0 : 8, 0), @@ -234,6 +245,7 @@ class _DrawerMenuState extends State { Widget build(BuildContext context) { final theme = Theme.of(context); final scale = theme.extension()!; + final scaleConfig = theme.extension()!; //final textTheme = theme.textTheme; final localAccounts = context.watch().state; final perAccountCollectionBlocMapState = @@ -269,13 +281,17 @@ class _DrawerMenuState extends State { fit: BoxFit.scaleDown, child: Row(children: [ SvgPicture.asset( - height: 48, - 'assets/images/icon.svg', - ).paddingLTRB(0, 0, 16, 0), + height: 48, + 'assets/images/icon.svg', + colorFilter: scaleConfig.useVisualIndicators + ? grayColorFilter + : null) + .paddingLTRB(0, 0, 16, 0), SvgPicture.asset( - height: 48, - 'assets/images/title.svg', - ), + height: 48, + 'assets/images/title.svg', + colorFilter: + scaleConfig.useVisualIndicators ? grayColorFilter : null), ])), const Spacer(), _getAccountList( diff --git a/lib/theme/models/scale_scheme.dart b/lib/theme/models/scale_scheme.dart index 990fe1e..642dfee 100644 --- a/lib/theme/models/scale_scheme.dart +++ b/lib/theme/models/scale_scheme.dart @@ -89,11 +89,9 @@ class ScaleScheme extends ThemeExtension { onError: errorScale.primaryText, // errorContainer: errorScale.hoverElementBackground, // onErrorContainer: errorScale.subtleText, - background: grayScale.appBackground, // reviewed - onBackground: grayScale.appText, // reviewed surface: primaryScale.primary, // reviewed onSurface: primaryScale.primaryText, // reviewed - surfaceVariant: secondaryScale.primary, + surfaceContainerHighest: secondaryScale.primary, onSurfaceVariant: secondaryScale.primaryText, // ?? reviewed a little outline: primaryScale.border, outlineVariant: secondaryScale.border, diff --git a/lib/theme/views/widget_helpers.dart b/lib/theme/views/widget_helpers.dart index 52f26ac..e3dfd94 100644 --- a/lib/theme/views/widget_helpers.dart +++ b/lib/theme/views/widget_helpers.dart @@ -5,7 +5,6 @@ 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'; import 'package:quickalert/quickalert.dart'; @@ -122,36 +121,45 @@ Future showErrorModal( } void showErrorToast(BuildContext context, String message) { - MotionToast.error( - title: Text(translate('toast.error')), + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + + MotionToast( + //title: Text(translate('toast.error')), description: Text(message), + constraints: BoxConstraints.loose(const Size(400, 100)), + contentPadding: const EdgeInsets.all(16), + primaryColor: scale.errorScale.elementBackground, + secondaryColor: scale.errorScale.calloutBackground, + borderRadius: 16, + toastDuration: const Duration(seconds: 4), + animationDuration: const Duration(milliseconds: 1000), + displayBorder: scaleConfig.useVisualIndicators, + icon: Icons.error, ).show(context); } void showInfoToast(BuildContext context, String message) { - MotionToast.info( - title: Text(translate('toast.info')), + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + + MotionToast( + //title: Text(translate('toast.info')), description: Text(message), + constraints: BoxConstraints.loose(const Size(400, 100)), + contentPadding: const EdgeInsets.all(16), + primaryColor: scale.tertiaryScale.elementBackground, + secondaryColor: scale.tertiaryScale.calloutBackground, + borderRadius: 16, + toastDuration: const Duration(seconds: 2), + animationDuration: const Duration(milliseconds: 500), + displayBorder: scaleConfig.useVisualIndicators, + icon: Icons.info, ).show(context); } -// Widget insetBorder( -// {required BuildContext context, -// required bool enabled, -// required Color color, -// required Widget child}) { -// if (!enabled) { -// return child; -// } - -// return Stack({ -// children: [] { -// DecoratedBox(decoration: BoxDecoration() -// child, -// } -// }) -// } - Widget styledTitleContainer({ required BuildContext context, required String title, @@ -230,3 +238,26 @@ Widget styledBottomSheet({ bool get isPlatformDark => WidgetsBinding.instance.platformDispatcher.platformBrightness == Brightness.dark; + +const grayColorFilter = ColorFilter.matrix([ + 0.2126, + 0.7152, + 0.0722, + 0, + 0, + 0.2126, + 0.7152, + 0.0722, + 0, + 0, + 0.2126, + 0.7152, + 0.0722, + 0, + 0, + 0, + 0, + 0, + 1, + 0, +]); 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 28fa907..e04af10 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 @@ -1,7 +1,5 @@ part of 'dht_record_pool.dart'; -const _sfListen = 'listen'; - @immutable class DHTRecordWatchChange extends Equatable { const DHTRecordWatchChange( @@ -41,7 +39,7 @@ enum DHTRecordRefreshMode { class DHTRecord implements DHTDeleteable { DHTRecord._( {required VeilidRoutingContext routingContext, - required SharedDHTRecordData sharedDHTRecordData, + required _SharedDHTRecordData sharedDHTRecordData, required int defaultSubkey, required KeyPair? writer, required VeilidCrypto crypto, @@ -241,7 +239,7 @@ class DHTRecord implements DHTDeleteable { // if so, shortcut and don't bother decrypting it if (newValueData.data.equals(encryptedNewValue)) { if (isUpdated) { - DHTRecordPool.instance.processLocalValueChange(key, newValue, subkey); + DHTRecordPool.instance._processLocalValueChange(key, newValue, subkey); } return null; } @@ -251,7 +249,7 @@ class DHTRecord implements DHTDeleteable { await (crypto ?? _crypto).decrypt(newValueData.data); if (isUpdated) { DHTRecordPool.instance - .processLocalValueChange(key, decryptedNewValue, subkey); + ._processLocalValueChange(key, decryptedNewValue, subkey); } return decryptedNewValue; } @@ -298,7 +296,7 @@ class DHTRecord implements DHTDeleteable { final isUpdated = newValueData.seq != lastSeq; if (isUpdated) { - DHTRecordPool.instance.processLocalValueChange(key, newValue, subkey); + DHTRecordPool.instance._processLocalValueChange(key, newValue, subkey); } } @@ -419,7 +417,7 @@ class DHTRecord implements DHTDeleteable { // Set up watch requirements which will get picked up by the next tick final oldWatchState = watchState; watchState = - WatchState(subkeys: subkeys, expiration: expiration, count: count); + _WatchState(subkeys: subkeys, expiration: expiration, count: count); if (oldWatchState != watchState) { _sharedDHTRecordData.needsWatchStateUpdate = true; } @@ -544,7 +542,7 @@ class DHTRecord implements DHTDeleteable { ////////////////////////////////////////////////////////////// - final SharedDHTRecordData _sharedDHTRecordData; + final _SharedDHTRecordData _sharedDHTRecordData; final VeilidRoutingContext _routingContext; final int _defaultSubkey; final KeyPair? _writer; @@ -554,5 +552,5 @@ class DHTRecord implements DHTDeleteable { int _openCount; StreamController? _watchController; @internal - WatchState? watchState; + _WatchState? watchState; } 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 3efd8a7..b80db1f 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 @@ -16,20 +16,11 @@ export 'package:fast_immutable_collections/fast_immutable_collections.dart' part 'dht_record_pool.freezed.dart'; part 'dht_record_pool.g.dart'; part 'dht_record.dart'; - -const int watchBackoffMultiplier = 2; -const int watchBackoffMax = 30; - -const int? defaultWatchDurationSecs = null; // 600 -const int watchRenewalNumerator = 4; -const int watchRenewalDenominator = 5; +part 'dht_record_pool_private.dart'; // Maximum number of concurrent DHT operations to perform on the network const int maxDHTConcurrency = 8; -// DHT crypto domain -const String cryptoDomainDHT = 'dht'; - typedef DHTRecordPoolLogger = void Function(String message); /// Record pool that managed DHTRecords and allows for tagged deletion @@ -62,114 +53,18 @@ class OwnedDHTRecordPointer with _$OwnedDHTRecordPointer { _$OwnedDHTRecordPointerFromJson(json as Map); } -/// Watch state -@immutable -class WatchState extends Equatable { - const WatchState( - {required this.subkeys, - required this.expiration, - required this.count, - this.realExpiration, - this.renewalTime}); - final List? subkeys; - final Timestamp? expiration; - final int? count; - final Timestamp? realExpiration; - final Timestamp? renewalTime; - - @override - List get props => - [subkeys, expiration, count, realExpiration, renewalTime]; -} - -/// Data shared amongst all DHTRecord instances -class SharedDHTRecordData { - SharedDHTRecordData( - {required this.recordDescriptor, - required this.defaultWriter, - required this.defaultRoutingContext}); - DHTRecordDescriptor recordDescriptor; - KeyPair? defaultWriter; - VeilidRoutingContext defaultRoutingContext; - bool needsWatchStateUpdate = false; - WatchState? unionWatchState; -} - -// 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 = {}; - - String get debugNames { - final r = records.toList() - ..sort((a, b) => a.key.toString().compareTo(b.key.toString())); - return '[${r.map((x) => x.debugName).join(',')}]'; - } - - String get details { - final r = records.toList() - ..sort((a, b) => a.key.toString().compareTo(b.key.toString())); - return '[${r.map((x) => "writer=${x._writer} " - "defaultSubkey=${x._defaultSubkey}").join(',')}]'; - } - - String get sharedDetails => shared.toString(); -} +////////////////////////////////////////////////////////////////////////////// +/// Allocator and management system for DHTRecord class DHTRecordPool with TableDBBackedJson { DHTRecordPool._(Veilid veilid, VeilidRoutingContext routingContext) : _state = const DHTRecordPoolAllocations(), _mutex = Mutex(), - _opened = {}, + _opened = {}, _markedForDelete = {}, _routingContext = routingContext, _veilid = veilid; - // Logger - DHTRecordPoolLogger? _logger; - - // Persistent DHT record list - DHTRecordPoolAllocations _state; - // Create/open Mutex - final Mutex _mutex; - // Which DHT records are currently open - final Map _opened; - // Which DHT records are marked for deletion - final Set _markedForDelete; - // 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; - // Tick counter for backoff - int _tickCount = 0; - // Backoff timer - int _watchBackoffTimer = 1; - - static DHTRecordPool? _singleton; - - ////////////////////////////////////////////////////////////// - /// AsyncTableDBBacked - @override - String tableName() => 'dht_record_pool'; - @override - String tableKeyName() => 'pool_allocations'; - @override - DHTRecordPoolAllocations valueFromJson(Object? obj) => obj != null - ? DHTRecordPoolAllocations.fromJson(obj) - : const DHTRecordPoolAllocations(); - @override - Object? valueToJson(DHTRecordPoolAllocations? val) => val?.toJson(); - ////////////////////////////////////////////////////////////// static DHTRecordPool get instance => _singleton!; @@ -190,337 +85,8 @@ class DHTRecordPool with TableDBBackedJson { } } - Veilid get veilid => _veilid; - - void log(String message) { - _logger?.call(message); - } - - Future _recordCreateInner( - {required String debugName, - required VeilidRoutingContext dhtctx, - required DHTSchema schema, - KeyPair? writer, - TypedKey? parent}) async { - if (!_mutex.isLocked) { - throw StateError('should be locked here'); - } - // Create the record - final recordDescriptor = await dhtctx.createDHTRecord(schema); - - log('createDHTRecord: debugName=$debugName key=${recordDescriptor.key}'); - - // Reopen if a writer is specified to ensure - // we switch the default writer - if (writer != null) { - await dhtctx.openDHTRecord(recordDescriptor.key, writer: writer); - } - final openedRecordInfo = OpenedRecordInfo( - recordDescriptor: recordDescriptor, - defaultWriter: writer ?? recordDescriptor.ownerKeyPair(), - defaultRoutingContext: dhtctx); - _opened[recordDescriptor.key] = openedRecordInfo; - - // Register the dependency - await _addDependencyInner( - parent, - recordDescriptor.key, - debugName: debugName, - ); - - return openedRecordInfo; - } - - Future _recordOpenInner( - {required String debugName, - required VeilidRoutingContext dhtctx, - required TypedKey recordKey, - KeyPair? writer, - TypedKey? parent}) async { - if (!_mutex.isLocked) { - throw StateError('should be locked here'); - } - log('openDHTRecord: debugName=$debugName key=$recordKey'); - - // If we are opening a key that already exists - // make sure we are using the same parent if one was specified - _validateParentInner(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, - debugName: debugName, - ); - - return newOpenedRecordInfo; - } - - // 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, - debugName: debugName, - ); - - return openedRecordInfo; - } - - // Called when a DHTRecord is closed - // Cleans up the opened record housekeeping and processes any late deletions - Future _recordClosed(DHTRecord record) async { - await _mutex.protect(() async { - final key = record.key; - - log('closeDHTRecord: debugName=${record.debugName} key=$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); - - await _checkForLateDeletesInner(key); - } - }); - } - - // Check to see if this key can finally be deleted - // If any parents are marked for deletion, try them first - Future _checkForLateDeletesInner(TypedKey key) async { - // Get parent list in bottom up order including our own key - final parents = []; - TypedKey? nextParent = key; - while (nextParent != null) { - parents.add(nextParent); - nextParent = getParentRecordKey(nextParent); - } - - // If any parent is ready to delete all its children do it - for (final parent in parents) { - if (_markedForDelete.contains(parent)) { - final deleted = await _deleteRecordInner(parent); - if (!deleted) { - // If we couldn't delete a child then no 'marked for delete' parents - // above us will be ready to delete either - break; - } - } - } - } - - // Collect all dependencies (including the record itself) - // in reverse (bottom-up/delete order) - List _collectChildrenInner(TypedKey recordKey) { - if (!_mutex.isLocked) { - throw StateError('should be locked here'); - } - final allDeps = []; - final currentDeps = [recordKey]; - while (currentDeps.isNotEmpty) { - final nextDep = currentDeps.removeLast(); - - allDeps.add(nextDep); - final childDeps = - _state.childrenByParent[nextDep.toJson()]?.toList() ?? []; - currentDeps.addAll(childDeps); - } - return allDeps.reversedView; - } - - /// Collect all dependencies (including the record itself) - /// in reverse (bottom-up/delete order) - Future> collectChildren(TypedKey recordKey) => - _mutex.protect(() async => _collectChildrenInner(recordKey)); - - /// Print children - String debugChildren(TypedKey recordKey, {List? allDeps}) { - allDeps ??= _collectChildrenInner(recordKey); - // ignore: avoid_print - var out = - 'Parent: $recordKey (${_state.debugNames[recordKey.toString()]})\n'; - for (final dep in allDeps) { - if (dep != recordKey) { - // ignore: avoid_print - out += ' Child: $dep (${_state.debugNames[dep.toString()]})\n'; - } - } - return out; - } - - // Actual delete function - Future _finalizeDeleteRecordInner(TypedKey recordKey) async { - log('_finalizeDeleteRecordInner: key=$recordKey'); - - // Remove this child from parents - await _removeDependenciesInner([recordKey]); - await _routingContext.deleteDHTRecord(recordKey); - _markedForDelete.remove(recordKey); - } - - // Deep delete mechanism inside mutex - Future _deleteRecordInner(TypedKey recordKey) async { - final toDelete = _readyForDeleteInner(recordKey); - if (toDelete.isNotEmpty) { - // delete now - for (final deleteKey in toDelete) { - await _finalizeDeleteRecordInner(deleteKey); - } - return true; - } - // mark for deletion - _markedForDelete.add(recordKey); - return false; - } - - /// Delete a record and its children if they are all closed - /// otherwise mark that record for deletion eventually - /// Returns true if the deletion was processed immediately - /// Returns false if the deletion was marked for later - Future deleteRecord(TypedKey recordKey) async => - _mutex.protect(() async => _deleteRecordInner(recordKey)); - - // If everything underneath is closed including itself, return the - // list of children (and itself) to finally actually delete - List _readyForDeleteInner(TypedKey recordKey) { - final allDeps = _collectChildrenInner(recordKey); - for (final dep in allDeps) { - if (_opened.containsKey(dep)) { - return []; - } - } - return allDeps; - } - - void _validateParentInner(TypedKey? parent, TypedKey child) { - if (!_mutex.isLocked) { - throw StateError('should be locked here'); - } - - final childJson = child.toJson(); - final existingParent = _state.parentByChild[childJson]; - if (parent == null) { - if (existingParent != null) { - throw StateError('Child is already parented: $child'); - } - } else { - if (_state.rootRecords.contains(child)) { - throw StateError('Child already added as root: $child'); - } - if (existingParent != null && existingParent != parent) { - throw StateError('Child has two parents: $child <- $parent'); - } - } - } - - Future _addDependencyInner(TypedKey? parent, TypedKey child, - {required String debugName}) async { - if (!_mutex.isLocked) { - throw StateError('should be locked here'); - } - if (parent == null) { - if (_state.rootRecords.contains(child)) { - // Dependency already added - return; - } - _state = await store(_state.copyWith( - rootRecords: _state.rootRecords.add(child), - debugNames: _state.debugNames.add(child.toJson(), debugName))); - } else { - final childrenOfParent = - _state.childrenByParent[parent.toJson()] ?? ISet(); - if (childrenOfParent.contains(child)) { - // Dependency already added (consecutive opens, etc) - return; - } - _state = await store(_state.copyWith( - childrenByParent: _state.childrenByParent - .add(parent.toJson(), childrenOfParent.add(child)), - parentByChild: _state.parentByChild.add(child.toJson(), parent), - debugNames: _state.debugNames.add(child.toJson(), debugName))); - } - } - - Future _removeDependenciesInner(List childList) async { - if (!_mutex.isLocked) { - throw StateError('should be locked here'); - } - var state = _state; - - for (final child in childList) { - if (_state.rootRecords.contains(child)) { - state = state.copyWith( - rootRecords: state.rootRecords.remove(child), - debugNames: state.debugNames.remove(child.toJson())); - } else { - 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()), - debugNames: state.debugNames.remove(child.toJson())); - } else { - state = state.copyWith( - childrenByParent: - state.childrenByParent.add(parent.toJson(), children), - parentByChild: state.parentByChild.remove(child.toJson()), - debugNames: state.debugNames.remove(child.toJson())); - } - } - } - - if (state != _state) { - _state = await store(state); - } - } - - bool _isValidRecordKeyInner(TypedKey key) { - if (_state.rootRecords.contains(key)) { - return true; - } - if (_state.childrenByParent.containsKey(key.toJson())) { - return true; - } - return false; - } - - Future isValidRecordKey(TypedKey key) => - _mutex.protect(() async => _isValidRecordKeyInner(key)); - - /////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + // Public Interface /// Create a root DHTRecord that has no dependent records Future createRecord({ @@ -653,23 +219,52 @@ class DHTRecordPool with TableDBBackedJson { 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; + /// Check if record is allocated + Future isValidRecordKey(TypedKey key) => + _mutex.protect(() async => _isValidRecordKeyInner(key)); + + /// Check if record is marked for deletion or already gone + Future isDeletedRecordKey(TypedKey key) => + _mutex.protect(() async => _isDeletedRecordKeyInner(key)); + + /// Delete a record and its children if they are all closed + /// otherwise mark that record for deletion eventually + /// Returns true if the deletion was processed immediately + /// Returns false if the deletion was marked for later + Future deleteRecord(TypedKey recordKey) async => + _mutex.protect(() async => _deleteRecordInner(recordKey)); + + // If everything underneath is closed including itself, return the + // list of children (and itself) to finally actually delete + List _readyForDeleteInner(TypedKey recordKey) { + final allDeps = _collectChildrenInner(recordKey); + for (final dep in allDeps) { + if (_opened.containsKey(dep)) { + return []; } } + return allDeps; } - /// Generate default VeilidCrypto for a writer - static Future privateCryptoFromTypedSecret( - TypedKey typedSecret) async => - VeilidCryptoPrivate.fromTypedKey(typedSecret, cryptoDomainDHT); + /// Collect all dependencies (including the record itself) + /// in reverse (bottom-up/delete order) + Future> collectChildren(TypedKey recordKey) => + _mutex.protect(() async => _collectChildrenInner(recordKey)); + + /// Print children + String debugChildren(TypedKey recordKey, {List? allDeps}) { + allDeps ??= _collectChildrenInner(recordKey); + // ignore: avoid_print + var out = + 'Parent: $recordKey (${_state.debugNames[recordKey.toString()]})\n'; + for (final dep in allDeps) { + if (dep != recordKey) { + // ignore: avoid_print + out += ' Child: $dep (${_state.debugNames[dep.toString()]})\n'; + } + } + return out; + } /// Handle the DHT record updates coming from Veilid void processRemoteValueChange(VeilidUpdateValueChange updateValueChange) { @@ -711,7 +306,360 @@ class DHTRecordPool with TableDBBackedJson { } } - WatchState? _collectUnionWatchState(Iterable records) { + /// Log the current record allocations + void debugPrintAllocations() { + final sortedAllocations = _state.debugNames.entries.asList() + ..sort((a, b) => a.key.compareTo(b.key)); + + log('DHTRecordPool Allocations: (count=${sortedAllocations.length})'); + + for (final entry in sortedAllocations) { + log(' ${entry.key}: ${entry.value}'); + } + } + + /// Log the current opened record details + void debugPrintOpened() { + final sortedOpened = _opened.entries.asList() + ..sort((a, b) => a.key.toString().compareTo(b.key.toString())); + + log('DHTRecordPool Opened Records: (count=${sortedOpened.length})'); + + for (final entry in sortedOpened) { + log(' ${entry.key}: \n' + ' debugNames=${entry.value.debugNames}\n' + ' details=${entry.value.details}\n' + ' sharedDetails=${entry.value.sharedDetails}\n'); + } + } + + /// Public interface to DHTRecordPool logger + void log(String message) { + _logger?.call(message); + } + + /// Generate default VeilidCrypto for a writer + static Future privateCryptoFromTypedSecret( + TypedKey typedSecret) async => + VeilidCryptoPrivate.fromTypedKey(typedSecret, _cryptoDomainDHT); + + //////////////////////////////////////////////////////////////////////////// + // Private Implementation + + Future<_OpenedRecordInfo> _recordCreateInner( + {required String debugName, + required VeilidRoutingContext dhtctx, + required DHTSchema schema, + KeyPair? writer, + TypedKey? parent}) async { + if (!_mutex.isLocked) { + throw StateError('should be locked here'); + } + // Create the record + final recordDescriptor = await dhtctx.createDHTRecord(schema); + + log('createDHTRecord: debugName=$debugName key=${recordDescriptor.key}'); + + // Reopen if a writer is specified to ensure + // we switch the default writer + if (writer != null) { + await dhtctx.openDHTRecord(recordDescriptor.key, writer: writer); + } + final openedRecordInfo = _OpenedRecordInfo( + recordDescriptor: recordDescriptor, + defaultWriter: writer ?? recordDescriptor.ownerKeyPair(), + defaultRoutingContext: dhtctx); + _opened[recordDescriptor.key] = openedRecordInfo; + + // Register the dependency + await _addDependencyInner( + parent, + recordDescriptor.key, + debugName: debugName, + ); + + return openedRecordInfo; + } + + Future<_OpenedRecordInfo> _recordOpenInner( + {required String debugName, + required VeilidRoutingContext dhtctx, + required TypedKey recordKey, + KeyPair? writer, + TypedKey? parent}) async { + if (!_mutex.isLocked) { + throw StateError('should be locked here'); + } + log('openDHTRecord: debugName=$debugName key=$recordKey'); + + // If we are opening a key that already exists + // make sure we are using the same parent if one was specified + _validateParentInner(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, + debugName: debugName, + ); + + return newOpenedRecordInfo; + } + + // 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) { + await dhtctx.openDHTRecord(recordKey, writer: writer); + // New writer if we didn't specify one before + openedRecordInfo.shared.defaultWriter = writer; + // New default routing context if we opened it again + openedRecordInfo.shared.defaultRoutingContext = dhtctx; + } + + // Register the dependency + await _addDependencyInner( + parent, + recordKey, + debugName: debugName, + ); + + return openedRecordInfo; + } + + // Called when a DHTRecord is closed + // Cleans up the opened record housekeeping and processes any late deletions + Future _recordClosed(DHTRecord record) async { + await _mutex.protect(() async { + final key = record.key; + + log('closeDHTRecord: debugName=${record.debugName} key=$key'); + + final openedRecordInfo = _opened[key]; + if (openedRecordInfo == null || + !openedRecordInfo.records.remove(record)) { + throw StateError('record already closed'); + } + if (openedRecordInfo.records.isEmpty) { + await _watchStateProcessors.remove(key); + await _routingContext.closeDHTRecord(key); + _opened.remove(key); + + await _checkForLateDeletesInner(key); + } + }); + } + + // Check to see if this key can finally be deleted + // If any parents are marked for deletion, try them first + Future _checkForLateDeletesInner(TypedKey key) async { + // Get parent list in bottom up order including our own key + final parents = []; + TypedKey? nextParent = key; + while (nextParent != null) { + parents.add(nextParent); + nextParent = getParentRecordKey(nextParent); + } + + // If any parent is ready to delete all its children do it + for (final parent in parents) { + if (_markedForDelete.contains(parent)) { + final deleted = await _deleteRecordInner(parent); + if (!deleted) { + // If we couldn't delete a child then no 'marked for delete' parents + // above us will be ready to delete either + break; + } + } + } + } + + // Collect all dependencies (including the record itself) + // in reverse (bottom-up/delete order) + List _collectChildrenInner(TypedKey recordKey) { + if (!_mutex.isLocked) { + throw StateError('should be locked here'); + } + final allDeps = []; + final currentDeps = [recordKey]; + while (currentDeps.isNotEmpty) { + final nextDep = currentDeps.removeLast(); + + allDeps.add(nextDep); + final childDeps = + _state.childrenByParent[nextDep.toJson()]?.toList() ?? []; + currentDeps.addAll(childDeps); + } + return allDeps.reversedView; + } + + // Actual delete function + Future _finalizeDeleteRecordInner(TypedKey recordKey) async { + log('_finalizeDeleteRecordInner: key=$recordKey'); + + // Remove this child from parents + await _removeDependenciesInner([recordKey]); + await _routingContext.deleteDHTRecord(recordKey); + _markedForDelete.remove(recordKey); + } + + // Deep delete mechanism inside mutex + Future _deleteRecordInner(TypedKey recordKey) async { + final toDelete = _readyForDeleteInner(recordKey); + if (toDelete.isNotEmpty) { + // delete now + for (final deleteKey in toDelete) { + await _finalizeDeleteRecordInner(deleteKey); + } + return true; + } + // mark for deletion + _markedForDelete.add(recordKey); + return false; + } + + void _validateParentInner(TypedKey? parent, TypedKey child) { + if (!_mutex.isLocked) { + throw StateError('should be locked here'); + } + + final childJson = child.toJson(); + final existingParent = _state.parentByChild[childJson]; + if (parent == null) { + if (existingParent != null) { + throw StateError('Child is already parented: $child'); + } + } else { + if (_state.rootRecords.contains(child)) { + throw StateError('Child already added as root: $child'); + } + if (existingParent != null && existingParent != parent) { + throw StateError('Child has two parents: $child <- $parent'); + } + } + } + + Future _addDependencyInner(TypedKey? parent, TypedKey child, + {required String debugName}) async { + if (!_mutex.isLocked) { + throw StateError('should be locked here'); + } + if (parent == null) { + if (_state.rootRecords.contains(child)) { + // Dependency already added + return; + } + _state = await store(_state.copyWith( + rootRecords: _state.rootRecords.add(child), + debugNames: _state.debugNames.add(child.toJson(), debugName))); + } else { + final childrenOfParent = + _state.childrenByParent[parent.toJson()] ?? ISet(); + if (childrenOfParent.contains(child)) { + // Dependency already added (consecutive opens, etc) + return; + } + _state = await store(_state.copyWith( + childrenByParent: _state.childrenByParent + .add(parent.toJson(), childrenOfParent.add(child)), + parentByChild: _state.parentByChild.add(child.toJson(), parent), + debugNames: _state.debugNames.add(child.toJson(), debugName))); + } + } + + Future _removeDependenciesInner(List childList) async { + if (!_mutex.isLocked) { + throw StateError('should be locked here'); + } + var state = _state; + + for (final child in childList) { + if (_state.rootRecords.contains(child)) { + state = state.copyWith( + rootRecords: state.rootRecords.remove(child), + debugNames: state.debugNames.remove(child.toJson())); + } else { + 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()), + debugNames: state.debugNames.remove(child.toJson())); + } else { + state = state.copyWith( + childrenByParent: + state.childrenByParent.add(parent.toJson(), children), + parentByChild: state.parentByChild.remove(child.toJson()), + debugNames: state.debugNames.remove(child.toJson())); + } + } + } + + if (state != _state) { + _state = await store(state); + } + } + + bool _isValidRecordKeyInner(TypedKey key) { + if (_state.rootRecords.contains(key)) { + return true; + } + if (_state.childrenByParent.containsKey(key.toJson())) { + return true; + } + return false; + } + + bool _isDeletedRecordKeyInner(TypedKey key) { + // Is this key gone? + if (!_isValidRecordKeyInner(key)) { + return true; + } + + // Is this key on its way out because it or one of its parents + // is scheduled to delete everything underneath it? + TypedKey? nextParent = key; + while (nextParent != null) { + if (_markedForDelete.contains(nextParent)) { + return true; + } + nextParent = getParentRecordKey(nextParent); + } + + return false; + } + + /// 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; + } + } + } + + static _WatchState? _collectUnionWatchState(Iterable records) { // Collect union of opened record watch states int? totalCount; Timestamp? maxExpiration; @@ -770,19 +718,19 @@ class DHTRecordPool with TableDBBackedJson { return null; } - return WatchState( + return _WatchState( subkeys: allSubkeys, expiration: maxExpiration, count: totalCount, renewalTime: earliestRenewalTime); } - void _updateWatchRealExpirations(Iterable records, + static void _updateWatchRealExpirations(Iterable records, Timestamp realExpiration, Timestamp renewalTime) { for (final rec in records) { final ws = rec.watchState; if (ws != null) { - rec.watchState = WatchState( + rec.watchState = _WatchState( subkeys: ws.subkeys, expiration: ws.expiration, count: ws.count, @@ -792,154 +740,194 @@ class DHTRecordPool with TableDBBackedJson { } } + Future _watchStateChange( + TypedKey openedRecordKey, _WatchState? unionWatchState) async { + // Get the current state for this watch + final openedRecordInfo = _opened[openedRecordKey]; + if (openedRecordInfo == null) { + // Record is gone, nothing to do + return; + } + final currentWatchState = openedRecordInfo.shared.unionWatchState; + final dhtctx = openedRecordInfo.shared.defaultRoutingContext; + + // If it's the same as our desired state there is nothing to do here + if (currentWatchState == unionWatchState) { + return; + } + + // Apply watch changes for record + if (unionWatchState == null) { + // Record needs watch cancel + // Only try this once, if it doesn't succeed then it can just expire + // on its own. + try { + final cancelled = await dhtctx.cancelDHTWatch(openedRecordKey); + + log('cancelDHTWatch: key=$openedRecordKey, cancelled=$cancelled, ' + 'debugNames=${openedRecordInfo.debugNames}'); + + openedRecordInfo.shared.unionWatchState = null; + openedRecordInfo.shared.needsWatchStateUpdate = false; + } on VeilidAPIException catch (e) { + // Failed to cancel DHT watch, try again next tick + log('Exception in watch cancel: $e'); + } + return; + } + + // Record needs new watch + try { + final subkeys = unionWatchState.subkeys?.toList(); + final count = unionWatchState.count; + final expiration = unionWatchState.expiration; + final now = veilid.now(); + + final realExpiration = await dhtctx.watchDHTValues(openedRecordKey, + subkeys: unionWatchState.subkeys?.toList(), + count: unionWatchState.count, + expiration: unionWatchState.expiration ?? + (_defaultWatchDurationSecs == null + ? null + : veilid.now().offset(TimestampDuration.fromMillis( + _defaultWatchDurationSecs! * 1000)))); + + final expirationDuration = realExpiration.diff(now); + final renewalTime = now.offset(TimestampDuration( + value: expirationDuration.value * + BigInt.from(_watchRenewalNumerator) ~/ + BigInt.from(_watchRenewalDenominator))); + + log('watchDHTValues: key=$openedRecordKey, subkeys=$subkeys, ' + 'count=$count, expiration=$expiration, ' + 'realExpiration=$realExpiration, ' + 'renewalTime=$renewalTime, ' + 'debugNames=${openedRecordInfo.debugNames}'); + + // Update watch states with real expiration + if (realExpiration.value != BigInt.zero) { + openedRecordInfo.shared.unionWatchState = unionWatchState; + _updateWatchRealExpirations( + openedRecordInfo.records, realExpiration, renewalTime); + openedRecordInfo.shared.needsWatchStateUpdate = false; + } + } on VeilidAPIException catch (e) { + // Failed to cancel DHT watch, try again next tick + log('Exception in watch update: $e'); + } + } + + void _pollWatch(TypedKey openedRecordKey, _OpenedRecordInfo openedRecordInfo, + _WatchState unionWatchState) { + singleFuture((this, _sfPollWatch, openedRecordKey), () async { + final dhtctx = openedRecordInfo.shared.defaultRoutingContext; + + // Get single subkey to poll + // XXX: veilid api limits this for now until everyone supports + // inspectDHTRecord + final pollSubkey = unionWatchState.subkeys?.firstSubkey; + if (pollSubkey == null) { + return; + } + final pollSubkeys = [ValueSubkeyRange.single(pollSubkey)]; + + final currentReport = + await dhtctx.inspectDHTRecord(openedRecordKey, subkeys: pollSubkeys); + final currentSeq = currentReport.localSeqs.firstOrNull ?? -1; + + final valueData = await dhtctx.getDHTValue(openedRecordKey, pollSubkey, + forceRefresh: true); + if (valueData == null) { + return; + } + if (valueData.seq > currentSeq) { + processRemoteValueChange(VeilidUpdateValueChange( + key: openedRecordKey, + subkeys: pollSubkeys, + count: 0xFFFFFFFF, + value: valueData)); + } + }); + } + /// Ticker to check watch state change requests Future tick() async { - if (_tickCount < _watchBackoffTimer) { - _tickCount++; - return; - } - if (_inTick) { - return; - } - _inTick = true; - _tickCount = 0; final now = veilid.now(); - try { - final allSuccess = await _mutex.protect(() async { - // See if any opened records need watch state changes - final unord = Function()>[]; + await _mutex.protect(() async { + // See if any opened records need watch state changes + for (final kv in _opened.entries) { + final openedRecordKey = kv.key; + final openedRecordInfo = kv.value; - for (final kv in _opened.entries) { - final openedRecordKey = kv.key; - final openedRecordInfo = kv.value; - final dhtctx = openedRecordInfo.shared.defaultRoutingContext; + var wantsWatchStateUpdate = + openedRecordInfo.shared.needsWatchStateUpdate; - var wantsWatchStateUpdate = - openedRecordInfo.shared.needsWatchStateUpdate; - - // Check if we have reached renewal time for the watch - if (openedRecordInfo.shared.unionWatchState != null && - openedRecordInfo.shared.unionWatchState!.renewalTime != null && - now.value > - openedRecordInfo.shared.unionWatchState!.renewalTime!.value) { - wantsWatchStateUpdate = true; - } - - if (wantsWatchStateUpdate) { - // Update union watch state - final unionWatchState = openedRecordInfo.shared.unionWatchState = - _collectUnionWatchState(openedRecordInfo.records); - - // Apply watch changes for record - if (unionWatchState == null) { - unord.add(() async { - // Record needs watch cancel - var success = false; - try { - success = await dhtctx.cancelDHTWatch(openedRecordKey); - - log('cancelDHTWatch: key=$openedRecordKey, success=$success, ' - 'debugNames=${openedRecordInfo.debugNames}'); - - openedRecordInfo.shared.needsWatchStateUpdate = false; - } on VeilidAPIException catch (e) { - // Failed to cancel DHT watch, try again next tick - log('Exception in watch cancel: $e'); - } - return success; - }); - } else { - unord.add(() async { - // Record needs new watch - var success = false; - try { - final subkeys = unionWatchState.subkeys?.toList(); - final count = unionWatchState.count; - final expiration = unionWatchState.expiration; - final now = veilid.now(); - - final realExpiration = await dhtctx.watchDHTValues( - openedRecordKey, - subkeys: unionWatchState.subkeys?.toList(), - count: unionWatchState.count, - expiration: unionWatchState.expiration ?? - (defaultWatchDurationSecs == null - ? null - : veilid.now().offset( - TimestampDuration.fromMillis( - defaultWatchDurationSecs! * 1000)))); - - final expirationDuration = realExpiration.diff(now); - final renewalTime = now.offset(TimestampDuration( - value: expirationDuration.value * - BigInt.from(watchRenewalNumerator) ~/ - BigInt.from(watchRenewalDenominator))); - - log('watchDHTValues: key=$openedRecordKey, subkeys=$subkeys, ' - 'count=$count, expiration=$expiration, ' - 'realExpiration=$realExpiration, ' - 'renewalTime=$renewalTime, ' - 'debugNames=${openedRecordInfo.debugNames}'); - - // Update watch states with real expiration - if (realExpiration.value != BigInt.zero) { - openedRecordInfo.shared.needsWatchStateUpdate = false; - _updateWatchRealExpirations( - openedRecordInfo.records, realExpiration, renewalTime); - success = true; - } - } on VeilidAPIException catch (e) { - // Failed to cancel DHT watch, try again next tick - log('Exception in watch update: $e'); - } - return success; - }); - } - } + // Check if we have reached renewal time for the watch + if (openedRecordInfo.shared.unionWatchState != null && + openedRecordInfo.shared.unionWatchState!.renewalTime != null && + now.value > + openedRecordInfo.shared.unionWatchState!.renewalTime!.value) { + wantsWatchStateUpdate = true; } - // Process all watch changes - return unord.isEmpty || - (await unord.map((f) => f()).wait).reduce((a, b) => a && b); - }); + if (wantsWatchStateUpdate) { + // Update union watch state + final unionWatchState = + _collectUnionWatchState(openedRecordInfo.records); - // If any watched did not success, back off the attempts to - // update the watches for a bit + final processed = _watchStateProcessors.updateState( + openedRecordKey, + unionWatchState, + (newState) => + _watchStateChange(openedRecordKey, unionWatchState)); - if (!allSuccess) { - _watchBackoffTimer *= watchBackoffMultiplier; - _watchBackoffTimer = min(_watchBackoffTimer, watchBackoffMax); - } else { - _watchBackoffTimer = 1; + // In lieu of a completed watch, set off a polling operation + // on the first value of the watched range, which, due to current + // veilid limitations can only be one subkey at a time right now + if (!processed && unionWatchState != null) { + _pollWatch(openedRecordKey, openedRecordInfo, unionWatchState); + } + } } - } finally { - _inTick = false; - } + }); } - void debugPrintAllocations() { - final sortedAllocations = _state.debugNames.entries.asList() - ..sort((a, b) => a.key.compareTo(b.key)); + ////////////////////////////////////////////////////////////// + // AsyncTableDBBacked + @override + String tableName() => 'dht_record_pool'; + @override + String tableKeyName() => 'pool_allocations'; + @override + DHTRecordPoolAllocations valueFromJson(Object? obj) => obj != null + ? DHTRecordPoolAllocations.fromJson(obj) + : const DHTRecordPoolAllocations(); + @override + Object? valueToJson(DHTRecordPoolAllocations? val) => val?.toJson(); - log('DHTRecordPool Allocations: (count=${sortedAllocations.length})'); + //////////////////////////////////////////////////////////////////////////// + // Fields - for (final entry in sortedAllocations) { - log(' ${entry.key}: ${entry.value}'); - } - } + // Logger + DHTRecordPoolLogger? _logger; - void debugPrintOpened() { - final sortedOpened = _opened.entries.asList() - ..sort((a, b) => a.key.toString().compareTo(b.key.toString())); + // Persistent DHT record list + DHTRecordPoolAllocations _state; + // Create/open Mutex + final Mutex _mutex; + // Which DHT records are currently open + final Map _opened; + // Which DHT records are marked for deletion + final Set _markedForDelete; + // Default routing context to use for new keys + final VeilidRoutingContext _routingContext; + // Convenience accessor + final Veilid _veilid; + Veilid get veilid => _veilid; + // Watch state processors + final _watchStateProcessors = + SingleStateProcessorMap(); - log('DHTRecordPool Opened Records: (count=${sortedOpened.length})'); - - for (final entry in sortedOpened) { - log(' ${entry.key}: \n' - ' debugNames=${entry.value.debugNames}\n' - ' details=${entry.value.details}\n' - ' sharedDetails=${entry.value.sharedDetails}\n'); - } - } + static DHTRecordPool? _singleton; } diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool_private.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool_private.dart new file mode 100644 index 0000000..b7cbba8 --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool_private.dart @@ -0,0 +1,77 @@ +part of 'dht_record_pool.dart'; + +const int _watchBackoffMultiplier = 2; +const int _watchBackoffMax = 30; + +const int? _defaultWatchDurationSecs = null; // 600 +const int _watchRenewalNumerator = 4; +const int _watchRenewalDenominator = 5; + +// DHT crypto domain +const String _cryptoDomainDHT = 'dht'; + +// Singlefuture keys +const _sfPollWatch = '_pollWatch'; +const _sfListen = 'listen'; + +/// Watch state +@immutable +class _WatchState extends Equatable { + const _WatchState( + {required this.subkeys, + required this.expiration, + required this.count, + this.realExpiration, + this.renewalTime}); + final List? subkeys; + final Timestamp? expiration; + final int? count; + final Timestamp? realExpiration; + final Timestamp? renewalTime; + + @override + List get props => + [subkeys, expiration, count, realExpiration, renewalTime]; +} + +/// Data shared amongst all DHTRecord instances +class _SharedDHTRecordData { + _SharedDHTRecordData( + {required this.recordDescriptor, + required this.defaultWriter, + required this.defaultRoutingContext}); + DHTRecordDescriptor recordDescriptor; + KeyPair? defaultWriter; + VeilidRoutingContext defaultRoutingContext; + bool needsWatchStateUpdate = false; + _WatchState? unionWatchState; +} + +// 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 = {}; + + String get debugNames { + final r = records.toList() + ..sort((a, b) => a.key.toString().compareTo(b.key.toString())); + return '[${r.map((x) => x.debugName).join(',')}]'; + } + + String get details { + final r = records.toList() + ..sort((a, b) => a.key.toString().compareTo(b.key.toString())); + return '[${r.map((x) => "writer=${x._writer} " + "defaultSubkey=${x._defaultSubkey}").join(',')}]'; + } + + String get sharedDetails => shared.toString(); +} diff --git a/packages/veilid_support/lib/src/persistent_queue.dart b/packages/veilid_support/lib/src/persistent_queue.dart index f0cf17a..c7abe97 100644 --- a/packages/veilid_support/lib/src/persistent_queue.dart +++ b/packages/veilid_support/lib/src/persistent_queue.dart @@ -8,8 +8,7 @@ import 'package:protobuf/protobuf.dart'; import 'table_db.dart'; class PersistentQueue - /*extends Cubit>>*/ with - TableDBBackedFromBuffer> { + with TableDBBackedFromBuffer> { // PersistentQueue( {required String table, From 554f9b9a545d1f7cf53ab0c37a05e91d7e4e3b51 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 21 Jun 2024 23:03:11 -0400 Subject: [PATCH 151/270] revert theme ugliness --- lib/theme/models/scale_scheme.dart | 4 +++- packages/veilid_support/example/pubspec.lock | 8 ++++---- packages/veilid_support/pubspec.lock | 14 ++++++++------ packages/veilid_support/pubspec.yaml | 10 +++++----- pubspec.lock | 14 ++++++++------ pubspec.yaml | 10 +++++----- 6 files changed, 33 insertions(+), 27 deletions(-) diff --git a/lib/theme/models/scale_scheme.dart b/lib/theme/models/scale_scheme.dart index 642dfee..990fe1e 100644 --- a/lib/theme/models/scale_scheme.dart +++ b/lib/theme/models/scale_scheme.dart @@ -89,9 +89,11 @@ class ScaleScheme extends ThemeExtension { onError: errorScale.primaryText, // errorContainer: errorScale.hoverElementBackground, // onErrorContainer: errorScale.subtleText, + background: grayScale.appBackground, // reviewed + onBackground: grayScale.appText, // reviewed surface: primaryScale.primary, // reviewed onSurface: primaryScale.primaryText, // reviewed - surfaceContainerHighest: secondaryScale.primary, + surfaceVariant: secondaryScale.primary, onSurfaceVariant: secondaryScale.primaryText, // ?? reviewed a little outline: primaryScale.border, outlineVariant: secondaryScale.border, diff --git a/packages/veilid_support/example/pubspec.lock b/packages/veilid_support/example/pubspec.lock index 5ff4899..304670e 100644 --- a/packages/veilid_support/example/pubspec.lock +++ b/packages/veilid_support/example/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: "direct dev" description: name: async_tools - sha256: "72590010ed6c6f5cbd5d40e33834abc08a43da6a73ac3c3645517d53899b8684" + sha256: f35f5590711f1422c7eff990351e879c5433486f9b5be5df30818521bf6ab8d6 url: "https://pub.dev" source: hosted - version: "0.1.2" + version: "0.1.3" bloc: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: bloc_advanced_tools - sha256: "0cf9b3a73a67addfe22ec3f97a1ac240f6ad53870d6b21a980260f390d7901cd" + sha256: "11f534f9e89561302de3bd07ab2dfe1c2dacaa8db9794ccdb57c55cfc02dffc6" url: "https://pub.dev" source: hosted - version: "0.1.2" + version: "0.1.3" boolean_selector: dependency: transitive description: diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index 107e8d4..f38de9a 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -36,9 +36,10 @@ packages: async_tools: dependency: "direct main" description: - path: "../../../dart_async_tools" - relative: true - source: path + name: async_tools + sha256: f35f5590711f1422c7eff990351e879c5433486f9b5be5df30818521bf6ab8d6 + url: "https://pub.dev" + source: hosted version: "0.1.3" bloc: dependency: "direct main" @@ -51,9 +52,10 @@ packages: bloc_advanced_tools: dependency: "direct main" description: - path: "../../../bloc_advanced_tools" - relative: true - source: path + name: bloc_advanced_tools + sha256: "11f534f9e89561302de3bd07ab2dfe1c2dacaa8db9794ccdb57c55cfc02dffc6" + url: "https://pub.dev" + source: hosted version: "0.1.3" boolean_selector: dependency: transitive diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index b2b0e5c..01970ba 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -24,11 +24,11 @@ dependencies: # veilid: ^0.0.1 path: ../../../veilid/veilid-flutter -dependency_overrides: - async_tools: - path: ../../../dart_async_tools - bloc_advanced_tools: - path: ../../../bloc_advanced_tools +# dependency_overrides: +# async_tools: +# path: ../../../dart_async_tools +# bloc_advanced_tools: +# path: ../../../bloc_advanced_tools dev_dependencies: build_runner: ^2.4.10 diff --git a/pubspec.lock b/pubspec.lock index 0855cbc..16edf0a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -60,9 +60,10 @@ packages: async_tools: dependency: "direct main" description: - path: "../dart_async_tools" - relative: true - source: path + name: async_tools + sha256: f35f5590711f1422c7eff990351e879c5433486f9b5be5df30818521bf6ab8d6 + url: "https://pub.dev" + source: hosted version: "0.1.3" awesome_extensions: dependency: "direct main" @@ -99,9 +100,10 @@ packages: bloc_advanced_tools: dependency: "direct main" description: - path: "../bloc_advanced_tools" - relative: true - source: path + name: bloc_advanced_tools + sha256: "11f534f9e89561302de3bd07ab2dfe1c2dacaa8db9794ccdb57c55cfc02dffc6" + url: "https://pub.dev" + source: hosted version: "0.1.3" blurry_modal_progress_hud: dependency: "direct main" diff --git a/pubspec.yaml b/pubspec.yaml index 2f28727..a81d632 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -93,11 +93,11 @@ dependencies: xterm: ^4.0.0 zxing2: ^0.2.3 -dependency_overrides: - async_tools: - path: ../dart_async_tools - bloc_advanced_tools: - path: ../bloc_advanced_tools +# dependency_overrides: +# async_tools: +# path: ../dart_async_tools +# bloc_advanced_tools: +# path: ../bloc_advanced_tools # flutter_chat_ui: # path: ../flutter_chat_ui From 01ba275c7195e81b8b19f3f3ab0744e0a8af601f Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 23 Jun 2024 13:19:54 -0400 Subject: [PATCH 152/270] fix developer view keyboard, update mobilescanner/ios --- assets/i18n/en.json | 1 + ios/Podfile.lock | 126 +++++++++--------- ios/Runner.xcodeproj/project.pbxproj | 18 +++ .../xcshareddata/xcschemes/Runner.xcscheme | 7 + lib/veilid_processor/views/developer.dart | 117 +++++++++------- 5 files changed, 159 insertions(+), 110 deletions(-) diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 035682e..f9e0a10 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -198,6 +198,7 @@ "title": "Developer Logs", "command": "Command", "copied": "Selection copied", + "copied_all": "Copied all content", "cleared": "Logs cleared", "are_you_sure_clear": "Are you sure you want to clear the logs?" }, diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 1411245..166b1aa 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -4,68 +4,66 @@ PODS: - Flutter (1.0.0) - flutter_native_splash (0.0.1): - Flutter - - GoogleDataTransport (9.2.5): + - GoogleDataTransport (9.4.1): - GoogleUtilities/Environment (~> 7.7) - - nanopb (< 2.30910.0, >= 2.30908.0) + - nanopb (< 2.30911.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) - - GoogleMLKit/BarcodeScanning (4.0.0): + - GoogleMLKit/BarcodeScanning (6.0.0): - GoogleMLKit/MLKitCore - - MLKitBarcodeScanning (~> 3.0.0) - - GoogleMLKit/MLKitCore (4.0.0): - - MLKitCommon (~> 9.0.0) - - GoogleToolboxForMac/DebugUtils (2.3.2): - - GoogleToolboxForMac/Defines (= 2.3.2) - - GoogleToolboxForMac/Defines (2.3.2) - - GoogleToolboxForMac/Logger (2.3.2): - - GoogleToolboxForMac/Defines (= 2.3.2) - - "GoogleToolboxForMac/NSData+zlib (2.3.2)": - - GoogleToolboxForMac/Defines (= 2.3.2) - - "GoogleToolboxForMac/NSDictionary+URLArguments (2.3.2)": - - GoogleToolboxForMac/DebugUtils (= 2.3.2) - - GoogleToolboxForMac/Defines (= 2.3.2) - - "GoogleToolboxForMac/NSString+URLArguments (= 2.3.2)" - - "GoogleToolboxForMac/NSString+URLArguments (2.3.2)" - - GoogleUtilities/Environment (7.11.5): + - MLKitBarcodeScanning (~> 5.0.0) + - GoogleMLKit/MLKitCore (6.0.0): + - MLKitCommon (~> 11.0.0) + - GoogleToolboxForMac/Defines (4.2.1) + - GoogleToolboxForMac/Logger (4.2.1): + - GoogleToolboxForMac/Defines (= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (4.2.1)": + - GoogleToolboxForMac/Defines (= 4.2.1) + - GoogleUtilities/Environment (7.13.3): + - GoogleUtilities/Privacy - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.11.5): + - GoogleUtilities/Logger (7.13.3): - GoogleUtilities/Environment - - GoogleUtilities/UserDefaults (7.11.5): + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (7.13.3) + - GoogleUtilities/UserDefaults (7.13.3): - GoogleUtilities/Logger + - GoogleUtilities/Privacy - GoogleUtilitiesComponents (1.1.0): - GoogleUtilities/Logger - - GTMSessionFetcher/Core (2.3.0) - - MLImage (1.0.0-beta4) - - MLKitBarcodeScanning (3.0.0): - - MLKitCommon (~> 9.0) - - MLKitVision (~> 5.0) - - MLKitCommon (9.0.0): - - GoogleDataTransport (~> 9.0) - - GoogleToolboxForMac/Logger (~> 2.1) - - "GoogleToolboxForMac/NSData+zlib (~> 2.1)" - - "GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1)" - - GoogleUtilities/UserDefaults (~> 7.0) + - GTMSessionFetcher/Core (3.4.1) + - MLImage (1.0.0-beta5) + - MLKitBarcodeScanning (5.0.0): + - MLKitCommon (~> 11.0) + - MLKitVision (~> 7.0) + - MLKitCommon (11.0.0): + - GoogleDataTransport (< 10.0, >= 9.4.1) + - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" + - GoogleUtilities/UserDefaults (< 8.0, >= 7.13.0) - GoogleUtilitiesComponents (~> 1.0) - - GTMSessionFetcher/Core (< 3.0, >= 1.1) - - MLKitVision (5.0.0): - - GoogleToolboxForMac/Logger (~> 2.1) - - "GoogleToolboxForMac/NSData+zlib (~> 2.1)" - - GTMSessionFetcher/Core (< 3.0, >= 1.1) - - MLImage (= 1.0.0-beta4) - - MLKitCommon (~> 9.0) - - mobile_scanner (3.5.6): + - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) + - MLKitVision (7.0.0): + - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" + - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) + - MLImage (= 1.0.0-beta5) + - MLKitCommon (~> 11.0) + - mobile_scanner (5.1.1): + - Flutter + - GoogleMLKit/BarcodeScanning (~> 6.0.0) + - nanopb (2.30910.0): + - nanopb/decode (= 2.30910.0) + - nanopb/encode (= 2.30910.0) + - nanopb/decode (2.30910.0) + - nanopb/encode (2.30910.0) + - package_info_plus (0.4.5): - Flutter - - GoogleMLKit/BarcodeScanning (~> 4.0.0) - - nanopb (2.30909.0): - - nanopb/decode (= 2.30909.0) - - nanopb/encode (= 2.30909.0) - - nanopb/decode (2.30909.0) - - nanopb/encode (2.30909.0) - pasteboard (0.0.1): - Flutter - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - PromisesObjC (2.3.1) + - PromisesObjC (2.4.0) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -88,6 +86,7 @@ DEPENDENCIES: - Flutter (from `Flutter`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - pasteboard (from `.symlinks/plugins/pasteboard/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - share_plus (from `.symlinks/plugins/share_plus/ios`) @@ -122,6 +121,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_native_splash/ios" mobile_scanner: :path: ".symlinks/plugins/mobile_scanner/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" pasteboard: :path: ".symlinks/plugins/pasteboard/ios" path_provider_foundation: @@ -145,27 +146,28 @@ SPEC CHECKSUMS: camera_avfoundation: 759172d1a77ae7be0de08fc104cfb79738b8a59e Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 - GoogleDataTransport: 54dee9d48d14580407f8f5fbf2f496e92437a2f2 - GoogleMLKit: 2bd0dc6253c4d4f227aad460f69215a504b2980e - GoogleToolboxForMac: 8bef7c7c5cf7291c687cf5354f39f9db6399ad34 - GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084 + GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a + GoogleMLKit: 97ac7af399057e99182ee8edfa8249e3226a4065 + GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 + GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe - GTMSessionFetcher: 3a63d75eecd6aa32c2fc79f578064e1214dfdec2 - MLImage: 7bb7c4264164ade9bf64f679b40fb29c8f33ee9b - MLKitBarcodeScanning: 04e264482c5f3810cb89ebc134ef6b61e67db505 - MLKitCommon: c1b791c3e667091918d91bda4bba69a91011e390 - MLKitVision: 8baa5f46ee3352614169b85250574fde38c36f49 - mobile_scanner: 38dcd8a49d7d485f632b7de65e4900010187aef2 - nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 + GTMSessionFetcher: 8000756fc1c19d2e5697b90311f7832d2e33f6cd + MLImage: 1824212150da33ef225fbd3dc49f184cf611046c + MLKitBarcodeScanning: 10ca0845a6d15f2f6e911f682a1998b68b973e8b + MLKitCommon: afec63980417d29ffbb4790529a1b0a2291699e1 + MLKitVision: e858c5f125ecc288e4a31127928301eaba9ae0c1 + mobile_scanner: 8564358885a9253c43f822435b70f9345c87224f + nanopb: 438bc412db1928dac798aa6fd75726007be04262 + package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0 - path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c - PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad - shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 smart_auth: 4bedbc118723912d0e45a07e8ab34039c19e04f2 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec system_info_plus: 5393c8da281d899950d751713575fbf91c7709aa - url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586 + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe veilid: f5c2e662f91907b30cf95762619526ac3e4512fd PODFILE CHECKSUM: 5d504085cd7c7a4d71ee600d7af087cb60ab75b2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 72c3178..417e34c 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -139,6 +139,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 02C44F9283ADDE9FAAA73512 /* [CP] Embed Pods Frameworks */, + 61BE8A90522682C17620991D /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -230,6 +231,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 61BE8A90522682C17620991D /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5e31d3d..8b3e7d0 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -59,6 +59,13 @@ ReferencedContainer = "container:Runner.xcodeproj"> + + + + { } } + Future copyAll(BuildContext context) async { + final text = globalDebugTerminal.buffer.getText(); + await Clipboard.setData(ClipboardData(text: text)); + if (context.mounted) { + showInfoToast(context, translate('developer.copied_all')); + } + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -157,6 +165,13 @@ class _DeveloperPageState extends State { : () async { await copySelection(context); }), + IconButton( + icon: const Icon(Icons.copy_all), + color: scale.primaryScale.primaryText, + disabledColor: scale.primaryScale.primaryText.withAlpha(0x3F), + onPressed: () async { + await copyAll(context); + }), IconButton( icon: const Icon(Icons.clear_all), color: scale.primaryScale.primaryText, @@ -243,54 +258,60 @@ class _DeveloperPageState extends State { ).paddingLTRB(0, 0, 8, 0) ], ), - body: SafeArea( - child: Column(children: [ - Stack(alignment: AlignmentDirectional.center, children: [ - Image.asset('assets/images/ellet.png'), - TerminalView(globalDebugTerminal, - textStyle: kDefaultTerminalStyle, - controller: _terminalController, - //autofocus: true, - backgroundOpacity: _showEllet ? 0.75 : 1.0, - onSecondaryTapDown: (details, offset) async { - await copySelection(context); - }) - ]).expanded(), - TextField( - controller: _debugCommandController, - decoration: InputDecoration( - filled: true, - contentPadding: const EdgeInsets.fromLTRB(8, 2, 8, 2), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - fillColor: scale.primaryScale.subtleBackground, - hintText: translate('developer.command'), - suffixIcon: IconButton( - icon: Icon(Icons.send, - color: _debugCommandController.text.isEmpty - ? scale.primaryScale.primary.withAlpha(0x3F) - : scale.primaryScale.primary), - onPressed: _debugCommandController.text.isEmpty - ? null - : () async { - final debugCommand = _debugCommandController.text; - _debugCommandController.clear(); - await _sendDebugCommand(debugCommand); - }, - )), - onChanged: (_) { - setState(() => {}); - }, - onSubmitted: (debugCommand) async { - _debugCommandController.clear(); - await _sendDebugCommand(debugCommand); - }, - ).paddingAll(4) - ]))); + body: GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: SafeArea( + child: Column(children: [ + Stack(alignment: AlignmentDirectional.center, children: [ + Image.asset('assets/images/ellet.png'), + TerminalView(globalDebugTerminal, + textStyle: kDefaultTerminalStyle, + controller: _terminalController, + keyboardType: TextInputType.none, + //autofocus: true, + backgroundOpacity: _showEllet ? 0.75 : 1.0, + onSecondaryTapDown: (details, offset) async { + await copySelection(context); + }) + ]).expanded(), + TextField( + controller: _debugCommandController, + onTapOutside: (event) { + FocusManager.instance.primaryFocus?.unfocus(); + }, + decoration: InputDecoration( + filled: true, + contentPadding: const EdgeInsets.fromLTRB(8, 2, 8, 2), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + fillColor: scale.primaryScale.subtleBackground, + hintText: translate('developer.command'), + suffixIcon: IconButton( + icon: Icon(Icons.send, + color: _debugCommandController.text.isEmpty + ? scale.primaryScale.primary.withAlpha(0x3F) + : scale.primaryScale.primary), + onPressed: _debugCommandController.text.isEmpty + ? null + : () async { + final debugCommand = _debugCommandController.text; + _debugCommandController.clear(); + await _sendDebugCommand(debugCommand); + }, + )), + onChanged: (_) { + setState(() => {}); + }, + onSubmitted: (debugCommand) async { + _debugCommandController.clear(); + await _sendDebugCommand(debugCommand); + }, + ).paddingAll(4) + ])))); } @override From 8c89ce91cfa5825a9428b63eb7fce1bb67f98ba5 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 24 Jun 2024 23:44:08 +0000 Subject: [PATCH 153/270] ui cleanups --- ...ctive_account_page_controller_wrapper.dart | 37 ----- lib/layout/home/home.dart | 1 - .../home_account_ready_main.dart | 51 ++++--- .../main_pager/main_pager.dart | 57 ++++---- lib/layout/home/home_screen.dart | 130 +++++++++++------- packages/veilid_support/lib/src/config.dart | 17 ++- pubspec.lock | 16 +++ pubspec.yaml | 2 + 8 files changed, 175 insertions(+), 136 deletions(-) delete mode 100644 lib/layout/home/active_account_page_controller_wrapper.dart diff --git a/lib/layout/home/active_account_page_controller_wrapper.dart b/lib/layout/home/active_account_page_controller_wrapper.dart deleted file mode 100644 index 79314a1..0000000 --- a/lib/layout/home/active_account_page_controller_wrapper.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'dart:async'; - -import 'package:async_tools/async_tools.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:veilid_support/veilid_support.dart'; - -import '../../account_manager/account_manager.dart'; - -class ActiveAccountPageControllerWrapper { - ActiveAccountPageControllerWrapper(Locator locator, int initialPage) { - pageController = PageController(initialPage: initialPage, keepPage: false); - - final activeLocalAccountCubit = locator(); - _subscription = - activeLocalAccountCubit.stream.listen((activeLocalAccountRecordKey) { - singleFuture(this, () async { - final localAccounts = locator().state; - final activeIndex = localAccounts.indexWhere( - (x) => x.superIdentity.recordKey == activeLocalAccountRecordKey); - if (pageController.page == activeIndex) { - return; - } - await pageController.animateToPage(activeIndex, - duration: const Duration(milliseconds: 250), - curve: Curves.fastOutSlowIn); - }); - }); - } - - void dispose() { - unawaited(_subscription.cancel()); - } - - late PageController pageController; - late StreamSubscription _subscription; -} diff --git a/lib/layout/home/home.dart b/lib/layout/home/home.dart index cb0cef7..74990ef 100644 --- a/lib/layout/home/home.dart +++ b/lib/layout/home/home.dart @@ -1,4 +1,3 @@ -export 'active_account_page_controller_wrapper.dart'; export 'drawer_menu/drawer_menu.dart'; export 'home_account_invalid.dart'; export 'home_account_locked.dart'; 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 aa9b1ba..e28904e 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 @@ -1,4 +1,5 @@ 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'; @@ -35,27 +36,32 @@ class _HomeAccountReadyMainState extends State { final theme = Theme.of(context); final scale = theme.extension()!; - return Column(children: [ - Row(children: [ - IconButton( - icon: const Icon(Icons.menu), - color: scale.secondaryScale.borderText, - constraints: const BoxConstraints.expand(height: 64, width: 64), - style: ButtonStyle( - backgroundColor: - WidgetStateProperty.all(scale.primaryScale.hoverBorder), - shape: WidgetStateProperty.all(const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(16))))), - tooltip: translate('menu.settings_tooltip'), - onPressed: () async { - final ctrl = context.read(); - await ctrl.toggle?.call(); - //await GoRouterHelper(context).push('/settings'); - }).paddingLTRB(0, 0, 8, 0), - ProfileWidget(profile: profile).expanded(), - ]).paddingAll(8), - const MainPager().expanded() - ]); + return ColoredBox( + color: scale.primaryScale.subtleBorder, + child: Column(children: [ + Row(children: [ + IconButton( + icon: const Icon(Icons.menu), + color: scale.secondaryScale.borderText, + constraints: + const BoxConstraints.expand(height: 64, width: 64), + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + scale.primaryScale.hoverBorder), + shape: WidgetStateProperty.all( + const RoundedRectangleBorder( + borderRadius: + BorderRadius.all(Radius.circular(16))))), + tooltip: translate('menu.settings_tooltip'), + onPressed: () async { + final ctrl = context.read(); + await ctrl.toggle?.call(); + //await GoRouterHelper(context).push('/settings'); + }).paddingLTRB(0, 0, 8, 0), + ProfileWidget(profile: profile).expanded(), + ]).paddingAll(8), + MainPager(key: _mainPagerKey).expanded() + ])); }); Widget buildPhone(BuildContext context) => @@ -107,4 +113,7 @@ class _HomeAccountReadyMainState extends State { ) ? buildTablet(context) : buildPhone(context); + + //////////////////////////////////////////////////////////////////////////// + final _mainPagerKey = GlobalKey(debugLabel: '_mainPagerKey'); } 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 b51d72f..1e79344 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 @@ -28,25 +28,6 @@ class MainPager extends StatefulWidget { class MainPagerState extends State with TickerProviderStateMixin { ////////////////////////////////////////////////////////////////// - var _currentPage = 0; - final pageController = PreloadPageController(); - - final _selectedIconList = [Icons.person, Icons.chat]; - // final _unselectedIconList = [ - // Icons.chat_outlined, - // Icons.person_outlined - // ]; - final _fabIconList = [ - Icons.person_add_sharp, - Icons.add_comment_sharp, - ]; - final _bottomLabelList = [ - translate('pager.contacts'), - translate('pager.chats'), - ]; - - ////////////////////////////////////////////////////////////////// - @override void initState() { super.initState(); @@ -124,10 +105,10 @@ class MainPagerState extends State with TickerProviderStateMixin { } Widget _bottomSheetBuilder(BuildContext sheetContext, BuildContext context) { - if (_currentPage == 0) { + if (currentPage == 0) { // New contact invitation return newContactBottomSheetBuilder(sheetContext, context); - } else if (_currentPage == 1) { + } else if (currentPage == 1) { // New chat return newChatBottomSheetBuilder(sheetContext, context); } else { @@ -148,11 +129,12 @@ class MainPagerState extends State with TickerProviderStateMixin { body: NotificationListener( onNotification: onScrollNotification, child: PreloadPageView( + key: _pageViewKey, controller: pageController, preloadPagesCount: 2, onPageChanged: (index) { setState(() { - _currentPage = index; + currentPage = index; }); }, children: const [ @@ -176,7 +158,7 @@ class MainPagerState extends State with TickerProviderStateMixin { items: _buildBottomBarItems(), hasNotch: true, fabLocation: StylishBarFabLocation.end, - currentIndex: _currentPage, + currentIndex: currentPage, onTap: (index) async { await pageController.animateToPage(index, duration: 250.ms, curve: Curves.easeInOut); @@ -189,7 +171,7 @@ class MainPagerState extends State with TickerProviderStateMixin { foregroundColor: scale.secondaryScale.borderText, backgroundColor: scale.secondaryScale.hoverBorder, builder: (context) => Icon( - _fabIconList[_currentPage], + _fabIconList[currentPage], color: scale.secondaryScale.borderText, ), bottomSheetBuilder: (sheetContext) => @@ -198,10 +180,33 @@ class MainPagerState extends State with TickerProviderStateMixin { ); } + ////////////////////////////////////////////////////////////////// + + final _selectedIconList = [Icons.person, Icons.chat]; + // final _unselectedIconList = [ + // Icons.chat_outlined, + // Icons.person_outlined + // ]; + final _fabIconList = [ + Icons.person_add_sharp, + Icons.add_comment_sharp, + ]; + final _bottomLabelList = [ + translate('pager.contacts'), + translate('pager.chats'), + ]; + final _pageViewKey = GlobalKey(debugLabel: '_pageViewKey'); + + // key-accessible controller + int currentPage = 0; + final pageController = PreloadPageController(); + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(DiagnosticsProperty( - 'pageController', pageController)); + properties + ..add(IntProperty('currentPage', currentPage)) + ..add(DiagnosticsProperty( + 'pageController', pageController)); } } diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index 8ade6f5..939c1dc 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -1,16 +1,16 @@ import 'dart:math'; -import 'package:async_tools/async_tools.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart'; import 'package:provider/provider.dart'; +import 'package:transitioned_indexed_stack/transitioned_indexed_stack.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 'active_account_page_controller_wrapper.dart'; import 'drawer_menu/drawer_menu.dart'; import 'home_account_invalid.dart'; import 'home_account_locked.dart'; @@ -23,34 +23,71 @@ class HomeScreen extends StatefulWidget { @override HomeScreenState createState() => HomeScreenState(); + + static HomeScreenState? of(BuildContext context) => + context.findAncestorStateOfType(); } -class HomeScreenState extends State { +class HomeScreenState extends State + with SingleTickerProviderStateMixin { @override void initState() { + // Chat animation setup (open in phone mode) + _chatAnimationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 250), + ); + _chatAnimation = Tween( + begin: const Offset(1, 0), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _chatAnimationController, + curve: Curves.easeInOut, + )); + + // Account animation setup + super.initState(); } @override void dispose() { + _chatAnimationController.dispose(); super.dispose(); } Widget _buildAccountReadyDeviceSpecific(BuildContext context) { - final hasActiveChat = context.watch().state != null; if (responsiveVisibility( context: context, tablet: false, tabletLandscape: false, desktop: false)) { - if (hasActiveChat) { - return const HomeAccountReadyChat(); - } + return BlocConsumer( + listener: (context, activeChat) { + final hasActiveChat = activeChat != null; + if (hasActiveChat) { + _chatAnimationController.forward(); + } else { + _chatAnimationController.reset(); + } + }, + builder: (context, activeChat) => Stack( + children: [ + const HomeAccountReadyMain(), + Offstage( + offstage: activeChat == null, + child: SlideTransition( + position: _chatAnimation, + child: const HomeAccountReadyChat())), + ], + )); } return const HomeAccountReadyMain(); } - Widget _buildAccount(BuildContext context, TypedKey superIdentityRecordKey, + Widget _buildAccountPage( + BuildContext context, + TypedKey superIdentityRecordKey, PerAccountCollectionState perAccountCollectionState) { switch (perAccountCollectionState.accountInfo.status) { case AccountInfoStatus.accountInvalid: @@ -81,38 +118,25 @@ class HomeScreenState extends State { return const HomeNoActive(); } - return Provider( - lazy: false, - create: (context) => - ActiveAccountPageControllerWrapper(context.read, activeIndex), - dispose: (context, value) { - value.dispose(); - }, - child: Builder( - builder: (context) => PageView.builder( - onPageChanged: (idx) { - singleFuture(this, () async { - await AccountRepository.instance.switchToAccount( - localAccounts[idx].superIdentity.recordKey); - }); - }, - controller: context - .read() - .pageController, - itemCount: localAccounts.length, - itemBuilder: (context, index) { - final superIdentityRecordKey = - localAccounts[index].superIdentity.recordKey; - final perAccountCollectionState = - perAccountCollectionBlocMapState - .get(superIdentityRecordKey); - if (perAccountCollectionState == null) { - return HomeAccountMissing( - key: ValueKey(superIdentityRecordKey)); - } - return _buildAccount(context, superIdentityRecordKey, - perAccountCollectionState); - }))); + final accountPages = []; + + for (var i = 0; i < localAccounts.length; i++) { + final superIdentityRecordKey = localAccounts[i].superIdentity.recordKey; + final perAccountCollectionState = + perAccountCollectionBlocMapState.get(superIdentityRecordKey); + if (perAccountCollectionState == null) { + return HomeAccountMissing(key: ValueKey(superIdentityRecordKey)); + } + final accountPage = _buildAccountPage( + context, superIdentityRecordKey, perAccountCollectionState); + accountPages.add(KeyedSubtree.wrap(accountPage, i)); + } + + return SlideIndexedStack( + index: activeIndex, + beginSlideOffset: const Offset(1, 0), + children: accountPages, + ); } @override @@ -134,15 +158,20 @@ class HomeScreenState extends State { child: ZoomDrawer( controller: _zoomDrawerController, //menuBackgroundColor: Colors.transparent, - menuScreen: const DrawerMenu(), - mainScreen: DecoratedBox( - decoration: BoxDecoration( - color: scale.primaryScale.activeElementBackground), - child: Provider.value( - value: _zoomDrawerController, - child: Builder(builder: _buildAccountPageView))), + menuScreen: Builder(builder: (context) { + final zoomDrawer = ZoomDrawer.of(context); + zoomDrawer!.stateNotifier.addListener(() { + if (zoomDrawer.isOpen()) { + FocusManager.instance.primaryFocus?.unfocus(); + } + }); + return const DrawerMenu(); + }), + mainScreen: Provider.value( + value: _zoomDrawerController, + child: Builder(builder: _buildAccountPageView)), borderRadius: 24, - showShadow: true, + //showShadow: false, angle: 0, drawerShadowsBackgroundColor: theme.shadowColor, mainScreenOverlayColor: theme.shadowColor.withAlpha(0x3F), @@ -151,10 +180,15 @@ class HomeScreenState extends State { // reverseDuration: const Duration(milliseconds: 250), menuScreenTapClose: true, mainScreenTapClose: true, + //disableDragGesture: false, mainScreenScale: .25, slideWidth: min(360, MediaQuery.of(context).size.width * 0.9), ))); } + //////////////////////////////////////////////////////////////////////////// + final _zoomDrawerController = ZoomDrawerController(); + late final Animation _chatAnimation; + late final AnimationController _chatAnimationController; } diff --git a/packages/veilid_support/lib/src/config.dart b/packages/veilid_support/lib/src/config.dart index 889a8fd..430717e 100644 --- a/packages/veilid_support/lib/src/config.dart +++ b/packages/veilid_support/lib/src/config.dart @@ -2,6 +2,12 @@ import 'dart:io' show Platform; import 'package:veilid/veilid.dart'; +// ignore: do_not_use_environment +const bool _kReleaseMode = bool.fromEnvironment('dart.vm.product'); +// ignore: do_not_use_environment +const bool _kProfileMode = bool.fromEnvironment('dart.vm.profile'); +const bool _kDebugMode = !_kReleaseMode && !_kProfileMode; + Map getDefaultVeilidPlatformConfig( bool isWeb, String appName) { final ignoreLogTargetsStr = @@ -16,7 +22,9 @@ Map getDefaultVeilidPlatformConfig( logging: VeilidWASMConfigLogging( performance: VeilidWASMConfigLoggingPerformance( enabled: true, - level: VeilidConfigLogLevel.debug, + level: _kDebugMode + ? VeilidConfigLogLevel.debug + : VeilidConfigLogLevel.info, logsInTimings: true, logsInConsole: false, ignoreLogTargets: ignoreLogTargets), @@ -29,8 +37,11 @@ Map getDefaultVeilidPlatformConfig( return VeilidFFIConfig( logging: VeilidFFIConfigLogging( terminal: VeilidFFIConfigLoggingTerminal( - enabled: false, - level: VeilidConfigLogLevel.debug, + enabled: + _kDebugMode && (Platform.isIOS || Platform.isAndroid), + level: _kDebugMode + ? VeilidConfigLogLevel.debug + : VeilidConfigLogLevel.info, ignoreLogTargets: ignoreLogTargets), otlp: VeilidFFIConfigLoggingOtlp( enabled: false, diff --git a/pubspec.lock b/pubspec.lock index 16edf0a..5bdc0dc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.4.1" + animated_switcher_transitions: + dependency: "direct main" + description: + name: animated_switcher_transitions + sha256: "0f3ef1b46ab3f0b5efe784dcff55bbeabdc75a3b9bcbefbf2315468c9cec87c3" + url: "https://pub.dev" + source: hosted + version: "1.0.0" animated_theme_switcher: dependency: "direct main" description: @@ -1399,6 +1407,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + transitioned_indexed_stack: + dependency: "direct main" + description: + name: transitioned_indexed_stack + sha256: "8023abb5efe72e6d40cc3775fb03d7504c32ac918ec2ce7f9ba6804753820259" + url: "https://pub.dev" + source: hosted + version: "1.0.2" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a81d632..a1f097b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,6 +8,7 @@ environment: flutter: '>=3.22.1' dependencies: + animated_switcher_transitions: ^1.0.0 animated_theme_switcher: ^2.0.10 ansicolor: ^2.0.2 archive: ^3.6.1 @@ -83,6 +84,7 @@ dependencies: stack_trace: ^1.11.1 stream_transform: ^2.1.0 stylish_bottom_bar: ^1.1.0 + transitioned_indexed_stack: ^1.0.2 uuid: ^4.4.0 veilid: # veilid: ^0.0.1 From 9dfb8c3f7175d322d6e8192b7ec218be4b2d9e82 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 3 Jul 2024 20:59:54 -0400 Subject: [PATCH 154/270] cleanup --- assets/i18n/en.json | 16 +- .../repository/account_repository.dart | 17 +- .../views/edit_account_page.dart | 257 ++++++++++++------ .../views/new_account_page.dart | 77 +++--- .../views/profile_edit_form.dart | 10 +- lib/init.dart | 2 +- lib/layout/home/drawer_menu/drawer_menu.dart | 20 +- .../home_account_ready_chat.dart | 6 - .../home_account_ready_main.dart | 5 - lib/layout/home/home_screen.dart | 26 +- lib/router/cubit/router_cubit.dart | 1 + .../identity_support/identity_instance.dart | 1 - packages/veilid_support/lib/src/config.dart | 20 +- packages/veilid_support/pubspec.lock | 4 +- packages/veilid_support/pubspec.yaml | 2 + process_flame.sh | 3 + 16 files changed, 305 insertions(+), 162 deletions(-) create mode 100755 process_flame.sh diff --git a/assets/i18n/en.json b/assets/i18n/en.json index f9e0a10..cbf2762 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -37,11 +37,17 @@ "pronouns": "Pronouns", "remove_account": "Remove Account", "delete_identity": "Delete Identity", - "remove_account_confirm": "Confirm Account Removal?", + "remove_account_confirm": "Confirm Account Removal", "remove_account_description": "Remove account from this device only", - "delete_identity_description": "Delete identity and all messages completely", - "delete_identity_confirm_message": "This action is PERMANENT, and your identity will no longer be recoverable with the recovery key. This will not remove your messages you have sent from other people's devices.", - "confirm_are_you_sure": "Are you sure you want to do this?" + "remove_account_confirm_message": " • Your account will be removed from this device ONLY\n • Your identity will remain recoverable with the recovery key\n • Your messages and contacts will remain available on other devices\n", + "delete_identity_description": "Delete identity from all devices everywhere", + "delete_identity_confirm_message": "This action is PERMANENT, and your identity will no longer be recoverable with the recovery key. Restoring from backups will not recover your account!", + "delete_identity_confirm_message_details": "You will lose access to:\n • Your entire message history\n • Your contacts\n • This will not remove your messages you have sent from other people's devices\n", + "confirm_are_you_sure": "Are you sure you want to do this?", + "failed_to_remove": "Failed to remove account.\n\nTry again when you have a more stable network connection.", + "failed_to_delete": "Failed to delete identity.\n\nTry again when you have a more stable network connection.", + "account_removed": "Account removed successfully", + "identity_deleted": "Identity deleted successfully" }, "show_recovery_key_page": { "titlebar": "Save Recovery Key", @@ -58,6 +64,8 @@ "accept": "Accept", "reject": "Reject", "finish": "Finish", + "yes_proceed": "Yes, proceed", + "no_cancel": "No, cancel", "waiting_for_network": "Waiting For Network" }, "toast": { diff --git a/lib/account_manager/repository/account_repository.dart b/lib/account_manager/repository/account_repository.dart index fd15b43..f485374 100644 --- a/lib/account_manager/repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository.dart @@ -168,7 +168,15 @@ class AccountRepository { } /// Remove an account and wipe the messages for this account from this device - Future deleteLocalAccount(TypedKey superIdentityRecordKey) async { + Future deleteLocalAccount(TypedKey superIdentityRecordKey, + OwnedDHTRecordPointer? accountRecord) async { + // Delete the account record locally which causes a deep delete + // of all the contacts, invites, chats, and messages in the dht record + // pool + if (accountRecord != null) { + await DHTRecordPool.instance.deleteRecord(accountRecord.recordKey); + } + await logout(superIdentityRecordKey); final localAccounts = await _localAccounts.get(); @@ -178,8 +186,6 @@ class AccountRepository { await _localAccounts.set(newLocalAccounts); _streamController.add(AccountRepositoryChange.localAccounts); - // TO DO: wipe messages - return true; } @@ -367,6 +373,11 @@ class AccountRepository { return; } + if (logoutUser == activeLocalAccount) { + await switchToAccount( + _localAccounts.value.firstOrNull?.superIdentity.recordKey); + } + final logoutUserLogin = fetchUserLogin(logoutUser); if (logoutUserLogin == null) { // Already logged out diff --git a/lib/account_manager/views/edit_account_page.dart b/lib/account_manager/views/edit_account_page.dart index 0e57d77..449eb00 100644 --- a/lib/account_manager/views/edit_account_page.dart +++ b/lib/account_manager/views/edit_account_page.dart @@ -20,6 +20,7 @@ class EditAccountPage extends StatefulWidget { const EditAccountPage( {required this.superIdentityRecordKey, required this.existingProfile, + required this.accountRecord, super.key}); @override @@ -27,6 +28,7 @@ class EditAccountPage extends StatefulWidget { final TypedKey superIdentityRecordKey; final proto.Profile existingProfile; + final OwnedDHTRecordPointer accountRecord; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); @@ -34,7 +36,9 @@ class EditAccountPage extends StatefulWidget { ..add(DiagnosticsProperty( 'superIdentityRecordKey', superIdentityRecordKey)) ..add(DiagnosticsProperty( - 'existingProfile', existingProfile)); + 'existingProfile', existingProfile)) + ..add(DiagnosticsProperty( + 'accountRecord', accountRecord)); } } @@ -67,92 +71,179 @@ class _EditAccountPageState extends State { }, ); + Future _onRemoveAccount() async { + final confirmed = await StyledDialog.show( + context: context, + title: translate('edit_account_page.remove_account_confirm'), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Text(translate('edit_account_page.remove_account_confirm_message')) + .paddingLTRB(24, 24, 24, 0), + Text(translate('edit_account_page.confirm_are_you_sure')) + .paddingAll(8), + Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: Row(mainAxisSize: MainAxisSize.min, children: [ + const Icon(Icons.cancel, size: 16).paddingLTRB(0, 0, 4, 0), + Text(translate('button.no_cancel')).paddingLTRB(0, 0, 4, 0) + ])), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + child: Row(mainAxisSize: MainAxisSize.min, children: [ + const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0), + Text(translate('button.yes_proceed')).paddingLTRB(0, 0, 4, 0) + ])) + ]).paddingAll(24) + ])); + if (confirmed != null && confirmed && mounted) { + // dismiss the keyboard by unfocusing the textfield + FocusScope.of(context).unfocus(); + + try { + setState(() { + _isInAsyncCall = true; + }); + try { + final success = await AccountRepository.instance.deleteLocalAccount( + widget.superIdentityRecordKey, widget.accountRecord); + if (success && mounted) { + showInfoToast( + context, translate('edit_account_page.account_removed')); + GoRouterHelper(context).pop(); + } else if (mounted) { + showErrorToast( + context, translate('edit_account_page.failed_to_remove')); + } + } finally { + if (mounted) { + setState(() { + _isInAsyncCall = false; + }); + } + } + } on Exception catch (e) { + if (mounted) { + await showErrorModal( + context, translate('new_account_page.error'), 'Exception: $e'); + } + } + } + } + + Future _onDeleteIdentity() async { + // + } + + Future _onSubmit(GlobalKey formKey) async { + // dismiss the keyboard by unfocusing the textfield + FocusScope.of(context).unfocus(); + + try { + final name = formKey + .currentState!.fields[EditProfileForm.formFieldName]!.value as String; + final pronouns = formKey.currentState! + .fields[EditProfileForm.formFieldPronouns]!.value as String? ?? + ''; + final newProfile = widget.existingProfile.deepCopy() + ..name = name + ..pronouns = pronouns + ..timestamp = Veilid.instance.now().toInt64(); + + setState(() { + _isInAsyncCall = true; + }); + try { + // Look up account cubit for this specific account + final perAccountCollectionBlocMapCubit = + context.read(); + final accountRecordCubit = await perAccountCollectionBlocMapCubit + .operate(widget.superIdentityRecordKey, + closure: (c) async => c.accountRecordCubit); + if (accountRecordCubit == null) { + return; + } + + // Update account profile DHT record + // This triggers ConversationCubits to update + await accountRecordCubit.updateProfile(newProfile); + + // Update local account profile + await AccountRepository.instance + .editAccountProfile(widget.superIdentityRecordKey, newProfile); + + if (mounted) { + Navigator.canPop(context) + ? GoRouterHelper(context).pop() + : GoRouterHelper(context).go('/'); + } + } finally { + if (mounted) { + setState(() { + _isInAsyncCall = false; + }); + } + } + } on Exception catch (e) { + if (mounted) { + await showErrorModal( + context, translate('edit_account_page.error'), 'Exception: $e'); + } + } + } + @override Widget build(BuildContext context) { final displayModalHUD = _isInAsyncCall; return Scaffold( - // resizeToAvoidBottomInset: false, - appBar: DefaultAppBar( - title: Text(translate('edit_account_page.titlebar')), - leading: Navigator.canPop(context) - ? IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () { - Navigator.pop(context); - }, - ) - : null, - actions: [ - const SignalStrengthMeterWidget(), - IconButton( - icon: const Icon(Icons.settings), - tooltip: translate('menu.settings_tooltip'), - onPressed: () async { - await GoRouterHelper(context).push('/settings'); - }) - ]), - body: _editAccountForm( - context, - onSubmit: (formKey) async { - // dismiss the keyboard by unfocusing the textfield - FocusScope.of(context).unfocus(); - - try { - final name = formKey.currentState! - .fields[EditProfileForm.formFieldName]!.value as String; - final pronouns = formKey - .currentState! - .fields[EditProfileForm.formFieldPronouns]! - .value as String? ?? - ''; - final newProfile = widget.existingProfile.deepCopy() - ..name = name - ..pronouns = pronouns - ..timestamp = Veilid.instance.now().toInt64(); - - setState(() { - _isInAsyncCall = true; - }); - try { - // Look up account cubit for this specific account - final perAccountCollectionBlocMapCubit = - context.read(); - final accountRecordCubit = await perAccountCollectionBlocMapCubit - .operate(widget.superIdentityRecordKey, - closure: (c) async => c.accountRecordCubit); - if (accountRecordCubit == null) { - return; - } - - // Update account profile DHT record - // This triggers ConversationCubits to update - await accountRecordCubit.updateProfile(newProfile); - - // Update local account profile - await AccountRepository.instance.editAccountProfile( - widget.superIdentityRecordKey, newProfile); - - if (context.mounted) { - Navigator.canPop(context) - ? GoRouterHelper(context).pop() - : GoRouterHelper(context).go('/'); - } - } finally { - if (mounted) { - setState(() { - _isInAsyncCall = false; - }); - } - } - } on Exception catch (e) { - if (context.mounted) { - await showErrorModal(context, - translate('edit_account_page.error'), 'Exception: $e'); - } - } - }, - ).paddingSymmetric(horizontal: 24, vertical: 8), - ).withModalHUD(context, displayModalHUD); + // resizeToAvoidBottomInset: false, + appBar: DefaultAppBar( + title: Text(translate('edit_account_page.titlebar')), + leading: Navigator.canPop(context) + ? IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + Navigator.pop(context); + }, + ) + : null, + actions: [ + const SignalStrengthMeterWidget(), + IconButton( + icon: const Icon(Icons.settings), + tooltip: translate('menu.settings_tooltip'), + onPressed: () async { + await GoRouterHelper(context).push('/settings'); + }) + ]), + body: Column(children: [ + _editAccountForm( + context, + onSubmit: _onSubmit, + ).expanded(), + Text(translate('edit_account_page.remove_account_description')), + ElevatedButton( + onPressed: _onRemoveAccount, + child: Row(mainAxisSize: MainAxisSize.min, children: [ + const Icon(Icons.person_remove_alt_1, size: 16) + .paddingLTRB(0, 0, 4, 0), + Text(translate('edit_account_page.remove_account')) + .paddingLTRB(0, 0, 4, 0) + ])).paddingLTRB(0, 8, 0, 24), + Text(translate('edit_account_page.delete_identity_description')), + ElevatedButton( + onPressed: _onDeleteIdentity, + child: Row(mainAxisSize: MainAxisSize.min, children: [ + const Icon(Icons.person_off, size: 16) + .paddingLTRB(0, 0, 4, 0), + Text(translate('edit_account_page.delete_identity')) + .paddingLTRB(0, 0, 4, 0) + ])).paddingLTRB(0, 8, 0, 24) + ]).paddingSymmetric(horizontal: 24, vertical: 8)) + .withModalHUD(context, displayModalHUD); } } diff --git a/lib/account_manager/views/new_account_page.dart b/lib/account_manager/views/new_account_page.dart index 65d57ea..f38bd3a 100644 --- a/lib/account_manager/views/new_account_page.dart +++ b/lib/account_manager/views/new_account_page.dart @@ -52,6 +52,43 @@ class _NewAccountPageState extends State { onSubmit: !canSubmit ? null : onSubmit); } + Future _onSubmit(GlobalKey formKey) async { + // dismiss the keyboard by unfocusing the textfield + FocusScope.of(context).unfocus(); + + try { + final name = formKey + .currentState!.fields[EditProfileForm.formFieldName]!.value as String; + final pronouns = formKey.currentState! + .fields[EditProfileForm.formFieldPronouns]!.value as String? ?? + ''; + final newProfile = proto.Profile() + ..name = name + ..pronouns = pronouns; + + setState(() { + _isInAsyncCall = true; + }); + try { + final superSecret = await AccountRepository.instance + .createWithNewSuperIdentity(newProfile); + GoRouterHelper(context) + .pushReplacement('/new_account/recovery_key', extra: superSecret); + } finally { + if (mounted) { + setState(() { + _isInAsyncCall = false; + }); + } + } + } on Exception catch (e) { + if (mounted) { + await showErrorModal( + context, translate('new_account_page.error'), 'Exception: $e'); + } + } + } + @override Widget build(BuildContext context) { final displayModalHUD = _isInAsyncCall; @@ -79,45 +116,7 @@ class _NewAccountPageState extends State { ]), body: _newAccountForm( context, - onSubmit: (formKey) async { - // dismiss the keyboard by unfocusing the textfield - FocusScope.of(context).unfocus(); - - try { - final name = formKey.currentState! - .fields[EditProfileForm.formFieldName]!.value as String; - final pronouns = formKey - .currentState! - .fields[EditProfileForm.formFieldPronouns]! - .value as String? ?? - ''; - final newProfile = proto.Profile() - ..name = name - ..pronouns = pronouns; - - setState(() { - _isInAsyncCall = true; - }); - try { - final superSecret = await AccountRepository.instance - .createWithNewSuperIdentity(newProfile); - GoRouterHelper(context).pushReplacement( - '/new_account/recovery_key', - extra: superSecret); - } finally { - if (mounted) { - setState(() { - _isInAsyncCall = false; - }); - } - } - } on Exception catch (e) { - if (context.mounted) { - await showErrorModal(context, translate('new_account_page.error'), - 'Exception: $e'); - } - } - }, + onSubmit: _onSubmit, ).paddingSymmetric(horizontal: 24, vertical: 8), ).withModalHUD(context, displayModalHUD); } diff --git a/lib/account_manager/views/profile_edit_form.dart b/lib/account_manager/views/profile_edit_form.dart index 2e14249..d7b3c7d 100644 --- a/lib/account_manager/views/profile_edit_form.dart +++ b/lib/account_manager/views/profile_edit_form.dart @@ -99,9 +99,13 @@ class _EditProfileFormState extends State { await widget.onSubmit!(_formKey); } }, - child: Text((widget.onSubmit == null) - ? widget.submitDisabledText - : widget.submitText), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0), + Text((widget.onSubmit == null) + ? widget.submitDisabledText + : widget.submitText) + .paddingLTRB(0, 0, 4, 0) + ]), ).paddingSymmetric(vertical: 4).alignAtCenterRight(), ], ), diff --git a/lib/init.dart b/lib/init.dart index cd01f97..fc836a5 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -15,7 +15,7 @@ class VeilidChatGlobalInit { Future _initializeVeilid() async { // Init Veilid Veilid.instance.initializeVeilidCore( - getDefaultVeilidPlatformConfig(kIsWeb, VeilidChatApp.name)); + await getDefaultVeilidPlatformConfig(kIsWeb, VeilidChatApp.name)); // Veilid logging initVeilidLog(kDebugMode); diff --git a/lib/layout/home/drawer_menu/drawer_menu.dart b/lib/layout/home/drawer_menu/drawer_menu.dart index 218d7ed..b8df2ea 100644 --- a/lib/layout/home/drawer_menu/drawer_menu.dart +++ b/lib/layout/home/drawer_menu/drawer_menu.dart @@ -39,11 +39,11 @@ class _DrawerMenuState extends State { }); } - void _doEditClick( - TypedKey superIdentityRecordKey, proto.Profile existingProfile) { + void _doEditClick(TypedKey superIdentityRecordKey, + proto.Profile existingProfile, OwnedDHTRecordPointer accountRecord) { singleFuture(this, () async { await GoRouterHelper(context).push('/edit_account', - extra: [superIdentityRecordKey, existingProfile]); + extra: [superIdentityRecordKey, existingProfile, accountRecord]); }); } @@ -128,10 +128,10 @@ class _DrawerMenuState extends State { final superIdentityRecordKey = la.superIdentity.recordKey; // See if this account is logged in - final avAccountRecordState = perAccountCollectionBlocMapState - .get(superIdentityRecordKey) - ?.avAccountRecordState; - if (avAccountRecordState != null) { + final perAccountState = + perAccountCollectionBlocMapState.get(superIdentityRecordKey); + final avAccountRecordState = perAccountState?.avAccountRecordState; + if (perAccountState != null && avAccountRecordState != null) { // Account is logged in final scale = theme.extension()!.tertiaryScale; final loggedInAccount = avAccountRecordState.when( @@ -144,7 +144,11 @@ class _DrawerMenuState extends State { _doSwitchClick(superIdentityRecordKey); }, footerCallback: () { - _doEditClick(superIdentityRecordKey, value.profile); + _doEditClick( + superIdentityRecordKey, + value.profile, + perAccountState.accountInfo.userLogin!.accountRecordInfo + .accountRecord); }), loading: () => _wrapInBox( child: buildProgressIndicator(), 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 6e1868c..cd56aa1 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,7 +2,6 @@ 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}); @@ -15,11 +14,6 @@ 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 e28904e..f682929 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 @@ -23,11 +23,6 @@ class _HomeAccountReadyMainState extends State { @override void initState() { super.initState(); - - WidgetsBinding.instance.addPostFrameCallback((_) async { - await changeWindowSetup( - TitleBarStyle.normal, OrientationCapability.normal); - }); } Widget buildUserPanel() => Builder(builder: (context) { diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index 939c1dc..47d3d64 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -45,8 +45,20 @@ class HomeScreenState extends State curve: Curves.easeInOut, )); - // Account animation setup + WidgetsBinding.instance.addPostFrameCallback((_) async { + final localAccounts = context.read().state; + final activeLocalAccount = context.read().state; + final activeIndex = localAccounts + .indexWhere((x) => x.superIdentity.recordKey == activeLocalAccount); + final canClose = activeIndex != -1; + await changeWindowSetup( + TitleBarStyle.normal, OrientationCapability.normal); + + if (!canClose) { + await _zoomDrawerController.open!(); + } + }); super.initState(); } @@ -152,6 +164,12 @@ class HomeScreenState extends State scale.tertiaryScale.appBackground, ]); + final localAccounts = context.watch().state; + final activeLocalAccount = context.watch().state; + final activeIndex = localAccounts + .indexWhere((x) => x.superIdentity.recordKey == activeLocalAccount); + final canClose = activeIndex != -1; + return SafeArea( child: DecoratedBox( decoration: BoxDecoration(gradient: gradient), @@ -178,9 +196,9 @@ class HomeScreenState extends State openCurve: Curves.fastEaseInToSlowEaseOut, // duration: const Duration(milliseconds: 250), // reverseDuration: const Duration(milliseconds: 250), - menuScreenTapClose: true, - mainScreenTapClose: true, - //disableDragGesture: false, + menuScreenTapClose: canClose, + mainScreenTapClose: canClose, + disableDragGesture: !canClose, mainScreenScale: .25, slideWidth: min(360, MediaQuery.of(context).size.width * 0.9), ))); diff --git a/lib/router/cubit/router_cubit.dart b/lib/router/cubit/router_cubit.dart index 4b4061e..6323da3 100644 --- a/lib/router/cubit/router_cubit.dart +++ b/lib/router/cubit/router_cubit.dart @@ -69,6 +69,7 @@ class RouterCubit extends Cubit { return EditAccountPage( superIdentityRecordKey: extra[0]! as TypedKey, existingProfile: extra[1]! as proto.Profile, + accountRecord: extra[2]! as OwnedDHTRecordPointer, ); }, ), diff --git a/packages/veilid_support/lib/identity_support/identity_instance.dart b/packages/veilid_support/lib/identity_support/identity_instance.dart index b11e223..abcc4f0 100644 --- a/packages/veilid_support/lib/identity_support/identity_instance.dart +++ b/packages/veilid_support/lib/identity_support/identity_instance.dart @@ -5,7 +5,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import '../src/veilid_log.dart'; import '../veilid_support.dart'; -import 'exceptions.dart'; part 'identity_instance.freezed.dart'; part 'identity_instance.g.dart'; diff --git a/packages/veilid_support/lib/src/config.dart b/packages/veilid_support/lib/src/config.dart index 430717e..a8b5ea0 100644 --- a/packages/veilid_support/lib/src/config.dart +++ b/packages/veilid_support/lib/src/config.dart @@ -1,5 +1,7 @@ import 'dart:io' show Platform; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; import 'package:veilid/veilid.dart'; // ignore: do_not_use_environment @@ -8,8 +10,8 @@ const bool _kReleaseMode = bool.fromEnvironment('dart.vm.product'); const bool _kProfileMode = bool.fromEnvironment('dart.vm.profile'); const bool _kDebugMode = !_kReleaseMode && !_kProfileMode; -Map getDefaultVeilidPlatformConfig( - bool isWeb, String appName) { +Future> getDefaultVeilidPlatformConfig( + bool isWeb, String appName) async { final ignoreLogTargetsStr = // ignore: do_not_use_environment const String.fromEnvironment('IGNORE_LOG_TARGETS').trim(); @@ -17,6 +19,16 @@ Map getDefaultVeilidPlatformConfig( ? [] : ignoreLogTargetsStr.split(',').map((e) => e.trim()).toList(); + // ignore: do_not_use_environment + var flamePathStr = const String.fromEnvironment('FLAME').trim(); + if (flamePathStr == '1') { + flamePathStr = p.join( + (await getApplicationSupportDirectory()).absolute.path, + '$appName.folded'); + // ignore: avoid_print + print('Flame data logged to $flamePathStr'); + } + if (isWeb) { return VeilidWASMConfig( logging: VeilidWASMConfigLogging( @@ -52,7 +64,9 @@ Map getDefaultVeilidPlatformConfig( api: VeilidFFIConfigLoggingApi( enabled: true, level: VeilidConfigLogLevel.info, - ignoreLogTargets: ignoreLogTargets))) + ignoreLogTargets: ignoreLogTargets), + flame: VeilidFFIConfigLoggingFlame( + enabled: flamePathStr.isNotEmpty, path: flamePathStr))) .toJson(); } diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index f38de9a..2f71a50 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -428,7 +428,7 @@ packages: source: hosted version: "2.1.0" path: - dependency: transitive + dependency: "direct main" description: name: path sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" @@ -436,7 +436,7 @@ packages: source: hosted version: "1.9.0" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index 01970ba..5fb9af9 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -19,6 +19,8 @@ dependencies: loggy: ^2.0.3 meta: ^1.12.0 + path: ^1.9.0 + path_provider: ^2.1.3 protobuf: ^3.1.0 veilid: # veilid: ^0.0.1 diff --git a/process_flame.sh b/process_flame.sh new file mode 100755 index 0000000..8f03418 --- /dev/null +++ b/process_flame.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cat "/Users/$USER/Library/Containers/com.veilid.veilidchat/Data/Library/Application Support/com.veilid.veilidchat/VeilidChat.folded" | inferno-flamegraph -c purple --fontsize 8 --height 24 --title "VeilidChat" --factor 0.000000001 --countname secs > /tmp/veilidchat.svg +cat "/Users/$USER/Library/Containers/com.veilid.veilidchat/Data/Library/Application Support/com.veilid.veilidchat/VeilidChat.folded" | inferno-flamegraph --reverse -c aqua --fontsize 8 --height 24 --title "VeilidChat Reverse" --factor 0.000000001 --countname secs > /tmp/veilidchat-reverse.svg From 94988718e80c49d53a6670033a3d1dd66851dd8d Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 4 Jul 2024 23:09:37 -0400 Subject: [PATCH 155/270] message length limit --- assets/i18n/en.json | 16 +- .../repository/account_repository.dart | 33 +- .../views/edit_account_page.dart | 72 +++- lib/chat/cubits/chat_component_cubit.dart | 1 + .../cubits/single_contact_messages_cubit.dart | 33 +- lib/chat/models/chat_component_state.dart | 5 +- .../models/chat_component_state.freezed.dart | 26 +- lib/chat/views/chat_component_widget.dart | 199 +++++---- lib/theme/models/chat_theme.dart | 397 ++++++++++++++++++ .../lib/identity_support/exceptions.dart | 3 +- .../identity_support/identity_instance.dart | 72 +++- .../lib/src/persistent_queue.dart | 63 ++- pubspec.lock | 4 +- 13 files changed, 792 insertions(+), 132 deletions(-) diff --git a/assets/i18n/en.json b/assets/i18n/en.json index cbf2762..3ae7b8a 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -36,18 +36,19 @@ "name": "Name", "pronouns": "Pronouns", "remove_account": "Remove Account", - "delete_identity": "Delete Identity", + "destroy_account": "Destroy Account", "remove_account_confirm": "Confirm Account Removal", "remove_account_description": "Remove account from this device only", "remove_account_confirm_message": " • Your account will be removed from this device ONLY\n • Your identity will remain recoverable with the recovery key\n • Your messages and contacts will remain available on other devices\n", - "delete_identity_description": "Delete identity from all devices everywhere", - "delete_identity_confirm_message": "This action is PERMANENT, and your identity will no longer be recoverable with the recovery key. Restoring from backups will not recover your account!", - "delete_identity_confirm_message_details": "You will lose access to:\n • Your entire message history\n • Your contacts\n • This will not remove your messages you have sent from other people's devices\n", + "destroy_account_confirm": "Confirm Account Destruction", + "destroy_account_description": "Destroy account, removing it completely from all devices everywhere", + "destroy_account_confirm_message": "This action is PERMANENT, and your VeilidChat account will no longer be recoverable with the recovery key. Restoring from backups will not recover your account!", + "destroy_account_confirm_message_details": "You will lose access to:\n • Your entire message history\n • Your contacts\n • This will not remove your messages you have sent from other people's devices\n", "confirm_are_you_sure": "Are you sure you want to do this?", "failed_to_remove": "Failed to remove account.\n\nTry again when you have a more stable network connection.", - "failed_to_delete": "Failed to delete identity.\n\nTry again when you have a more stable network connection.", + "failed_to_destroy": "Failed to destroy account.\n\nTry again when you have a more stable network connection.", "account_removed": "Account removed successfully", - "identity_deleted": "Identity deleted successfully" + "account_destroyed": "Account destroyed successfully" }, "show_recovery_key_page": { "titlebar": "Save Recovery Key", @@ -103,7 +104,8 @@ }, "chat": { "start_a_conversation": "Start A Conversation", - "say_something": "Say Something" + "say_something": "Say Something", + "message_too_long": "Message too long" }, "create_invitation_dialog": { "title": "Create Contact Invitation", diff --git a/lib/account_manager/repository/account_repository.dart b/lib/account_manager/repository/account_repository.dart index f485374..52351af 100644 --- a/lib/account_manager/repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository.dart @@ -7,7 +7,7 @@ import '../../../proto/proto.dart' as proto; import '../../tools/tools.dart'; import '../models/models.dart'; -const String veilidChatAccountKey = 'com.veilid.veilidchat'; +const String veilidChatApplicationId = 'com.veilid.veilidchat'; enum AccountRepositoryChange { localAccounts, userLogins, activeLocalAccount } @@ -194,6 +194,33 @@ class AccountRepository { /// Recover an account with the master identity secret /// Delete an account from all devices + Future destroyAccount(TypedKey superIdentityRecordKey, + OwnedDHTRecordPointer accountRecord) async { + // Get which local account we want to fetch the profile for + final localAccount = fetchLocalAccount(superIdentityRecordKey); + if (localAccount == null) { + return false; + } + + // See if we've logged into this account or if it is locked + final userLogin = fetchUserLogin(superIdentityRecordKey); + if (userLogin == null) { + return false; + } + + final success = await localAccount.superIdentity.currentInstance + .removeAccount( + superRecordKey: localAccount.superIdentity.recordKey, + secretKey: userLogin.identitySecret.value, + applicationId: veilidChatApplicationId, + removeAccountCallback: (accountRecordInfos) async => + accountRecordInfos.singleOrNull); + if (!success) { + return false; + } + + return deleteLocalAccount(superIdentityRecordKey, accountRecord); + } Future switchToAccount(TypedKey? superIdentityRecordKey) async { final activeLocalAccount = await _activeLocalAccount.get(); @@ -231,7 +258,7 @@ class AccountRepository { await superIdentity.currentInstance.addAccount( superRecordKey: superIdentity.recordKey, secretKey: identitySecret, - accountKey: veilidChatAccountKey, + applicationId: veilidChatApplicationId, createAccountCallback: (parent) async { // Make empty contact list log.debug('Creating contacts list'); @@ -305,7 +332,7 @@ class AccountRepository { .readAccount( superRecordKey: superIdentity.recordKey, secretKey: identitySecret, - accountKey: veilidChatAccountKey); + applicationId: veilidChatApplicationId); if (accountRecordInfoList.length > 1) { throw IdentityException.limitExceeded; } else if (accountRecordInfoList.isEmpty) { diff --git a/lib/account_manager/views/edit_account_page.dart b/lib/account_manager/views/edit_account_page.dart index 449eb00..36a9c28 100644 --- a/lib/account_manager/views/edit_account_page.dart +++ b/lib/account_manager/views/edit_account_page.dart @@ -134,8 +134,70 @@ class _EditAccountPageState extends State { } } - Future _onDeleteIdentity() async { - // + Future _onDestroyAccount() async { + final confirmed = await StyledDialog.show( + context: context, + title: translate('edit_account_page.destroy_account_confirm'), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Text(translate('edit_account_page.destroy_account_confirm_message')) + .paddingLTRB(24, 24, 24, 0), + Text(translate( + 'edit_account_page.destroy_account_confirm_message_details')) + .paddingLTRB(24, 24, 24, 0), + Text(translate('edit_account_page.confirm_are_you_sure')) + .paddingAll(8), + Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: Row(mainAxisSize: MainAxisSize.min, children: [ + const Icon(Icons.cancel, size: 16).paddingLTRB(0, 0, 4, 0), + Text(translate('button.no_cancel')).paddingLTRB(0, 0, 4, 0) + ])), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + child: Row(mainAxisSize: MainAxisSize.min, children: [ + const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0), + Text(translate('button.yes_proceed')).paddingLTRB(0, 0, 4, 0) + ])) + ]).paddingAll(24) + ])); + if (confirmed != null && confirmed && mounted) { + // dismiss the keyboard by unfocusing the textfield + FocusScope.of(context).unfocus(); + + try { + setState(() { + _isInAsyncCall = true; + }); + try { + final success = await AccountRepository.instance.destroyAccount( + widget.superIdentityRecordKey, widget.accountRecord); + if (success && mounted) { + showInfoToast( + context, translate('edit_account_page.account_destroyed')); + GoRouterHelper(context).pop(); + } else if (mounted) { + showErrorToast( + context, translate('edit_account_page.failed_to_destroy')); + } + } finally { + if (mounted) { + setState(() { + _isInAsyncCall = false; + }); + } + } + } on Exception catch (e) { + if (mounted) { + await showErrorModal( + context, translate('new_account_page.error'), 'Exception: $e'); + } + } + } } Future _onSubmit(GlobalKey formKey) async { @@ -234,13 +296,13 @@ class _EditAccountPageState extends State { Text(translate('edit_account_page.remove_account')) .paddingLTRB(0, 0, 4, 0) ])).paddingLTRB(0, 8, 0, 24), - Text(translate('edit_account_page.delete_identity_description')), + Text(translate('edit_account_page.destroy_account_description')), ElevatedButton( - onPressed: _onDeleteIdentity, + onPressed: _onDestroyAccount, child: Row(mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.person_off, size: 16) .paddingLTRB(0, 0, 4, 0), - Text(translate('edit_account_page.delete_identity')) + Text(translate('edit_account_page.destroy_account')) .paddingLTRB(0, 0, 4, 0) ])).paddingLTRB(0, 8, 0, 24) ]).paddingSymmetric(horizontal: 24, vertical: 8)) diff --git a/lib/chat/cubits/chat_component_cubit.dart b/lib/chat/cubits/chat_component_cubit.dart index 83e3d21..12ed135 100644 --- a/lib/chat/cubits/chat_component_cubit.dart +++ b/lib/chat/cubits/chat_component_cubit.dart @@ -42,6 +42,7 @@ class ChatComponentCubit extends Cubit { super(ChatComponentState( chatKey: GlobalKey(), scrollController: AutoScrollController(), + textEditingController: InputTextFieldController(), localUser: null, remoteUsers: const IMap.empty(), historicalRemoteUsers: const IMap.empty(), diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 9854535..ce4368a 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -97,11 +97,13 @@ class SingleContactMessagesCubit extends Cubit { // Initialize everything Future _init() async { _unsentMessagesQueue = PersistentQueue( - table: 'SingleContactUnsentMessages', - key: _remoteConversationRecordKey.toString(), - fromBuffer: proto.Message.fromBuffer, - closure: _processUnsentMessages, - ); + table: 'SingleContactUnsentMessages', + key: _remoteConversationRecordKey.toString(), + fromBuffer: proto.Message.fromBuffer, + closure: _processUnsentMessages, + onError: (e, sp) { + log.error('Exception while processing unsent messages: $e\n$sp\n'); + }); // Make crypto await _initCrypto(); @@ -297,16 +299,23 @@ class SingleContactMessagesCubit extends Cubit { proto.Message? previousMessage; final processedMessages = messages.toList(); for (final message in processedMessages) { - await _processMessageToSend(message, previousMessage); - previousMessage = message; + try { + await _processMessageToSend(message, previousMessage); + previousMessage = message; + } on Exception catch (e) { + log.error('Exception processing unsent message: $e'); + } } // _sendingMessages = messages; // _renderState(); - - await _sentMessagesCubit!.operateAppendEventual((writer) => - writer.addAll(messages.map((m) => m.writeToBuffer()).toList())); + try { + await _sentMessagesCubit!.operateAppendEventual((writer) => + writer.addAll(messages.map((m) => m.writeToBuffer()).toList())); + } on Exception catch (e) { + log.error('Exception appending unsent messages: $e'); + } // _sendingMessages = const IList.empty(); } @@ -403,6 +412,10 @@ class SingleContactMessagesCubit extends Cubit { ..author = _accountInfo.identityTypedPublicKey.toProto() ..timestamp = Veilid.instance.now().toInt64(); + if ((message.writeToBuffer().lengthInBytes + 256) > 4096) { + throw const FormatException('message is too long'); + } + // Put in the queue _unsentMessagesQueue.addSync(message); diff --git a/lib/chat/models/chat_component_state.dart b/lib/chat/models/chat_component_state.dart index ae69da7..b06b413 100644 --- a/lib/chat/models/chat_component_state.dart +++ b/lib/chat/models/chat_component_state.dart @@ -2,7 +2,8 @@ import 'package:async_tools/async_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart' show Message, User; -import 'package:flutter_chat_ui/flutter_chat_ui.dart' show ChatState; +import 'package:flutter_chat_ui/flutter_chat_ui.dart' + show ChatState, InputTextFieldController; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:veilid_support/veilid_support.dart'; @@ -19,6 +20,8 @@ class ChatComponentState with _$ChatComponentState { required GlobalKey chatKey, // ScrollController for the chat required AutoScrollController scrollController, + // TextEditingController for the chat + required InputTextFieldController textEditingController, // Local user required User? localUser, // Active remote users diff --git a/lib/chat/models/chat_component_state.freezed.dart b/lib/chat/models/chat_component_state.freezed.dart index ea7e8ae..41ab7e2 100644 --- a/lib/chat/models/chat_component_state.freezed.dart +++ b/lib/chat/models/chat_component_state.freezed.dart @@ -20,6 +20,8 @@ mixin _$ChatComponentState { GlobalKey get chatKey => throw _privateConstructorUsedError; // ScrollController for the chat AutoScrollController get scrollController => + throw _privateConstructorUsedError; // TextEditingController for the chat + InputTextFieldController get textEditingController => throw _privateConstructorUsedError; // Local user User? get localUser => throw _privateConstructorUsedError; // Active remote users @@ -47,6 +49,7 @@ abstract class $ChatComponentStateCopyWith<$Res> { $Res call( {GlobalKey chatKey, AutoScrollController scrollController, + InputTextFieldController textEditingController, User? localUser, IMap, User> remoteUsers, IMap, User> historicalRemoteUsers, @@ -72,6 +75,7 @@ class _$ChatComponentStateCopyWithImpl<$Res, $Val extends ChatComponentState> $Res call({ Object? chatKey = null, Object? scrollController = null, + Object? textEditingController = null, Object? localUser = freezed, Object? remoteUsers = null, Object? historicalRemoteUsers = null, @@ -88,6 +92,10 @@ class _$ChatComponentStateCopyWithImpl<$Res, $Val extends ChatComponentState> ? _value.scrollController : scrollController // ignore: cast_nullable_to_non_nullable as AutoScrollController, + textEditingController: null == textEditingController + ? _value.textEditingController + : textEditingController // ignore: cast_nullable_to_non_nullable + as InputTextFieldController, localUser: freezed == localUser ? _value.localUser : localUser // ignore: cast_nullable_to_non_nullable @@ -136,6 +144,7 @@ abstract class _$$ChatComponentStateImplCopyWith<$Res> $Res call( {GlobalKey chatKey, AutoScrollController scrollController, + InputTextFieldController textEditingController, User? localUser, IMap, User> remoteUsers, IMap, User> historicalRemoteUsers, @@ -160,6 +169,7 @@ class __$$ChatComponentStateImplCopyWithImpl<$Res> $Res call({ Object? chatKey = null, Object? scrollController = null, + Object? textEditingController = null, Object? localUser = freezed, Object? remoteUsers = null, Object? historicalRemoteUsers = null, @@ -176,6 +186,10 @@ class __$$ChatComponentStateImplCopyWithImpl<$Res> ? _value.scrollController : scrollController // ignore: cast_nullable_to_non_nullable as AutoScrollController, + textEditingController: null == textEditingController + ? _value.textEditingController + : textEditingController // ignore: cast_nullable_to_non_nullable + as InputTextFieldController, localUser: freezed == localUser ? _value.localUser : localUser // ignore: cast_nullable_to_non_nullable @@ -210,6 +224,7 @@ class _$ChatComponentStateImpl implements _ChatComponentState { const _$ChatComponentStateImpl( {required this.chatKey, required this.scrollController, + required this.textEditingController, required this.localUser, required this.remoteUsers, required this.historicalRemoteUsers, @@ -223,6 +238,9 @@ class _$ChatComponentStateImpl implements _ChatComponentState { // ScrollController for the chat @override final AutoScrollController scrollController; +// TextEditingController for the chat + @override + final InputTextFieldController textEditingController; // Local user @override final User? localUser; @@ -244,7 +262,7 @@ class _$ChatComponentStateImpl implements _ChatComponentState { @override String toString() { - return 'ChatComponentState(chatKey: $chatKey, scrollController: $scrollController, localUser: $localUser, remoteUsers: $remoteUsers, historicalRemoteUsers: $historicalRemoteUsers, unknownUsers: $unknownUsers, messageWindow: $messageWindow, title: $title)'; + return 'ChatComponentState(chatKey: $chatKey, scrollController: $scrollController, textEditingController: $textEditingController, localUser: $localUser, remoteUsers: $remoteUsers, historicalRemoteUsers: $historicalRemoteUsers, unknownUsers: $unknownUsers, messageWindow: $messageWindow, title: $title)'; } @override @@ -255,6 +273,8 @@ class _$ChatComponentStateImpl implements _ChatComponentState { (identical(other.chatKey, chatKey) || other.chatKey == chatKey) && (identical(other.scrollController, scrollController) || other.scrollController == scrollController) && + (identical(other.textEditingController, textEditingController) || + other.textEditingController == textEditingController) && (identical(other.localUser, localUser) || other.localUser == localUser) && (identical(other.remoteUsers, remoteUsers) || @@ -273,6 +293,7 @@ class _$ChatComponentStateImpl implements _ChatComponentState { runtimeType, chatKey, scrollController, + textEditingController, localUser, remoteUsers, historicalRemoteUsers, @@ -292,6 +313,7 @@ abstract class _ChatComponentState implements ChatComponentState { const factory _ChatComponentState( {required final GlobalKey chatKey, required final AutoScrollController scrollController, + required final InputTextFieldController textEditingController, required final User? localUser, required final IMap, User> remoteUsers, required final IMap, User> @@ -304,6 +326,8 @@ abstract class _ChatComponentState implements ChatComponentState { GlobalKey get chatKey; @override // ScrollController for the chat AutoScrollController get scrollController; + @override // TextEditingController for the chat + InputTextFieldController get textEditingController; @override // Local user User? get localUser; @override // Active remote users diff --git a/lib/chat/views/chat_component_widget.dart b/lib/chat/views/chat_component_widget.dart index 6d0fa73..2d38b3c 100644 --- a/lib/chat/views/chat_component_widget.dart +++ b/lib/chat/views/chat_component_widget.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:math'; import 'package:async_tools/async_tools.dart'; @@ -6,6 +7,7 @@ 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:flutter_translate/flutter_translate.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; @@ -154,6 +156,14 @@ class ChatComponentWidget extends StatelessWidget { final scale = theme.extension()!; final textTheme = Theme.of(context).textTheme; final chatTheme = makeChatTheme(scale, textTheme); + final errorChatTheme = (ChatThemeEditor(chatTheme) + ..inputTextColor = scale.errorScale.primary + ..sendButtonIcon = Image.asset( + 'assets/icon-send.png', + color: scale.errorScale.primary, + package: 'flutter_chat_ui', + )) + .commit(); // Get the enclosing chat component cubit that contains our state // (created by ChatComponentWidget.builder()) @@ -216,80 +226,125 @@ class ChatComponentWidget extends StatelessWidget { ), Expanded( child: DecoratedBox( - decoration: const BoxDecoration(), - child: NotificationListener( - onNotification: (notification) { - if (chatComponentCubit.scrollOffset != 0) { + decoration: const BoxDecoration(), + child: NotificationListener( + onNotification: (notification) { + if (chatComponentCubit.scrollOffset != 0) { + return false; + } + + if (!isFirstPage && + notification.metrics.pixels <= + ((notification.metrics.maxScrollExtent - + notification.metrics + .minScrollExtent) * + (1.0 - onEndReachedThreshold) + + notification + .metrics.minScrollExtent)) { + // + final scrollOffset = (notification + .metrics.maxScrollExtent - + notification.metrics.minScrollExtent) * + (1.0 - onEndReachedThreshold); + + chatComponentCubit.scrollOffset = scrollOffset; + + // + singleFuture(chatComponentState.chatKey, + () async { + await _handlePageForward(chatComponentCubit, + messageWindow, notification); + }); + } else if (!isLastPage && + notification.metrics.pixels >= + ((notification.metrics.maxScrollExtent - + notification.metrics + .minScrollExtent) * + onEndReachedThreshold + + notification + .metrics.minScrollExtent)) { + // + final scrollOffset = -(notification + .metrics.maxScrollExtent - + notification.metrics.minScrollExtent) * + (1.0 - onEndReachedThreshold); + + chatComponentCubit.scrollOffset = scrollOffset; + // + singleFuture(chatComponentState.chatKey, + () async { + await _handlePageBackward(chatComponentCubit, + messageWindow, notification); + }); + } return false; - } + }, + child: ValueListenableBuilder( + valueListenable: + chatComponentState.textEditingController, + builder: (context, textEditingValue, __) { + final messageIsValid = utf8 + .encode(textEditingValue.text) + .lengthInBytes < + 2048; - if (!isFirstPage && - notification.metrics.pixels <= - ((notification.metrics.maxScrollExtent - - notification - .metrics.minScrollExtent) * - (1.0 - onEndReachedThreshold) + - notification.metrics.minScrollExtent)) { - // - final scrollOffset = (notification - .metrics.maxScrollExtent - - notification.metrics.minScrollExtent) * - (1.0 - onEndReachedThreshold); - - chatComponentCubit.scrollOffset = scrollOffset; - - // - singleFuture(chatComponentState.chatKey, - () async { - await _handlePageForward(chatComponentCubit, - messageWindow, notification); - }); - } else if (!isLastPage && - notification.metrics.pixels >= - ((notification.metrics.maxScrollExtent - - notification - .metrics.minScrollExtent) * - onEndReachedThreshold + - notification.metrics.minScrollExtent)) { - // - final scrollOffset = -(notification - .metrics.maxScrollExtent - - notification.metrics.minScrollExtent) * - (1.0 - onEndReachedThreshold); - - chatComponentCubit.scrollOffset = scrollOffset; - // - singleFuture(chatComponentState.chatKey, - () async { - await _handlePageBackward(chatComponentCubit, - messageWindow, notification); - }); - } - return false; - }, - child: Chat( - key: chatComponentState.chatKey, - theme: chatTheme, - messages: messageWindow.window.toList(), - scrollToBottomOnSend: isFirstPage, - scrollController: - chatComponentState.scrollController, - // isLastPage: isLastPage, - // onEndReached: () async { - // await _handlePageBackward( - // chatComponentCubit, messageWindow); - // }, - //onEndReachedThreshold: onEndReachedThreshold, - //onAttachmentPressed: _handleAttachmentPressed, - //onMessageTap: _handleMessageTap, - //onPreviewDataFetched: _handlePreviewDataFetched, - onSendPressed: (pt) => - _handleSendPressed(chatComponentCubit, pt), - //showUserAvatars: false, - //showUserNames: true, - user: localUser, - emptyState: const EmptyChatWidget())), - ), + return Chat( + key: chatComponentState.chatKey, + theme: messageIsValid + ? chatTheme + : errorChatTheme, + messages: messageWindow.window.toList(), + scrollToBottomOnSend: isFirstPage, + scrollController: + chatComponentState.scrollController, + inputOptions: InputOptions( + inputClearMode: messageIsValid + ? InputClearMode.always + : InputClearMode.never, + textEditingController: + chatComponentState + .textEditingController), + // isLastPage: isLastPage, + // onEndReached: () async { + // await _handlePageBackward( + // chatComponentCubit, messageWindow); + // }, + //onEndReachedThreshold: onEndReachedThreshold, + //onAttachmentPressed: _handleAttachmentPressed, + //onMessageTap: _handleMessageTap, + //onPreviewDataFetched: _handlePreviewDataFetched, + onSendPressed: (pt) { + try { + if (!messageIsValid) { + showErrorToast( + context, + translate( + 'chat.message_too_long')); + return; + } + _handleSendPressed( + chatComponentCubit, pt); + } on FormatException { + showErrorToast( + context, + translate( + 'chat.message_too_long')); + } + }, + listBottomWidget: messageIsValid + ? null + : Text( + translate( + 'chat.message_too_long'), + style: TextStyle( + color: scale + .errorScale.primary)) + .toCenter(), + //showUserAvatars: false, + //showUserNames: true, + user: localUser, + emptyState: const EmptyChatWidget()); + }))), ), ], ), diff --git a/lib/theme/models/chat_theme.dart b/lib/theme/models/chat_theme.dart index b6ef7ba..15fcdc3 100644 --- a/lib/theme/models/chat_theme.dart +++ b/lib/theme/models/chat_theme.dart @@ -1,3 +1,5 @@ +// ignore_for_file: always_put_required_named_parameters_first + import 'package:flutter/material.dart'; import 'package:flutter_chat_ui/flutter_chat_ui.dart'; @@ -52,3 +54,398 @@ ChatTheme makeChatTheme(ScaleScheme scale, TextTheme textTheme) => color: Colors.white, fontSize: 64, )); + +class EditedChatTheme extends ChatTheme { + const EditedChatTheme({ + required super.attachmentButtonIcon, + required super.attachmentButtonMargin, + required super.backgroundColor, + super.bubbleMargin, + required super.dateDividerMargin, + required super.dateDividerTextStyle, + required super.deliveredIcon, + required super.documentIcon, + required super.emptyChatPlaceholderTextStyle, + required super.errorColor, + required super.errorIcon, + required super.inputBackgroundColor, + required super.inputSurfaceTintColor, + required super.inputElevation, + required super.inputBorderRadius, + super.inputContainerDecoration, + required super.inputMargin, + required super.inputPadding, + required super.inputTextColor, + super.inputTextCursorColor, + required super.inputTextDecoration, + required super.inputTextStyle, + required super.messageBorderRadius, + required super.messageInsetsHorizontal, + required super.messageInsetsVertical, + required super.messageMaxWidth, + required super.primaryColor, + required super.receivedEmojiMessageTextStyle, + super.receivedMessageBodyBoldTextStyle, + super.receivedMessageBodyCodeTextStyle, + super.receivedMessageBodyLinkTextStyle, + required super.receivedMessageBodyTextStyle, + required super.receivedMessageCaptionTextStyle, + required super.receivedMessageDocumentIconColor, + required super.receivedMessageLinkDescriptionTextStyle, + required super.receivedMessageLinkTitleTextStyle, + required super.secondaryColor, + required super.seenIcon, + required super.sendButtonIcon, + required super.sendButtonMargin, + required super.sendingIcon, + required super.sentEmojiMessageTextStyle, + super.sentMessageBodyBoldTextStyle, + super.sentMessageBodyCodeTextStyle, + super.sentMessageBodyLinkTextStyle, + required super.sentMessageBodyTextStyle, + required super.sentMessageCaptionTextStyle, + required super.sentMessageDocumentIconColor, + required super.sentMessageLinkDescriptionTextStyle, + required super.sentMessageLinkTitleTextStyle, + required super.statusIconPadding, + required super.systemMessageTheme, + required super.typingIndicatorTheme, + required super.unreadHeaderTheme, + required super.userAvatarImageBackgroundColor, + required super.userAvatarNameColors, + required super.userAvatarTextStyle, + required super.userNameTextStyle, + super.highlightMessageColor, + }); +} + +class ChatThemeEditor { + ChatThemeEditor(ChatTheme base) + : attachmentButtonIcon = base.attachmentButtonIcon, + attachmentButtonMargin = base.attachmentButtonMargin, + backgroundColor = base.backgroundColor, + bubbleMargin = base.bubbleMargin, + dateDividerMargin = base.dateDividerMargin, + dateDividerTextStyle = base.dateDividerTextStyle, + deliveredIcon = base.deliveredIcon, + documentIcon = base.documentIcon, + emptyChatPlaceholderTextStyle = base.emptyChatPlaceholderTextStyle, + errorColor = base.errorColor, + errorIcon = base.errorIcon, + inputBackgroundColor = base.inputBackgroundColor, + inputSurfaceTintColor = base.inputSurfaceTintColor, + inputElevation = base.inputElevation, + inputBorderRadius = base.inputBorderRadius, + inputContainerDecoration = base.inputContainerDecoration, + inputMargin = base.inputMargin, + inputPadding = base.inputPadding, + inputTextColor = base.inputTextColor, + inputTextCursorColor = base.inputTextCursorColor, + inputTextDecoration = base.inputTextDecoration, + inputTextStyle = base.inputTextStyle, + messageBorderRadius = base.messageBorderRadius, + messageInsetsHorizontal = base.messageInsetsHorizontal, + messageInsetsVertical = base.messageInsetsVertical, + messageMaxWidth = base.messageMaxWidth, + primaryColor = base.primaryColor, + receivedEmojiMessageTextStyle = base.receivedEmojiMessageTextStyle, + receivedMessageBodyBoldTextStyle = + base.receivedMessageBodyBoldTextStyle, + receivedMessageBodyCodeTextStyle = + base.receivedMessageBodyCodeTextStyle, + receivedMessageBodyLinkTextStyle = + base.receivedMessageBodyLinkTextStyle, + receivedMessageBodyTextStyle = base.receivedMessageBodyTextStyle, + receivedMessageCaptionTextStyle = base.receivedMessageCaptionTextStyle, + receivedMessageDocumentIconColor = + base.receivedMessageDocumentIconColor, + receivedMessageLinkDescriptionTextStyle = + base.receivedMessageLinkDescriptionTextStyle, + receivedMessageLinkTitleTextStyle = + base.receivedMessageLinkTitleTextStyle, + secondaryColor = base.secondaryColor, + seenIcon = base.seenIcon, + sendButtonIcon = base.sendButtonIcon, + sendButtonMargin = base.sendButtonMargin, + sendingIcon = base.sendingIcon, + sentEmojiMessageTextStyle = base.sentEmojiMessageTextStyle, + sentMessageBodyBoldTextStyle = base.sentMessageBodyBoldTextStyle, + sentMessageBodyCodeTextStyle = base.sentMessageBodyCodeTextStyle, + sentMessageBodyLinkTextStyle = base.sentMessageBodyLinkTextStyle, + sentMessageBodyTextStyle = base.sentMessageBodyTextStyle, + sentMessageCaptionTextStyle = base.sentMessageCaptionTextStyle, + sentMessageDocumentIconColor = base.sentMessageDocumentIconColor, + sentMessageLinkDescriptionTextStyle = + base.sentMessageLinkDescriptionTextStyle, + sentMessageLinkTitleTextStyle = base.sentMessageLinkTitleTextStyle, + statusIconPadding = base.statusIconPadding, + systemMessageTheme = base.systemMessageTheme, + typingIndicatorTheme = base.typingIndicatorTheme, + unreadHeaderTheme = base.unreadHeaderTheme, + userAvatarImageBackgroundColor = base.userAvatarImageBackgroundColor, + userAvatarNameColors = base.userAvatarNameColors, + userAvatarTextStyle = base.userAvatarTextStyle, + userNameTextStyle = base.userNameTextStyle, + highlightMessageColor = base.highlightMessageColor; + + EditedChatTheme commit() => EditedChatTheme( + attachmentButtonIcon: attachmentButtonIcon, + attachmentButtonMargin: attachmentButtonMargin, + backgroundColor: backgroundColor, + bubbleMargin: bubbleMargin, + dateDividerMargin: dateDividerMargin, + dateDividerTextStyle: dateDividerTextStyle, + deliveredIcon: deliveredIcon, + documentIcon: documentIcon, + emptyChatPlaceholderTextStyle: emptyChatPlaceholderTextStyle, + errorColor: errorColor, + errorIcon: errorIcon, + inputBackgroundColor: inputBackgroundColor, + inputSurfaceTintColor: inputSurfaceTintColor, + inputElevation: inputElevation, + inputBorderRadius: inputBorderRadius, + inputContainerDecoration: inputContainerDecoration, + inputMargin: inputMargin, + inputPadding: inputPadding, + inputTextColor: inputTextColor, + inputTextCursorColor: inputTextCursorColor, + inputTextDecoration: inputTextDecoration, + inputTextStyle: inputTextStyle, + messageBorderRadius: messageBorderRadius, + messageInsetsHorizontal: messageInsetsHorizontal, + messageInsetsVertical: messageInsetsVertical, + messageMaxWidth: messageMaxWidth, + primaryColor: primaryColor, + receivedEmojiMessageTextStyle: receivedEmojiMessageTextStyle, + receivedMessageBodyBoldTextStyle: receivedMessageBodyBoldTextStyle, + receivedMessageBodyCodeTextStyle: receivedMessageBodyCodeTextStyle, + receivedMessageBodyLinkTextStyle: receivedMessageBodyLinkTextStyle, + receivedMessageBodyTextStyle: receivedMessageBodyTextStyle, + receivedMessageCaptionTextStyle: receivedMessageCaptionTextStyle, + receivedMessageDocumentIconColor: receivedMessageDocumentIconColor, + receivedMessageLinkDescriptionTextStyle: + receivedMessageLinkDescriptionTextStyle, + receivedMessageLinkTitleTextStyle: receivedMessageLinkTitleTextStyle, + secondaryColor: secondaryColor, + seenIcon: seenIcon, + sendButtonIcon: sendButtonIcon, + sendButtonMargin: sendButtonMargin, + sendingIcon: sendingIcon, + sentEmojiMessageTextStyle: sentEmojiMessageTextStyle, + sentMessageBodyBoldTextStyle: sentMessageBodyBoldTextStyle, + sentMessageBodyCodeTextStyle: sentMessageBodyCodeTextStyle, + sentMessageBodyLinkTextStyle: sentMessageBodyLinkTextStyle, + sentMessageBodyTextStyle: sentMessageBodyTextStyle, + sentMessageCaptionTextStyle: sentMessageCaptionTextStyle, + sentMessageDocumentIconColor: sentMessageDocumentIconColor, + sentMessageLinkDescriptionTextStyle: + sentMessageLinkDescriptionTextStyle, + sentMessageLinkTitleTextStyle: sentMessageLinkTitleTextStyle, + statusIconPadding: statusIconPadding, + systemMessageTheme: systemMessageTheme, + typingIndicatorTheme: typingIndicatorTheme, + unreadHeaderTheme: unreadHeaderTheme, + userAvatarImageBackgroundColor: userAvatarImageBackgroundColor, + userAvatarNameColors: userAvatarNameColors, + userAvatarTextStyle: userAvatarTextStyle, + userNameTextStyle: userNameTextStyle, + highlightMessageColor: highlightMessageColor, + ); + + ///////////////////////////////////////////////////////////////////////////// + + /// Icon for select attachment button. + Widget? attachmentButtonIcon; + + /// Margin of attachment button. + EdgeInsets? attachmentButtonMargin; + + /// Used as a background color of a chat widget. + Color backgroundColor; + + // Margin around the message bubble. + EdgeInsetsGeometry? bubbleMargin; + + /// Margin around date dividers. + EdgeInsets dateDividerMargin; + + /// Text style of the date dividers. + TextStyle dateDividerTextStyle; + + /// Icon for message's `delivered` status. For the best look use size of 16. + Widget? deliveredIcon; + + /// Icon inside file message. + Widget? documentIcon; + + /// Text style of the empty chat placeholder. + TextStyle emptyChatPlaceholderTextStyle; + + /// Color to indicate something bad happened (usually - shades of red). + Color errorColor; + + /// Icon for message's `error` status. For the best look use size of 16. + Widget? errorIcon; + + /// Color of the bottom bar where text field is. + Color inputBackgroundColor; + + /// Surface Tint Color of the bottom bar where text field is. + Color inputSurfaceTintColor; + + double inputElevation; + + /// Top border radius of the bottom bar where text field is. + BorderRadius inputBorderRadius; + + /// Decoration of the container wrapping the text field. + Decoration? inputContainerDecoration; + + /// Outer insets of the bottom bar where text field is. + EdgeInsets inputMargin; + + /// Inner insets of the bottom bar where text field is. + EdgeInsets inputPadding; + + /// Color of the text field's text and attachment/send buttons. + Color inputTextColor; + + /// Color of the text field's cursor. + Color? inputTextCursorColor; + + /// Decoration of the input text field. + InputDecoration inputTextDecoration; + + /// Text style of the message input. To change the color use [inputTextColor]. + TextStyle inputTextStyle; + + /// Border radius of message container. + double messageBorderRadius; + + /// Horizontal message bubble insets. + double messageInsetsHorizontal; + + /// Vertical message bubble insets. + double messageInsetsVertical; + + /// Message bubble max width. set to [double.infinity] adaptive screen. + double messageMaxWidth; + + /// Primary color of the chat used as a background of sent messages + /// and statuses. + Color primaryColor; + + /// Text style used for displaying emojis on text messages. + TextStyle receivedEmojiMessageTextStyle; + + /// Body text style used for displaying bold text on received text messages. + /// Default to a bold version of [receivedMessageBodyTextStyle]. + TextStyle? receivedMessageBodyBoldTextStyle; + + /// Body text style used for displaying code text on received text messages. + /// Defaults to a mono version of [receivedMessageBodyTextStyle]. + TextStyle? receivedMessageBodyCodeTextStyle; + + /// Text style used for displaying link text on received text messages. + /// Defaults to [receivedMessageBodyTextStyle]. + TextStyle? receivedMessageBodyLinkTextStyle; + + /// Body text style used for displaying text on different types + /// of received messages. + TextStyle receivedMessageBodyTextStyle; + + /// Caption text style used for displaying secondary info (e.g. file size) on + /// different types of received messages. + TextStyle receivedMessageCaptionTextStyle; + + /// Color of the document icon on received messages. Has no effect when + /// [documentIcon] is used. + Color receivedMessageDocumentIconColor; + + /// Text style used for displaying link description on received messages. + TextStyle receivedMessageLinkDescriptionTextStyle; + + /// Text style used for displaying link title on received messages. + TextStyle receivedMessageLinkTitleTextStyle; + + /// Secondary color, used as a background of received messages. + Color secondaryColor; + + /// Icon for message's `seen` status. For the best look use size of 16. + Widget? seenIcon; + + /// Icon for send button. + Widget? sendButtonIcon; + + /// Margin of send button. + EdgeInsets? sendButtonMargin; + + /// Icon for message's `sending` status. For the best look use size of 10. + Widget? sendingIcon; + + /// Text style used for displaying emojis on text messages. + TextStyle sentEmojiMessageTextStyle; + + /// Body text style used for displaying bold text on sent text messages. + /// Defaults to a bold version of [sentMessageBodyTextStyle]. + TextStyle? sentMessageBodyBoldTextStyle; + + /// Body text style used for displaying code text on sent text messages. + /// Defaults to a mono version of [sentMessageBodyTextStyle]. + TextStyle? sentMessageBodyCodeTextStyle; + + /// Text style used for displaying link text on sent text messages. + /// Defaults to [sentMessageBodyTextStyle]. + TextStyle? sentMessageBodyLinkTextStyle; + + /// Body text style used for displaying text on different types + /// of sent messages. + TextStyle sentMessageBodyTextStyle; + + /// Caption text style used for displaying secondary info (e.g. file size) on + /// different types of sent messages. + TextStyle sentMessageCaptionTextStyle; + + /// Color of the document icon on sent messages. Has no effect when + /// [documentIcon] is used. + Color sentMessageDocumentIconColor; + + /// Text style used for displaying link description on sent messages. + TextStyle sentMessageLinkDescriptionTextStyle; + + /// Text style used for displaying link title on sent messages. + TextStyle sentMessageLinkTitleTextStyle; + + /// Padding around status icons. + EdgeInsets statusIconPadding; + + /// Theme for the system message. Will not have an effect if a custom builder + /// is provided. + SystemMessageTheme systemMessageTheme; + + /// Theme for typing indicator. See [TypingIndicator]. + TypingIndicatorTheme typingIndicatorTheme; + + /// Theme for the unread header. + UnreadHeaderTheme unreadHeaderTheme; + + /// Color used as a background for user avatar if an image is provided. + /// Visible if the image has some transparent parts. + Color userAvatarImageBackgroundColor; + + /// Colors used as backgrounds for user avatars with no image and so, + /// corresponding user names. + /// Calculated based on a user ID, so unique across the whole app. + List userAvatarNameColors; + + /// Text style used for displaying initials on user avatar if no + /// image is provided. + TextStyle userAvatarTextStyle; + + /// User names text style. Color will be overwritten + /// with [userAvatarNameColors]. + TextStyle userNameTextStyle; + + /// Color used as background of message row on highligth. + Color? highlightMessageColor; +} diff --git a/packages/veilid_support/lib/identity_support/exceptions.dart b/packages/veilid_support/lib/identity_support/exceptions.dart index f0774b4..280f468 100644 --- a/packages/veilid_support/lib/identity_support/exceptions.dart +++ b/packages/veilid_support/lib/identity_support/exceptions.dart @@ -3,7 +3,8 @@ 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'); + invalid('identity is corrupted or secret is invalid'), + cancelled('account operation cancelled'); const IdentityException(this.message); final String message; diff --git a/packages/veilid_support/lib/identity_support/identity_instance.dart b/packages/veilid_support/lib/identity_support/identity_instance.dart index abcc4f0..978a30d 100644 --- a/packages/veilid_support/lib/identity_support/identity_instance.dart +++ b/packages/veilid_support/lib/identity_support/identity_instance.dart @@ -68,12 +68,12 @@ class IdentityInstance with _$IdentityInstance { return cs; } - /// Read the account record info for a specific accountKey from the identity - /// instance record using the identity instance secret key to decrypt + /// Read the account record info for a specific applicationId from the + /// identity instance record using the identity instance secret key to decrypt Future> readAccount( {required TypedKey superRecordKey, required SecretKey secretKey, - required String accountKey}) async { + required String applicationId}) async { // Read the identity key to get the account keys final pool = DHTRecordPool.instance; @@ -91,7 +91,7 @@ class IdentityInstance with _$IdentityInstance { throw IdentityException.readError; } final accountRecords = IMapOfSets.from(identity.accountRecords); - final vcAccounts = accountRecords.get(accountKey); + final vcAccounts = accountRecords.get(applicationId); accountRecordInfo = vcAccounts.toList(); }); @@ -104,7 +104,7 @@ class IdentityInstance with _$IdentityInstance { Future addAccount({ required TypedKey superRecordKey, required SecretKey secretKey, - required String accountKey, + required String applicationId, required Future Function(TypedKey parent) createAccountCallback, int maxAccounts = 1, }) async { @@ -143,11 +143,12 @@ class IdentityInstance with _$IdentityInstance { } final oldAccountRecords = IMapOfSets.from(oldIdentity.accountRecords); - if (oldAccountRecords.get(accountKey).length >= maxAccounts) { + if (oldAccountRecords.get(applicationId).length >= maxAccounts) { throw IdentityException.limitExceeded; } - final accountRecords = - oldAccountRecords.add(accountKey, newAccountRecordInfo).asIMap(); + final accountRecords = oldAccountRecords + .add(applicationId, newAccountRecordInfo) + .asIMap(); return oldIdentity.copyWith(accountRecords: accountRecords); }); @@ -156,6 +157,61 @@ class IdentityInstance with _$IdentityInstance { }); } + /// Removes an Account associated with super identity from the identity + /// instance record. 'removeAccountCallback' returns the account to be + /// removed from the list passed to it. + Future removeAccount({ + required TypedKey superRecordKey, + required SecretKey secretKey, + required String applicationId, + required Future Function( + List accountRecordInfos) + removeAccountCallback, + }) async { + final pool = DHTRecordPool.instance; + + /////// Add account with profile to DHT + + // Open identity key for writing + veilidLoggy.debug('Opening identity record'); + return (await pool.openRecordWrite(recordKey, writer(secretKey), + debugName: 'IdentityInstance::addAccount::IdentityRecord', + parent: superRecordKey)) + .scope((identityRec) async { + try { + // Update identity key to remove account + veilidLoggy.debug('Updating identity to remove account'); + await identityRec.eventualUpdateJson(Identity.fromJson, + (oldIdentity) async { + if (oldIdentity == null) { + throw IdentityException.readError; + } + final oldAccountRecords = IMapOfSets.from(oldIdentity.accountRecords); + + // Get list of accounts associated with the application + final vcAccounts = oldAccountRecords.get(applicationId); + final accountRecordInfos = vcAccounts.toList(); + + // Call the callback to return what account to remove + final toRemove = await removeAccountCallback(accountRecordInfos); + if (toRemove == null) { + throw IdentityException.cancelled; + } + final newAccountRecords = + oldAccountRecords.remove(applicationId, toRemove).asIMap(); + + return oldIdentity.copyWith(accountRecords: newAccountRecords); + }); + } on IdentityException catch (e) { + if (e == IdentityException.cancelled) { + return false; + } + rethrow; + } + return true; + }); + } + //////////////////////////////////////////////////////////////////////////// // Internal implementation diff --git a/packages/veilid_support/lib/src/persistent_queue.dart b/packages/veilid_support/lib/src/persistent_queue.dart index c7abe97..598d8a7 100644 --- a/packages/veilid_support/lib/src/persistent_queue.dart +++ b/packages/veilid_support/lib/src/persistent_queue.dart @@ -15,12 +15,14 @@ class PersistentQueue required String key, required T Function(Uint8List) fromBuffer, required Future Function(IList) closure, - bool deleteOnClose = true}) + bool deleteOnClose = true, + void Function(Object, StackTrace)? onError}) : _table = table, _key = key, _fromBuffer = fromBuffer, _closure = closure, - _deleteOnClose = deleteOnClose { + _deleteOnClose = deleteOnClose, + _onError = onError { _initWait.add(_init); } @@ -61,9 +63,17 @@ class PersistentQueue })); // Load the queue if we have one - await _queueMutex.protect(() async { - _queue = await load() ?? await store(IList.empty()); - }); + try { + await _queueMutex.protect(() async { + _queue = await load() ?? await store(IList.empty()); + }); + } on Exception catch (e, st) { + if (_onError != null) { + _onError(e, st); + } else { + rethrow; + } + } } Future _updateQueueInner(IList newQueue) async { @@ -132,24 +142,32 @@ class PersistentQueue // } Future _process() async { - // Take a copy of the current queue - // (doesn't need queue mutex because this is a sync operation) - final toProcess = _queue; - final processCount = toProcess.length; - if (processCount == 0) { - return; + try { + // Take a copy of the current queue + // (doesn't need queue mutex because this is a sync operation) + final toProcess = _queue; + final processCount = toProcess.length; + if (processCount == 0) { + return; + } + + // Run the processing closure + await _closure(toProcess); + + // If there was no exception, remove the processed items + await _queueMutex.protect(() async { + // Get the queue from the state again as items could + // have been added during processing + final newQueue = _queue.skip(processCount).toIList(); + await _updateQueueInner(newQueue); + }); + } on Exception catch (e, sp) { + if (_onError != null) { + _onError(e, sp); + } else { + rethrow; + } } - - // Run the processing closure - await _closure(toProcess); - - // If there was no exception, remove the processed items - await _queueMutex.protect(() async { - // Get the queue from the state again as items could - // have been added during processing - final newQueue = _queue.skip(processCount).toIList(); - await _updateQueueInner(newQueue); - }); } IList get queue => _queue; @@ -190,4 +208,5 @@ class PersistentQueue final StreamController> _syncAddController = StreamController(); final StreamController _queueReady = StreamController(); final Future Function(IList) _closure; + final void Function(Object, StackTrace)? _onError; } diff --git a/pubspec.lock b/pubspec.lock index 5bdc0dc..2e743b1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -483,10 +483,10 @@ packages: description: path: "." ref: main - resolved-ref: "6712d897e25041de38fb53ec06dc7a12cc6bff7d" + resolved-ref: "0d8ac2fcafe24eba1adff9290a9ccd41f7718480" url: "https://gitlab.com/veilid/flutter-chat-ui.git" source: git - version: "1.6.13" + version: "1.6.14" flutter_form_builder: dependency: "direct main" description: From 44fe198e5d12139b76e01364d9ad5ab961d92d68 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 6 Jul 2024 20:09:18 -0400 Subject: [PATCH 156/270] ui cleanup, new themes --- assets/i18n/en.json | 5 +- .../views/edit_account_page.dart | 2 +- .../views/new_account_page.dart | 2 +- lib/account_manager/views/profile_widget.dart | 32 +- lib/app.dart | 61 ++-- lib/chat/views/chat_component_widget.dart | 185 ++++++------ .../views/contact_invitation_display.dart | 4 +- .../views/contact_invitation_list_widget.dart | 6 +- lib/layout/home/drawer_menu/drawer_menu.dart | 278 +++++++++++++----- .../home/drawer_menu/menu_item_widget.dart | 16 +- .../home_account_ready_main.dart | 34 ++- .../main_pager/account_page.dart | 7 +- .../main_pager/main_pager.dart | 139 ++++++--- lib/layout/home/home_screen.dart | 42 +-- lib/settings/settings_page.dart | 25 +- lib/theme/models/chat_theme.dart | 79 +++-- lib/theme/models/contrast_generator.dart | 265 +++++++++++++++-- lib/theme/models/radix_generator.dart | 12 +- .../models/scale_input_decorator_theme.dart | 9 +- lib/theme/models/scale_scheme.dart | 21 +- lib/theme/models/slider_tile.dart | 21 +- lib/theme/models/theme_preference.dart | 60 +++- lib/theme/views/color_preferences.dart | 1 + lib/theme/views/styled_dialog.dart | 12 +- lib/theme/views/styled_scaffold.dart | 27 ++ lib/theme/views/views.dart | 1 + lib/theme/views/widget_helpers.dart | 49 ++- lib/tools/enter_pin.dart | 3 +- lib/veilid_processor/views/developer.dart | 28 +- pubspec.lock | 24 +- pubspec.yaml | 8 +- 31 files changed, 1051 insertions(+), 407 deletions(-) create mode 100644 lib/theme/views/styled_scaffold.dart diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 3ae7b8a..7c47707 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -4,7 +4,9 @@ }, "menu": { "settings_tooltip": "Settings", - "add_account_tooltip": "Add Account" + "add_account_tooltip": "Add Account", + "accounts": "Accounts", + "version": "Version" }, "pager": { "chats": "Chats", @@ -192,6 +194,7 @@ "eggplant": "Eggplant", "lime": "Lime", "grim": "Grim", + "elite": "31337", "contrast": "Contrast" }, "brightness": { diff --git a/lib/account_manager/views/edit_account_page.dart b/lib/account_manager/views/edit_account_page.dart index 36a9c28..0cf0264 100644 --- a/lib/account_manager/views/edit_account_page.dart +++ b/lib/account_manager/views/edit_account_page.dart @@ -261,7 +261,7 @@ class _EditAccountPageState extends State { Widget build(BuildContext context) { final displayModalHUD = _isInAsyncCall; - return Scaffold( + return StyledScaffold( // resizeToAvoidBottomInset: false, appBar: DefaultAppBar( title: Text(translate('edit_account_page.titlebar')), diff --git a/lib/account_manager/views/new_account_page.dart b/lib/account_manager/views/new_account_page.dart index f38bd3a..6b98467 100644 --- a/lib/account_manager/views/new_account_page.dart +++ b/lib/account_manager/views/new_account_page.dart @@ -93,7 +93,7 @@ class _NewAccountPageState extends State { Widget build(BuildContext context) { final displayModalHUD = _isInAsyncCall; - return Scaffold( + return StyledScaffold( // resizeToAvoidBottomInset: false, appBar: DefaultAppBar( title: Text(translate('new_account_page.titlebar')), diff --git a/lib/account_manager/views/profile_widget.dart b/lib/account_manager/views/profile_widget.dart index ecb7c3d..1f06476 100644 --- a/lib/account_manager/views/profile_widget.dart +++ b/lib/account_manager/views/profile_widget.dart @@ -21,24 +21,42 @@ class ProfileWidget extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + final textTheme = theme.textTheme; return DecoratedBox( decoration: ShapeDecoration( - color: scale.primaryScale.border, - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), + color: scaleConfig.preferBorders + ? scale.primaryScale.elementBackground + : scale.primaryScale.border, + shape: RoundedRectangleBorder( + side: !scaleConfig.useVisualIndicators + ? BorderSide.none + : BorderSide( + strokeAlign: BorderSide.strokeAlignCenter, + color: scaleConfig.preferBorders + ? scale.primaryScale.border + : scale.primaryScale.borderText, + width: 2), + borderRadius: BorderRadius.all( + Radius.circular(16 * scaleConfig.borderRadiusScale))), + ), child: Column(children: [ Text( _profile.name, - style: textTheme.headlineSmall! - .copyWith(color: scale.primaryScale.borderText), + style: textTheme.headlineSmall!.copyWith( + color: scaleConfig.preferBorders + ? scale.primaryScale.border + : scale.primaryScale.borderText), textAlign: TextAlign.left, ).paddingAll(4), if (_profile.pronouns.isNotEmpty) Text(_profile.pronouns, - style: textTheme.bodyMedium! - .copyWith(color: scale.primaryScale.borderText)) + style: textTheme.bodyMedium!.copyWith( + color: scaleConfig.preferBorders + ? scale.primaryScale.border + : scale.primaryScale.borderText)) .paddingLTRB(4, 0, 4, 4), ]), ); diff --git a/lib/app.dart b/lib/app.dart index 01d2a6d..3dd2d57 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -11,12 +11,12 @@ import 'package:provider/provider.dart'; import 'package:veilid_support/veilid_support.dart'; import 'account_manager/account_manager.dart'; -import 'account_manager/cubits/active_local_account_cubit.dart'; import 'init.dart'; import 'layout/splash.dart'; import 'router/router.dart'; import 'settings/settings.dart'; import 'theme/models/theme_preference.dart'; +import 'theme/theme.dart'; import 'tick.dart'; import 'tools/loggy.dart'; import 'veilid_processor/veilid_processor.dart'; @@ -69,9 +69,7 @@ class VeilidChatApp extends StatelessWidget { }); } - Widget _buildShortcuts( - {required BuildContext context, - required Widget Function(BuildContext) builder}) => + Widget _buildShortcuts({required Widget Function(BuildContext) builder}) => ThemeSwitcher( builder: (context) => Shortcuts( shortcuts: { @@ -136,25 +134,42 @@ class VeilidChatApp extends StatelessWidget { accountRepository: AccountRepository.instance, locator: context.read)), ], - child: BackgroundTicker( - child: _buildShortcuts( - context: context, - builder: (context) => MaterialApp.router( - debugShowCheckedModeBanner: false, - routerConfig: - context.read().router(), - title: translate('app.title'), - theme: theme, - localizationsDelegates: [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - FormBuilderLocalizations.delegate, - localizationDelegate - ], - supportedLocales: - localizationDelegate.supportedLocales, - locale: localizationDelegate.currentLocale, - ))), + child: + BackgroundTicker(child: _buildShortcuts(builder: (context) { + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + + final gradient = LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: scaleConfig.preferBorders && + theme.brightness == Brightness.light + ? [ + scale.grayScale.hoverElementBackground, + scale.grayScale.subtleBackground, + ] + : [ + scale.tertiaryScale.hoverElementBackground, + scale.tertiaryScale.subtleBackground, + ]); + + return DecoratedBox( + decoration: BoxDecoration(gradient: gradient), + child: MaterialApp.router( + debugShowCheckedModeBanner: false, + routerConfig: context.read().router(), + title: translate('app.title'), + theme: theme, + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + FormBuilderLocalizations.delegate, + localizationDelegate + ], + supportedLocales: localizationDelegate.supportedLocales, + locale: localizationDelegate.currentLocale, + )); + })), )), ); }); diff --git a/lib/chat/views/chat_component_widget.dart b/lib/chat/views/chat_component_widget.dart index 2d38b3c..f95cebf 100644 --- a/lib/chat/views/chat_component_widget.dart +++ b/lib/chat/views/chat_component_widget.dart @@ -154,8 +154,9 @@ class ChatComponentWidget extends StatelessWidget { 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 scaleConfig = theme.extension()!; + final textTheme = theme.textTheme; + final chatTheme = makeChatTheme(scale, scaleConfig, textTheme); final errorChatTheme = (ChatThemeEditor(chatTheme) ..inputTextColor = scale.errorScale.primary ..sendButtonIcon = Image.asset( @@ -191,104 +192,99 @@ class ChatComponentWidget extends StatelessWidget { chatComponentCubit.scrollOffset = 0; } - return DefaultTextStyle( - style: textTheme.bodySmall!, - child: Align( - alignment: AlignmentDirectional.centerEnd, - child: Stack( + return Align( + alignment: AlignmentDirectional.centerEnd, + child: Stack( + children: [ + Column( children: [ - Column( - children: [ - Container( - height: 48, + 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(title, + textAlign: TextAlign.start, + style: textTheme.titleMedium!.copyWith( + color: scale.primaryScale.borderText)), + )), + const Spacer(), + IconButton( + icon: Icon(Icons.close, + color: scale.primaryScale.borderText), + onPressed: () async { + context.read().setActiveChat(null); + }).paddingLTRB(16, 0, 16, 0) + ]), + ), + Expanded( + child: DecoratedBox( 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(title, - textAlign: TextAlign.start, - style: textTheme.titleMedium!.copyWith( - color: scale.primaryScale.borderText)), - )), - const Spacer(), - IconButton( - icon: Icon(Icons.close, - color: scale.primaryScale.borderText), - onPressed: () async { - context.read().setActiveChat(null); - }).paddingLTRB(16, 0, 16, 0) - ]), - ), - Expanded( - child: DecoratedBox( - decoration: const BoxDecoration(), - child: NotificationListener( - onNotification: (notification) { - if (chatComponentCubit.scrollOffset != 0) { - return false; - } + color: scale.primaryScale.subtleBackground), + child: NotificationListener( + onNotification: (notification) { + if (chatComponentCubit.scrollOffset != 0) { + return false; + } - if (!isFirstPage && - notification.metrics.pixels <= - ((notification.metrics.maxScrollExtent - - notification.metrics - .minScrollExtent) * - (1.0 - onEndReachedThreshold) + - notification - .metrics.minScrollExtent)) { - // - final scrollOffset = (notification - .metrics.maxScrollExtent - + if (!isFirstPage && + notification.metrics.pixels <= + ((notification.metrics.maxScrollExtent - + notification + .metrics.minScrollExtent) * + (1.0 - onEndReachedThreshold) + + notification.metrics.minScrollExtent)) { + // + final scrollOffset = + (notification.metrics.maxScrollExtent - notification.metrics.minScrollExtent) * (1.0 - onEndReachedThreshold); - chatComponentCubit.scrollOffset = scrollOffset; + chatComponentCubit.scrollOffset = scrollOffset; - // - singleFuture(chatComponentState.chatKey, - () async { - await _handlePageForward(chatComponentCubit, - messageWindow, notification); - }); - } else if (!isLastPage && - notification.metrics.pixels >= - ((notification.metrics.maxScrollExtent - - notification.metrics - .minScrollExtent) * - onEndReachedThreshold + - notification - .metrics.minScrollExtent)) { - // - final scrollOffset = -(notification - .metrics.maxScrollExtent - + // + singleFuture(chatComponentState.chatKey, () async { + await _handlePageForward(chatComponentCubit, + messageWindow, notification); + }); + } else if (!isLastPage && + notification.metrics.pixels >= + ((notification.metrics.maxScrollExtent - + notification + .metrics.minScrollExtent) * + onEndReachedThreshold + + notification.metrics.minScrollExtent)) { + // + final scrollOffset = + -(notification.metrics.maxScrollExtent - notification.metrics.minScrollExtent) * (1.0 - onEndReachedThreshold); - chatComponentCubit.scrollOffset = scrollOffset; - // - singleFuture(chatComponentState.chatKey, - () async { - await _handlePageBackward(chatComponentCubit, - messageWindow, notification); - }); - } - return false; - }, - child: ValueListenableBuilder( - valueListenable: - chatComponentState.textEditingController, - builder: (context, textEditingValue, __) { - final messageIsValid = utf8 - .encode(textEditingValue.text) - .lengthInBytes < - 2048; + chatComponentCubit.scrollOffset = scrollOffset; + // + singleFuture(chatComponentState.chatKey, () async { + await _handlePageBackward(chatComponentCubit, + messageWindow, notification); + }); + } + return false; + }, + child: ValueListenableBuilder( + valueListenable: + chatComponentState.textEditingController, + builder: (context, textEditingValue, __) { + final messageIsValid = utf8 + .encode(textEditingValue.text) + .lengthInBytes < + 2048; - return Chat( + return Chat( key: chatComponentState.chatKey, theme: messageIsValid ? chatTheme @@ -343,13 +339,14 @@ class ChatComponentWidget extends StatelessWidget { //showUserAvatars: false, //showUserNames: true, user: localUser, - emptyState: const EmptyChatWidget()); - }))), - ), - ], + emptyState: const EmptyChatWidget()) + .paddingLTRB(0, 2, 0, 0); + }))), ), ], ), - )); + ], + ), + ); } } diff --git a/lib/contact_invitation/views/contact_invitation_display.dart b/lib/contact_invitation/views/contact_invitation_display.dart index 2e4acad..25ca4e5 100644 --- a/lib/contact_invitation/views/contact_invitation_display.dart +++ b/lib/contact_invitation/views/contact_invitation_display.dart @@ -47,6 +47,7 @@ class ContactInvitationDisplayDialog extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); final textTheme = theme.textTheme; + final scaleConfig = theme.extension()!; final signedContactInvitationBytesV = context.watch().state; @@ -59,7 +60,8 @@ class ContactInvitationDisplayDialog extends StatelessWidget { child: Dialog( shape: RoundedRectangleBorder( side: const BorderSide(width: 2), - borderRadius: BorderRadius.circular(16)), + borderRadius: + BorderRadius.circular(16 * scaleConfig.borderRadiusScale)), backgroundColor: Colors.white, child: ConstrainedBox( constraints: BoxConstraints( diff --git a/lib/contact_invitation/views/contact_invitation_list_widget.dart b/lib/contact_invitation/views/contact_invitation_list_widget.dart index 19243b7..4ea44a3 100644 --- a/lib/contact_invitation/views/contact_invitation_list_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_list_widget.dart @@ -40,13 +40,14 @@ class ContactInvitationListWidgetState final theme = Theme.of(context); //final textTheme = theme.textTheme; final scale = theme.extension()!; + final scaleConfig = theme.extension()!; return Container( width: double.infinity, margin: const EdgeInsets.fromLTRB(4, 0, 4, 4), decoration: ShapeDecoration( shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(16 * scaleConfig.borderRadiusScale), )), constraints: const BoxConstraints(maxHeight: 200), child: Container( @@ -54,7 +55,8 @@ class ContactInvitationListWidgetState decoration: ShapeDecoration( color: scale.primaryScale.subtleBackground, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: + BorderRadius.circular(16 * scaleConfig.borderRadiusScale), )), child: ListView.builder( controller: _scrollController, diff --git a/lib/layout/home/drawer_menu/drawer_menu.dart b/lib/layout/home/drawer_menu/drawer_menu.dart index b8df2ea..855edcc 100644 --- a/lib/layout/home/drawer_menu/drawer_menu.dart +++ b/lib/layout/home/drawer_menu/drawer_menu.dart @@ -1,6 +1,7 @@ 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/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -47,18 +48,22 @@ class _DrawerMenuState extends State { }); } - Widget _wrapInBox({required Widget child, required Color color}) => + Widget _wrapInBox( + {required Widget child, + required Color color, + required double borderRadius}) => DecoratedBox( decoration: ShapeDecoration( color: color, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16))), + borderRadius: BorderRadius.circular(borderRadius))), child: child); Widget _makeAccountWidget( {required String name, required bool selected, required ScaleColor scale, + required ScaleConfig scaleConfig, required bool loggedIn, required void Function()? callback, required void Function()? footerCallback}) { @@ -71,13 +76,37 @@ class _DrawerMenuState extends State { shortname = abbrev; } + late final Color background; + late final Color hoverBackground; + late final Color activeBackground; + late final Color border; + late final Color hoverBorder; + late final Color activeBorder; + if (scaleConfig.useVisualIndicators && !scaleConfig.preferBorders) { + background = loggedIn ? scale.border : scale.subtleBorder; + hoverBackground = background; + activeBackground = background; + border = + selected ? scale.activeElementBackground : scale.elementBackground; + hoverBorder = border; + activeBorder = border; + } else { + background = + selected ? scale.activeElementBackground : scale.elementBackground; + hoverBackground = scale.hoverElementBackground; + activeBackground = scale.activeElementBackground; + border = loggedIn ? scale.border : scale.subtleBorder; + hoverBorder = scale.hoverBorder; + activeBorder = scale.primary; + } + final avatar = Container( height: 34, width: 34, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( - color: loggedIn ? scale.border : scale.subtleBorder, + color: border, width: 2, strokeAlign: BorderSide.strokeAlignOutside), color: Colors.blue, @@ -89,27 +118,28 @@ class _DrawerMenuState extends State { child: Text(shortname, style: theme.textTheme.titleLarge))); return AnimatedPadding( - padding: EdgeInsets.fromLTRB(selected ? 0 : 0, 0, selected ? 0 : 8, 0), + padding: EdgeInsets.fromLTRB(selected ? 0 : 8, selected ? 0 : 2, + selected ? 0 : 8, selected ? 0 : 2), duration: const Duration(milliseconds: 50), child: MenuItemWidget( title: name, headerWidget: avatar, - titleStyle: theme.textTheme.titleLarge!, + titleStyle: theme.textTheme.titleSmall! + .copyWith(color: scaleConfig.useVisualIndicators ? border : null), foregroundColor: scale.primary, - backgroundColor: selected - ? scale.activeElementBackground - : scale.elementBackground, - backgroundHoverColor: scale.hoverElementBackground, - backgroundFocusColor: scale.activeElementBackground, - borderColor: scale.border, - borderHoverColor: scale.hoverBorder, - borderFocusColor: scale.primary, + backgroundColor: background, + backgroundHoverColor: hoverBackground, + backgroundFocusColor: activeBackground, + borderColor: border, + borderHoverColor: hoverBorder, + borderFocusColor: activeBorder, + borderRadius: 16 * scaleConfig.borderRadiusScale, callback: callback, footerButtonIcon: loggedIn ? Icons.edit_outlined : null, footerCallback: footerCallback, - footerButtonIconColor: scale.border, - footerButtonIconHoverColor: scale.hoverElementBackground, - footerButtonIconFocusColor: scale.activeElementBackground, + footerButtonIconColor: border, + footerButtonIconHoverColor: hoverBackground, + footerButtonIconFocusColor: activeBackground, )); } @@ -120,6 +150,7 @@ class _DrawerMenuState extends State { perAccountCollectionBlocMapState}) { final theme = Theme.of(context); final scaleScheme = theme.extension()!; + final scaleConfig = theme.extension()!; final loggedInAccounts = []; final loggedOutAccounts = []; @@ -133,11 +164,12 @@ class _DrawerMenuState extends State { final avAccountRecordState = perAccountState?.avAccountRecordState; if (perAccountState != null && avAccountRecordState != null) { // Account is logged in - final scale = theme.extension()!.tertiaryScale; + final scale = theme.extension()!.primaryScale; final loggedInAccount = avAccountRecordState.when( data: (value) => _makeAccountWidget( name: value.profile.name, scale: scale, + scaleConfig: scaleConfig, selected: superIdentityRecordKey == activeLocalAccount, loggedIn: true, callback: () { @@ -152,10 +184,12 @@ class _DrawerMenuState extends State { }), loading: () => _wrapInBox( child: buildProgressIndicator(), - color: scaleScheme.grayScale.subtleBorder), + color: scaleScheme.grayScale.subtleBorder, + borderRadius: 16 * scaleConfig.borderRadiusScale), error: (err, st) => _wrapInBox( child: errorPage(err, st), - color: scaleScheme.errorScale.subtleBorder), + color: scaleScheme.errorScale.subtleBorder, + borderRadius: 16 * scaleConfig.borderRadiusScale), ); loggedInAccounts.add(loggedInAccount.paddingLTRB(0, 0, 0, 8)); } else { @@ -164,6 +198,7 @@ class _DrawerMenuState extends State { final loggedOutAccount = _makeAccountWidget( name: la.name, scale: scale, + scaleConfig: scaleConfig, selected: superIdentityRecordKey == activeLocalAccount, loggedIn: false, callback: () => {_doSwitchClick(superIdentityRecordKey)}, @@ -185,49 +220,77 @@ class _DrawerMenuState extends State { } Widget _getButton( - {required Icon icon, - required ScaleColor scale, - required String tooltip, - required void Function()? onPressed}) => - IconButton( - icon: icon, - color: scale.hoverBorder, - constraints: const BoxConstraints.expand(height: 64, width: 64), - style: ButtonStyle( - backgroundColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.hovered)) { - return scale.hoverElementBackground; - } - if (states.contains(WidgetState.focused)) { - return scale.activeElementBackground; - } - return scale.elementBackground; - }), shape: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.hovered)) { - return RoundedRectangleBorder( - side: BorderSide(color: scale.hoverBorder), - borderRadius: const BorderRadius.all(Radius.circular(16))); - } - if (states.contains(WidgetState.focused)) { - return RoundedRectangleBorder( - side: BorderSide(color: scale.primary), - borderRadius: const BorderRadius.all(Radius.circular(16))); - } + {required Icon icon, + required ScaleColor scale, + required ScaleConfig scaleConfig, + required String tooltip, + required void Function()? onPressed}) { + late final Color background; + late final Color hoverBackground; + late final Color activeBackground; + late final Color border; + late final Color hoverBorder; + late final Color activeBorder; + if (scaleConfig.useVisualIndicators && !scaleConfig.preferBorders) { + background = scale.border; + hoverBackground = scale.hoverBorder; + activeBackground = scale.primary; + border = scale.elementBackground; + hoverBorder = scale.hoverElementBackground; + activeBorder = scale.activeElementBackground; + } else { + background = scale.elementBackground; + hoverBackground = scale.hoverElementBackground; + activeBackground = scale.activeElementBackground; + border = scale.border; + hoverBorder = scale.hoverBorder; + activeBorder = scale.primary; + } + return IconButton( + icon: icon, + color: border, + constraints: const BoxConstraints.expand(height: 64, width: 64), + style: ButtonStyle( + backgroundColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.hovered)) { + return hoverBackground; + } + if (states.contains(WidgetState.focused)) { + return activeBackground; + } + return background; + }), shape: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.hovered)) { return RoundedRectangleBorder( - side: BorderSide(color: scale.border), - borderRadius: const BorderRadius.all(Radius.circular(16))); - })), - tooltip: tooltip, - onPressed: onPressed); + side: BorderSide(color: hoverBorder), + borderRadius: BorderRadius.all( + Radius.circular(16 * scaleConfig.borderRadiusScale))); + } + if (states.contains(WidgetState.focused)) { + return RoundedRectangleBorder( + side: BorderSide(color: activeBorder), + borderRadius: BorderRadius.all( + Radius.circular(16 * scaleConfig.borderRadiusScale))); + } + return RoundedRectangleBorder( + side: BorderSide(color: border), + borderRadius: BorderRadius.all( + Radius.circular(16 * scaleConfig.borderRadiusScale))); + })), + tooltip: tooltip, + onPressed: onPressed); + } Widget _getBottomButtons() { final theme = Theme.of(context); final scale = theme.extension()!; + final scaleConfig = theme.extension()!; final settingsButton = _getButton( icon: const Icon(Icons.settings), tooltip: translate('menu.settings_tooltip'), scale: scale.tertiaryScale, + scaleConfig: scaleConfig, onPressed: () async { await GoRouterHelper(context).push('/settings'); }).paddingLTRB(0, 0, 16, 0); @@ -236,6 +299,7 @@ class _DrawerMenuState extends State { icon: const Icon(Icons.add), tooltip: translate('menu.add_account_tooltip'), scale: scale.tertiaryScale, + scaleConfig: scaleConfig, onPressed: () async { await GoRouterHelper(context).push('/new_account'); }).paddingLTRB(0, 0, 16, 0); @@ -259,53 +323,105 @@ class _DrawerMenuState extends State { begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ - scale.tertiaryScale.hoverElementBackground, + scale.tertiaryScale.subtleBorder, scale.tertiaryScale.subtleBackground, ]); return DecoratedBox( decoration: ShapeDecoration( shadows: [ - BoxShadow( - color: scale.tertiaryScale.appBackground, - blurRadius: 6, - offset: const Offset( - 0, - 3, + if (scaleConfig.useVisualIndicators && !scaleConfig.preferBorders) + BoxShadow( + color: scale.tertiaryScale.primary.darken(80), + spreadRadius: 2, + ) + else if (scaleConfig.useVisualIndicators && + scaleConfig.preferBorders) + BoxShadow( + color: scale.tertiaryScale.border, + spreadRadius: 2, + ) + else + BoxShadow( + color: scale.tertiaryScale.primary.darken(40), + blurRadius: 6, + offset: const Offset( + 0, + 4, + ), ), - ), ], - gradient: gradient, - shape: const RoundedRectangleBorder( + gradient: scaleConfig.useVisualIndicators ? null : gradient, + color: scaleConfig.useVisualIndicators + ? (scaleConfig.preferBorders + ? scale.tertiaryScale.appBackground + : scale.tertiaryScale.subtleBorder) + : null, + shape: RoundedRectangleBorder( + side: scaleConfig.preferBorders + ? BorderSide(color: scale.tertiaryScale.primary, width: 2) + : BorderSide.none, borderRadius: BorderRadius.only( - topRight: Radius.circular(16), - bottomRight: Radius.circular(16)))), + topRight: Radius.circular(16 * scaleConfig.borderRadiusScale), + bottomRight: + Radius.circular(16 * scaleConfig.borderRadiusScale)))), child: Column(children: [ FittedBox( fit: BoxFit.scaleDown, - child: Row(children: [ - SvgPicture.asset( + child: ColorFiltered( + colorFilter: ColorFilter.mode( + theme.brightness == Brightness.light + ? scale.tertiaryScale.primary + : scale.tertiaryScale.border, + scaleConfig.preferBorders + ? BlendMode.modulate + : BlendMode.dst), + child: Row(children: [ + SvgPicture.asset( + height: 48, + 'assets/images/icon.svg', + colorFilter: scaleConfig.useVisualIndicators + ? grayColorFilter + : null) + .paddingLTRB(0, 0, 16, 0), + SvgPicture.asset( height: 48, - 'assets/images/icon.svg', + 'assets/images/title.svg', colorFilter: scaleConfig.useVisualIndicators ? grayColorFilter - : null) - .paddingLTRB(0, 0, 16, 0), - SvgPicture.asset( - height: 48, - 'assets/images/title.svg', - colorFilter: - scaleConfig.useVisualIndicators ? grayColorFilter : null), - ])), + : null), + ]))), const Spacer(), - _getAccountList( - localAccounts: localAccounts, - activeLocalAccount: activeLocalAccount, - perAccountCollectionBlocMapState: perAccountCollectionBlocMapState), + DecoratedBox( + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: !scaleConfig.useVisualIndicators + ? BorderSide.none + : scaleConfig.preferBorders + ? BorderSide(color: scale.tertiaryScale.border) + : BorderSide(color: scale.tertiaryScale.primary), + borderRadius: BorderRadius.circular( + 16 * scaleConfig.borderRadiusScale)), + color: scaleConfig.preferBorders + ? Colors.transparent + : scale.tertiaryScale.border.withAlpha(0x5F)), + child: Column(children: [ + Text(translate('menu.accounts'), + style: theme.textTheme.titleMedium!.copyWith( + color: scaleConfig.preferBorders + ? scale.tertiaryScale.border + : scale.tertiaryScale.primary)) + .paddingLTRB(0, 0, 0, 16), + _getAccountList( + localAccounts: localAccounts, + activeLocalAccount: activeLocalAccount, + perAccountCollectionBlocMapState: + perAccountCollectionBlocMapState) + ]).paddingAll(16)), _getBottomButtons(), const Spacer(), Row(children: [ - Text('Version $packageInfoVersion', + Text('${translate('menu.version')} $packageInfoVersion', style: theme.textTheme.labelMedium! .copyWith(color: scale.tertiaryScale.hoverBorder)), const Spacer(), @@ -315,6 +431,6 @@ class _DrawerMenuState extends State { ), ]) ]).paddingAll(16), - ); + ).paddingLTRB(0, 2, 2, 2); } } diff --git a/lib/layout/home/drawer_menu/menu_item_widget.dart b/lib/layout/home/drawer_menu/menu_item_widget.dart index 260646f..b94a243 100644 --- a/lib/layout/home/drawer_menu/menu_item_widget.dart +++ b/lib/layout/home/drawer_menu/menu_item_widget.dart @@ -1,7 +1,6 @@ import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; class MenuItemWidget extends StatelessWidget { const MenuItemWidget({ @@ -17,6 +16,7 @@ class MenuItemWidget extends StatelessWidget { this.borderColor, this.borderHoverColor, this.borderFocusColor, + this.borderRadius, this.footerButtonIcon, this.footerButtonIconColor, this.footerButtonIconHoverColor, @@ -41,18 +41,20 @@ class MenuItemWidget extends StatelessWidget { side: WidgetStateBorderSide.resolveWith((states) { if (states.contains(WidgetState.hovered)) { return borderColor != null - ? BorderSide(color: borderHoverColor!) + ? BorderSide(width: 2, color: borderHoverColor!) : null; } if (states.contains(WidgetState.focused)) { return borderColor != null - ? BorderSide(color: borderFocusColor!) + ? BorderSide(width: 2, color: borderFocusColor!) : null; } - return borderColor != null ? BorderSide(color: borderColor!) : null; + return borderColor != null + ? BorderSide(width: 2, color: borderColor!) + : null; }), - shape: WidgetStateProperty.all( - RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)))), + shape: WidgetStateProperty.all(RoundedRectangleBorder( + borderRadius: BorderRadius.circular(borderRadius ?? 0)))), child: Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( @@ -104,6 +106,7 @@ class MenuItemWidget extends StatelessWidget { ..add(ColorProperty('backgroundHoverColor', backgroundHoverColor)) ..add(ColorProperty('backgroundFocusColor', backgroundFocusColor)) ..add(ColorProperty('borderColor', borderColor)) + ..add(DoubleProperty('borderRadius', borderRadius)) ..add(ColorProperty('borderHoverColor', borderHoverColor)) ..add(ColorProperty('borderFocusColor', borderFocusColor)); } @@ -122,6 +125,7 @@ class MenuItemWidget extends StatelessWidget { final Color? backgroundHoverColor; final Color? backgroundFocusColor; final Color? borderColor; + final double? borderRadius; final Color? borderHoverColor; final Color? borderFocusColor; final Color? footerButtonIconColor; 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 f682929..0c50bbd 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 @@ -30,23 +30,39 @@ class _HomeAccountReadyMainState extends State { (c) => c.state.asData!.value.profile); final theme = Theme.of(context); final scale = theme.extension()!; + final scaleConfig = theme.extension()!; return ColoredBox( - color: scale.primaryScale.subtleBorder, + color: scaleConfig.preferBorders + ? scale.primaryScale.subtleBackground + : scale.primaryScale.subtleBorder, child: Column(children: [ Row(children: [ IconButton( icon: const Icon(Icons.menu), - color: scale.secondaryScale.borderText, + color: scaleConfig.preferBorders + ? scale.primaryScale.border + : scale.primaryScale.borderText, constraints: const BoxConstraints.expand(height: 64, width: 64), style: ButtonStyle( backgroundColor: WidgetStateProperty.all( - scale.primaryScale.hoverBorder), + scaleConfig.preferBorders + ? scale.primaryScale.hoverElementBackground + : scale.primaryScale.hoverBorder), shape: WidgetStateProperty.all( - const RoundedRectangleBorder( - borderRadius: - BorderRadius.all(Radius.circular(16))))), + RoundedRectangleBorder( + side: !scaleConfig.useVisualIndicators + ? BorderSide.none + : BorderSide( + strokeAlign: BorderSide.strokeAlignCenter, + color: scaleConfig.preferBorders + ? scale.primaryScale.border + : scale.primaryScale.borderText, + width: 2), + borderRadius: BorderRadius.all(Radius.circular( + 16 * scaleConfig.borderRadiusScale))), + )), tooltip: translate('menu.settings_tooltip'), onPressed: () async { final ctrl = context.read(); @@ -82,6 +98,7 @@ class _HomeAccountReadyMainState extends State { final w = MediaQuery.of(context).size.width; final theme = Theme.of(context); final scale = theme.extension()!; + final scaleConfig = theme.extension()!; final children = [ ConstrainedBox( @@ -92,7 +109,10 @@ class _HomeAccountReadyMainState extends State { SizedBox( width: 2, height: double.infinity, - child: ColoredBox(color: scale.primaryScale.hoverBorder)), + child: ColoredBox( + color: scaleConfig.preferBorders + ? scale.primaryScale.subtleBorder + : scale.primaryScale.subtleBackground)), Expanded(child: buildTabletRightPane(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 0d8650e..f80a8fe 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 @@ -34,6 +34,7 @@ class AccountPageState extends State { final theme = Theme.of(context); final textTheme = theme.textTheme; final scale = theme.extension()!; + final scaleConfig = theme.extension()!; final cilState = context.watch().state; final cilBusy = cilState.busy; @@ -55,10 +56,12 @@ class AccountPageState extends State { backgroundColor: scale.primaryScale.border, collapsedBackgroundColor: scale.primaryScale.border, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: + BorderRadius.circular(16 * scaleConfig.borderRadiusScale), ), collapsedShape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: + BorderRadius.circular(16 * scaleConfig.borderRadiusScale), ), title: Text( translate('account_page.contact_invitations'), 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 1e79344..d043433 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,12 +1,14 @@ import 'dart:async'; +import 'package:animated_bottom_navigation_bar/' + 'animated_bottom_navigation_bar.dart'; +import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:preload_page_view/preload_page_view.dart'; import 'package:provider/provider.dart'; -import 'package:stylish_bottom_bar/stylish_bottom_bar.dart'; import '../../../../chat/chat.dart'; import '../../../../contact_invitation/contact_invitation.dart'; @@ -39,7 +41,7 @@ class MainPagerState extends State with TickerProviderStateMixin { super.dispose(); } - bool onScrollNotification(ScrollNotification notification) { + bool _onScrollNotification(ScrollNotification notification) { if (notification is UserScrollNotification && notification.metrics.axis == Axis.vertical) { switch (notification.direction) { @@ -58,30 +60,6 @@ class MainPagerState extends State with TickerProviderStateMixin { return false; } - BottomBarItem buildBottomBarItem(int index) { - final theme = Theme.of(context); - final scale = theme.extension()!; - return BottomBarItem( - title: Text(_bottomLabelList[index]), - icon: - Icon(_selectedIconList[index], color: scale.primaryScale.borderText), - selectedIcon: - Icon(_selectedIconList[index], color: scale.primaryScale.borderText), - backgroundColor: scale.primaryScale.borderText, - //badge: const Text('9+'), - //showBadge: true, - ); - } - - List _buildBottomBarItems() { - final bottomBarItems = List.empty(growable: true); - for (var index = 0; index < _bottomLabelList.length; index++) { - final item = buildBottomBarItem(index); - bottomBarItems.add(item); - } - return bottomBarItems; - } - Future scanContactInvitationDialog(BuildContext context) async { await showDialog( context: context, @@ -104,6 +82,63 @@ class MainPagerState extends State with TickerProviderStateMixin { }); } + Widget _buildBottomBarItem(int index, bool isActive) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + + final color = scaleConfig.useVisualIndicators + ? (scaleConfig.preferBorders + ? scale.primaryScale.border + : scale.primaryScale.borderText) + : (isActive + ? (scaleConfig.preferBorders + ? scale.primaryScale.border + : scale.primaryScale.borderText) + : (scaleConfig.preferBorders + ? scale.primaryScale.subtleBorder + : scale.primaryScale.borderText.withAlpha(0x80))); + + final item = Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + _selectedIconList[index], + size: 24, + color: color, + ), + const SizedBox(height: 4), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Text( + _bottomLabelList[index], + style: theme.textTheme.labelLarge!.copyWith( + fontWeight: isActive ? FontWeight.bold : FontWeight.normal, + color: color), + ), + ) + ], + ); + + if (scaleConfig.useVisualIndicators && isActive) { + return DecoratedBox( + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 14 * scaleConfig.borderRadiusScale), + side: BorderSide( + width: 2, + color: scaleConfig.preferBorders + ? scale.primaryScale.border + : scale.primaryScale.borderText))), + child: item) + .paddingLTRB(8, 0, 8, 6); + } + + return item; + } + Widget _bottomSheetBuilder(BuildContext sheetContext, BuildContext context) { if (currentPage == 0) { // New contact invitation @@ -122,12 +157,13 @@ class MainPagerState extends State with TickerProviderStateMixin { Widget build(BuildContext context) { final theme = Theme.of(context); final scale = theme.extension()!; + final scaleConfig = theme.extension()!; return Scaffold( //extendBody: true, backgroundColor: Colors.transparent, body: NotificationListener( - onNotification: onScrollNotification, + onNotification: _onScrollNotification, child: PreloadPageView( key: _pageViewKey, controller: pageController, @@ -148,31 +184,46 @@ class MainPagerState extends State with TickerProviderStateMixin { // style: Theme.of(context).textTheme.headlineSmall, // ), // ), - bottomNavigationBar: StylishBottomBar( - backgroundColor: scale.primaryScale.hoverBorder, - option: AnimatedBarOptions( - inkEffect: true, - inkColor: scale.primaryScale.hoverPrimary, - opacity: 0.3, - ), - items: _buildBottomBarItems(), - hasNotch: true, - fabLocation: StylishBarFabLocation.end, - currentIndex: currentPage, + bottomNavigationBar: AnimatedBottomNavigationBar.builder( + itemCount: 2, + height: 64, + tabBuilder: _buildBottomBarItem, + activeIndex: currentPage, + gapLocation: GapLocation.end, + gapWidth: 90, + notchSmoothness: NotchSmoothness.defaultEdge, + notchMargin: 4, + backgroundColor: scaleConfig.preferBorders + ? scale.primaryScale.hoverElementBackground + : scale.primaryScale.hoverBorder, + elevation: 0, onTap: (index) async { await pageController.animateToPage(index, duration: 250.ms, curve: Curves.easeInOut); }, ), - floatingActionButton: BottomSheetActionButton( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(14))), - foregroundColor: scale.secondaryScale.borderText, - backgroundColor: scale.secondaryScale.hoverBorder, + shape: CircleBorder( + side: !scaleConfig.useVisualIndicators + ? BorderSide.none + : BorderSide( + strokeAlign: BorderSide.strokeAlignCenter, + color: scaleConfig.preferBorders + ? scale.secondaryScale.border + : scale.secondaryScale.borderText, + width: 2), + ), + foregroundColor: scaleConfig.preferBorders + ? scale.secondaryScale.border + : scale.secondaryScale.borderText, + backgroundColor: scaleConfig.preferBorders + ? scale.secondaryScale.hoverElementBackground + : scale.secondaryScale.hoverBorder, builder: (context) => Icon( _fabIconList[currentPage], - color: scale.secondaryScale.borderText, + color: scaleConfig.preferBorders + ? scale.secondaryScale.border + : scale.secondaryScale.borderText, ), bottomSheetBuilder: (sheetContext) => _bottomSheetBuilder(sheetContext, context)), diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index 47d3d64..1e10d27 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -118,6 +118,23 @@ class HomeScreenState extends State } } + Widget _applyPageBorder(Widget child) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + + return ValueListenableBuilder( + valueListenable: _zoomDrawerController.stateNotifier!, + child: child, + builder: (context, drawerState, staticChild) => clipBorder( + clipEnabled: drawerState != DrawerState.closed, + borderEnabled: + scaleConfig.preferBorders && scaleConfig.useVisualIndicators, + borderRadius: 16 * scaleConfig.borderRadiusScale, + borderColor: scale.primaryScale.border, + child: staticChild!)); + } + Widget _buildAccountPageView(BuildContext context) { final localAccounts = context.watch().state; final activeLocalAccount = context.watch().state; @@ -127,7 +144,7 @@ class HomeScreenState extends State final activeIndex = localAccounts .indexWhere((x) => x.superIdentity.recordKey == activeLocalAccount); if (activeIndex == -1) { - return const HomeNoActive(); + return _applyPageBorder(const HomeNoActive()); } final accountPages = []; @@ -141,7 +158,7 @@ class HomeScreenState extends State } final accountPage = _buildAccountPage( context, superIdentityRecordKey, perAccountCollectionState); - accountPages.add(KeyedSubtree.wrap(accountPage, i)); + accountPages.add(_applyPageBorder(accountPage)); } return SlideIndexedStack( @@ -154,15 +171,6 @@ class HomeScreenState extends State @override Widget build(BuildContext context) { final theme = Theme.of(context); - final scale = theme.extension()!; - - final gradient = LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - scale.tertiaryScale.subtleBackground, - scale.tertiaryScale.appBackground, - ]); final localAccounts = context.watch().state; final activeLocalAccount = context.watch().state; @@ -171,8 +179,8 @@ class HomeScreenState extends State final canClose = activeIndex != -1; return SafeArea( - child: DecoratedBox( - decoration: BoxDecoration(gradient: gradient), + child: DefaultTextStyle( + style: theme.textTheme.bodySmall!, child: ZoomDrawer( controller: _zoomDrawerController, //menuBackgroundColor: Colors.transparent, @@ -188,18 +196,16 @@ class HomeScreenState extends State mainScreen: Provider.value( value: _zoomDrawerController, child: Builder(builder: _buildAccountPageView)), - borderRadius: 24, - //showShadow: false, + borderRadius: 0, angle: 0, - drawerShadowsBackgroundColor: theme.shadowColor, - mainScreenOverlayColor: theme.shadowColor.withAlpha(0x3F), + mainScreenOverlayColor: theme.shadowColor.withAlpha(0x2F), openCurve: Curves.fastEaseInToSlowEaseOut, // duration: const Duration(milliseconds: 250), // reverseDuration: const Duration(milliseconds: 250), menuScreenTapClose: canClose, mainScreenTapClose: canClose, disableDragGesture: !canClose, - mainScreenScale: .25, + mainScreenScale: .15, slideWidth: min(360, MediaQuery.of(context).size.width * 0.9), ))); } diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart index 5eb89fb..eefbbd2 100644 --- a/lib/settings/settings_page.dart +++ b/lib/settings/settings_page.dart @@ -36,19 +36,18 @@ class SettingsPageState extends State { @override Widget build(BuildContext context) => AsyncBlocBuilder( - builder: (context, state) => ThemeSwitchingArea( - child: Scaffold( - appBar: DefaultAppBar( - title: Text(translate('settings_page.titlebar')), - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => GoRouterHelper(context).pop(), - ), - actions: [ - const SignalStrengthMeterWidget() - .paddingLTRB(16, 0, 16, 0), - ]), - body: FormBuilder( + builder: (context, state) => StyledScaffold( + appBar: DefaultAppBar( + title: Text(translate('settings_page.titlebar')), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => GoRouterHelper(context).pop(), + ), + actions: [ + const SignalStrengthMeterWidget().paddingLTRB(16, 0, 16, 0), + ]), + body: ThemeSwitchingArea( + child: FormBuilder( key: _formKey, child: ListView( children: [ diff --git a/lib/theme/models/chat_theme.dart b/lib/theme/models/chat_theme.dart index 15fcdc3..2d52eae 100644 --- a/lib/theme/models/chat_theme.dart +++ b/lib/theme/models/chat_theme.dart @@ -5,47 +5,82 @@ import 'package:flutter_chat_ui/flutter_chat_ui.dart'; import 'scale_scheme.dart'; -ChatTheme makeChatTheme(ScaleScheme scale, TextTheme textTheme) => +ChatTheme makeChatTheme( + ScaleScheme scale, ScaleConfig scaleConfig, TextTheme textTheme) => DefaultChatTheme( - primaryColor: scale.primaryScale.calloutBackground, - secondaryColor: scale.secondaryScale.calloutBackground, + primaryColor: scaleConfig.preferBorders + ? scale.primaryScale.calloutText + : scale.primaryScale.calloutBackground, + secondaryColor: scaleConfig.preferBorders + ? scale.secondaryScale.calloutText + : scale.secondaryScale.calloutBackground, backgroundColor: scale.grayScale.appBackground, + messageBorderRadius: scaleConfig.borderRadiusScale * 16, + bubbleBorderSide: scaleConfig.preferBorders + ? BorderSide( + color: scale.primaryScale.calloutBackground, + width: 2, + ) + : null, sendButtonIcon: Image.asset( 'assets/icon-send.png', - color: scale.primaryScale.borderText, + color: scaleConfig.preferBorders + ? scale.primaryScale.border + : scale.primaryScale.borderText, package: 'flutter_chat_ui', ), inputBackgroundColor: Colors.blue, inputBorderRadius: BorderRadius.zero, inputTextDecoration: InputDecoration( - filled: true, - fillColor: scale.primaryScale.elementBackground, + filled: !scaleConfig.preferBorders, + fillColor: scale.primaryScale.subtleBackground, isDense: true, contentPadding: const EdgeInsets.fromLTRB(8, 12, 8, 12), - border: const OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.all(Radius.circular(8))), - focusedBorder: const OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.all(Radius.circular(8))), + disabledBorder: OutlineInputBorder( + borderSide: scaleConfig.preferBorders + ? BorderSide(color: scale.grayScale.border, width: 2) + : BorderSide.none, + borderRadius: BorderRadius.all( + Radius.circular(8 * scaleConfig.borderRadiusScale))), + enabledBorder: OutlineInputBorder( + borderSide: scaleConfig.preferBorders + ? BorderSide(color: scale.primaryScale.border, width: 2) + : BorderSide.none, + borderRadius: BorderRadius.all( + Radius.circular(8 * scaleConfig.borderRadiusScale))), + focusedBorder: OutlineInputBorder( + borderSide: scaleConfig.preferBorders + ? BorderSide(color: scale.primaryScale.border, width: 2) + : BorderSide.none, + borderRadius: BorderRadius.all( + Radius.circular(8 * scaleConfig.borderRadiusScale))), ), - inputContainerDecoration: - BoxDecoration(color: scale.primaryScale.border), - inputPadding: const EdgeInsets.all(9), - inputTextColor: scale.primaryScale.appText, + inputContainerDecoration: BoxDecoration( + border: scaleConfig.preferBorders + ? Border( + top: BorderSide(color: scale.primaryScale.border, width: 2)) + : null, + color: scaleConfig.preferBorders + ? scale.primaryScale.elementBackground + : scale.primaryScale.border), + inputPadding: const EdgeInsets.all(12), + inputTextColor: !scaleConfig.preferBorders + ? scale.primaryScale.appText + : scale.primaryScale.border, attachmentButtonIcon: const Icon(Icons.attach_file), - sentMessageBodyTextStyle: TextStyle( - color: scale.primaryScale.calloutText, - fontSize: 16, - fontWeight: FontWeight.w500, - height: 1.5, + sentMessageBodyTextStyle: textTheme.bodyLarge!.copyWith( + color: scaleConfig.preferBorders + ? scale.primaryScale.calloutBackground + : scale.primaryScale.calloutText, ), sentEmojiMessageTextStyle: const TextStyle( color: Colors.white, fontSize: 64, ), receivedMessageBodyTextStyle: TextStyle( - color: scale.secondaryScale.calloutText, + color: scaleConfig.preferBorders + ? scale.secondaryScale.calloutBackground + : scale.secondaryScale.calloutText, fontSize: 16, fontWeight: FontWeight.w500, height: 1.5, diff --git a/lib/theme/models/contrast_generator.dart b/lib/theme/models/contrast_generator.dart index 52b32c9..09cef2b 100644 --- a/lib/theme/models/contrast_generator.dart +++ b/lib/theme/models/contrast_generator.dart @@ -5,11 +5,14 @@ import 'scale_color.dart'; import 'scale_input_decorator_theme.dart'; import 'scale_scheme.dart'; -ScaleScheme _contrastScale(Brightness brightness) { - final back = brightness == Brightness.light ? Colors.white : Colors.black; - final front = brightness == Brightness.light ? Colors.black : Colors.white; +ScaleColor _contrastScaleColor( + {required Brightness brightness, + required Color frontColor, + required Color backColor}) { + final back = brightness == Brightness.light ? backColor : frontColor; + final front = brightness == Brightness.light ? frontColor : backColor; - final primaryScale = ScaleColor( + return ScaleColor( appBackground: back, subtleBackground: back, elementBackground: back, @@ -28,21 +31,236 @@ ScaleScheme _contrastScale(Brightness brightness) { calloutBackground: front, calloutText: back, ); - - return ScaleScheme( - primaryScale: primaryScale, - primaryAlphaScale: primaryScale, - secondaryScale: primaryScale, - tertiaryScale: primaryScale, - grayScale: primaryScale, - errorScale: primaryScale); } -ThemeData contrastGenerator(Brightness brightness) { - final textTheme = makeRadixTextTheme(brightness); - final scaleScheme = _contrastScale(brightness); - final colorScheme = scaleScheme.toColorScheme(brightness); - final scaleConfig = ScaleConfig(useVisualIndicators: true); +const kMonoSpaceFontDisplay = 'Source Code Pro'; +const kMonoSpaceFontText = 'Source Code Pro'; + +TextTheme makeMonoSpaceTextTheme(Brightness brightness) => + (brightness == Brightness.light) + ? const TextTheme( + displayLarge: TextStyle( + debugLabel: 'blackMonoSpace displayLarge', + fontFamily: kMonoSpaceFontDisplay, + color: Colors.black54, + decoration: TextDecoration.none), + displayMedium: TextStyle( + debugLabel: 'blackMonoSpace displayMedium', + fontFamily: kMonoSpaceFontDisplay, + color: Colors.black54, + decoration: TextDecoration.none), + displaySmall: TextStyle( + debugLabel: 'blackMonoSpace displaySmall', + fontFamily: kMonoSpaceFontDisplay, + color: Colors.black54, + decoration: TextDecoration.none), + headlineLarge: TextStyle( + debugLabel: 'blackMonoSpace headlineLarge', + fontFamily: kMonoSpaceFontDisplay, + color: Colors.black54, + decoration: TextDecoration.none), + headlineMedium: TextStyle( + debugLabel: 'blackMonoSpace headlineMedium', + fontFamily: kMonoSpaceFontDisplay, + color: Colors.black54, + decoration: TextDecoration.none), + headlineSmall: TextStyle( + debugLabel: 'blackMonoSpace headlineSmall', + fontFamily: kMonoSpaceFontDisplay, + color: Colors.black87, + decoration: TextDecoration.none), + titleLarge: TextStyle( + debugLabel: 'blackMonoSpace titleLarge', + fontFamily: kMonoSpaceFontDisplay, + color: Colors.black87, + decoration: TextDecoration.none), + titleMedium: TextStyle( + debugLabel: 'blackMonoSpace titleMedium', + fontFamily: kMonoSpaceFontText, + color: Colors.black87, + decoration: TextDecoration.none), + titleSmall: TextStyle( + debugLabel: 'blackMonoSpace titleSmall', + fontFamily: kMonoSpaceFontText, + color: Colors.black, + decoration: TextDecoration.none), + bodyLarge: TextStyle( + debugLabel: 'blackMonoSpace bodyLarge', + fontFamily: kMonoSpaceFontText, + color: Colors.black87, + decoration: TextDecoration.none), + bodyMedium: TextStyle( + debugLabel: 'blackMonoSpace bodyMedium', + fontFamily: kMonoSpaceFontText, + color: Colors.black87, + decoration: TextDecoration.none), + bodySmall: TextStyle( + debugLabel: 'blackMonoSpace bodySmall', + fontFamily: kMonoSpaceFontText, + color: Colors.black54, + decoration: TextDecoration.none), + labelLarge: TextStyle( + debugLabel: 'blackMonoSpace labelLarge', + fontFamily: kMonoSpaceFontText, + color: Colors.black87, + decoration: TextDecoration.none), + labelMedium: TextStyle( + debugLabel: 'blackMonoSpace labelMedium', + fontFamily: kMonoSpaceFontText, + color: Colors.black, + decoration: TextDecoration.none), + labelSmall: TextStyle( + debugLabel: 'blackMonoSpace labelSmall', + fontFamily: kMonoSpaceFontText, + color: Colors.black, + decoration: TextDecoration.none), + ) + : const TextTheme( + displayLarge: TextStyle( + debugLabel: 'whiteMonoSpace displayLarge', + fontFamily: kMonoSpaceFontDisplay, + color: Colors.white70, + decoration: TextDecoration.none), + displayMedium: TextStyle( + debugLabel: 'whiteMonoSpace displayMedium', + fontFamily: kMonoSpaceFontDisplay, + color: Colors.white70, + decoration: TextDecoration.none), + displaySmall: TextStyle( + debugLabel: 'whiteMonoSpace displaySmall', + fontFamily: kMonoSpaceFontDisplay, + color: Colors.white70, + decoration: TextDecoration.none), + headlineLarge: TextStyle( + debugLabel: 'whiteMonoSpace headlineLarge', + fontFamily: kMonoSpaceFontDisplay, + color: Colors.white70, + decoration: TextDecoration.none), + headlineMedium: TextStyle( + debugLabel: 'whiteMonoSpace headlineMedium', + fontFamily: kMonoSpaceFontDisplay, + color: Colors.white70, + decoration: TextDecoration.none), + headlineSmall: TextStyle( + debugLabel: 'whiteMonoSpace headlineSmall', + fontFamily: kMonoSpaceFontDisplay, + color: Colors.white, + decoration: TextDecoration.none), + titleLarge: TextStyle( + debugLabel: 'whiteMonoSpace titleLarge', + fontFamily: kMonoSpaceFontDisplay, + color: Colors.white, + decoration: TextDecoration.none), + titleMedium: TextStyle( + debugLabel: 'whiteMonoSpace titleMedium', + fontFamily: kMonoSpaceFontText, + color: Colors.white, + decoration: TextDecoration.none), + titleSmall: TextStyle( + debugLabel: 'whiteMonoSpace titleSmall', + fontFamily: kMonoSpaceFontText, + color: Colors.white, + decoration: TextDecoration.none), + bodyLarge: TextStyle( + debugLabel: 'whiteMonoSpace bodyLarge', + fontFamily: kMonoSpaceFontText, + color: Colors.white, + decoration: TextDecoration.none), + bodyMedium: TextStyle( + debugLabel: 'whiteMonoSpace bodyMedium', + fontFamily: kMonoSpaceFontText, + color: Colors.white, + decoration: TextDecoration.none), + bodySmall: TextStyle( + debugLabel: 'whiteMonoSpace bodySmall', + fontFamily: kMonoSpaceFontText, + color: Colors.white70, + decoration: TextDecoration.none), + labelLarge: TextStyle( + debugLabel: 'whiteMonoSpace labelLarge', + fontFamily: kMonoSpaceFontText, + color: Colors.white, + decoration: TextDecoration.none), + labelMedium: TextStyle( + debugLabel: 'whiteMonoSpace labelMedium', + fontFamily: kMonoSpaceFontText, + color: Colors.white, + decoration: TextDecoration.none), + labelSmall: TextStyle( + debugLabel: 'whiteMonoSpace labelSmall', + fontFamily: kMonoSpaceFontText, + color: Colors.white, + decoration: TextDecoration.none), + ); + +ScaleScheme _contrastScaleScheme( + {required Brightness brightness, + required Color primaryFront, + required Color primaryBack, + required Color secondaryFront, + required Color secondaryBack, + required Color tertiaryFront, + required Color tertiaryBack, + required Color grayFront, + required Color grayBack, + required Color errorFront, + required Color errorBack}) => + ScaleScheme( + primaryScale: _contrastScaleColor( + brightness: brightness, + frontColor: primaryFront, + backColor: primaryBack), + primaryAlphaScale: _contrastScaleColor( + brightness: brightness, + frontColor: primaryFront, + backColor: primaryBack), + secondaryScale: _contrastScaleColor( + brightness: brightness, + frontColor: secondaryFront, + backColor: secondaryBack), + tertiaryScale: _contrastScaleColor( + brightness: brightness, + frontColor: tertiaryFront, + backColor: tertiaryBack), + grayScale: _contrastScaleColor( + brightness: brightness, frontColor: grayFront, backColor: grayBack), + errorScale: _contrastScaleColor( + brightness: brightness, + frontColor: errorFront, + backColor: errorBack)); + +ThemeData contrastGenerator({ + required Brightness brightness, + required ScaleConfig scaleConfig, + required Color primaryFront, + required Color primaryBack, + required Color secondaryFront, + required Color secondaryBack, + required Color tertiaryFront, + required Color tertiaryBack, + required Color grayFront, + required Color grayBack, + required Color errorFront, + required Color errorBack, + TextTheme? customTextTheme, +}) { + final textTheme = customTextTheme ?? makeRadixTextTheme(brightness); + final scaleScheme = _contrastScaleScheme( + brightness: brightness, + primaryFront: primaryFront, + primaryBack: primaryBack, + secondaryFront: secondaryFront, + secondaryBack: secondaryBack, + tertiaryFront: tertiaryFront, + tertiaryBack: tertiaryBack, + grayFront: grayFront, + grayBack: grayBack, + errorFront: errorFront, + errorBack: errorBack, + ); + final colorScheme = scaleScheme.toColorScheme( + brightness, + ); final themeData = ThemeData.from( colorScheme: colorScheme, textTheme: textTheme, useMaterial3: true); @@ -50,10 +268,11 @@ ThemeData contrastGenerator(Brightness brightness) { bottomSheetTheme: themeData.bottomSheetTheme.copyWith( elevation: 0, modalElevation: 0, - shape: const RoundedRectangleBorder( + shape: RoundedRectangleBorder( borderRadius: BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16)))), + topLeft: Radius.circular(16 * scaleConfig.borderRadiusScale), + topRight: + Radius.circular(16 * scaleConfig.borderRadiusScale)))), canvasColor: scaleScheme.primaryScale.subtleBackground, chipTheme: themeData.chipTheme.copyWith( backgroundColor: scaleScheme.primaryScale.elementBackground, @@ -69,13 +288,15 @@ ThemeData contrastGenerator(Brightness brightness) { disabledForegroundColor: scaleScheme.grayScale.appText, shape: RoundedRectangleBorder( side: BorderSide(color: scaleScheme.primaryScale.border), - borderRadius: BorderRadius.circular(8))), + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale))), ), textSelectionTheme: TextSelectionThemeData( cursorColor: scaleScheme.primaryScale.appText, selectionColor: scaleScheme.primaryScale.appText.withAlpha(0x7F), selectionHandleColor: scaleScheme.primaryScale.appText), - inputDecorationTheme: ScaleInputDecoratorTheme(scaleScheme, textTheme), + inputDecorationTheme: + ScaleInputDecoratorTheme(scaleScheme, scaleConfig, textTheme), extensions: >[ scaleScheme, scaleConfig, diff --git a/lib/theme/models/radix_generator.dart b/lib/theme/models/radix_generator.dart index 4bd593f..92d52c8 100644 --- a/lib/theme/models/radix_generator.dart +++ b/lib/theme/models/radix_generator.dart @@ -604,7 +604,11 @@ ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) { final radix = _radixScheme(brightness, themeColor); final scaleScheme = radix.toScale(); final colorScheme = scaleScheme.toColorScheme(brightness); - final scaleConfig = ScaleConfig(useVisualIndicators: false); + final scaleConfig = ScaleConfig( + useVisualIndicators: false, + preferBorders: false, + borderRadiusScale: 1, + ); final themeData = ThemeData.from( colorScheme: colorScheme, textTheme: textTheme, useMaterial3: true); @@ -654,8 +658,10 @@ ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) { disabledForegroundColor: scaleScheme.grayScale.primary, shape: RoundedRectangleBorder( side: BorderSide(color: scaleScheme.primaryScale.border), - borderRadius: BorderRadius.circular(8))), + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale))), ), - inputDecorationTheme: ScaleInputDecoratorTheme(scaleScheme, textTheme), + inputDecorationTheme: + ScaleInputDecoratorTheme(scaleScheme, scaleConfig, textTheme), extensions: >[scaleScheme, scaleConfig]); } diff --git a/lib/theme/models/scale_input_decorator_theme.dart b/lib/theme/models/scale_input_decorator_theme.dart index f6865cd..265670e 100644 --- a/lib/theme/models/scale_input_decorator_theme.dart +++ b/lib/theme/models/scale_input_decorator_theme.dart @@ -3,11 +3,13 @@ import 'package:flutter/material.dart'; import 'scale_scheme.dart'; class ScaleInputDecoratorTheme extends InputDecorationTheme { - ScaleInputDecoratorTheme(this._scaleScheme, this._textTheme) + ScaleInputDecoratorTheme( + this._scaleScheme, ScaleConfig scaleConfig, this._textTheme) : super( border: OutlineInputBorder( borderSide: BorderSide(color: _scaleScheme.primaryScale.border), - borderRadius: BorderRadius.circular(8)), + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale)), contentPadding: const EdgeInsets.all(8), labelStyle: TextStyle( color: _scaleScheme.primaryScale.subtleText.withAlpha(127)), @@ -16,7 +18,8 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme { focusedBorder: OutlineInputBorder( borderSide: BorderSide( color: _scaleScheme.primaryScale.hoverBorder, width: 2), - borderRadius: BorderRadius.circular(8))); + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale))); final ScaleScheme _scaleScheme; final TextTheme _textTheme; diff --git a/lib/theme/models/scale_scheme.dart b/lib/theme/models/scale_scheme.dart index 990fe1e..512fda6 100644 --- a/lib/theme/models/scale_scheme.dart +++ b/lib/theme/models/scale_scheme.dart @@ -1,3 +1,6 @@ +import 'dart:ui'; + +import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/material.dart'; import 'scale_color.dart'; @@ -97,7 +100,7 @@ class ScaleScheme extends ThemeExtension { onSurfaceVariant: secondaryScale.primaryText, // ?? reviewed a little outline: primaryScale.border, outlineVariant: secondaryScale.border, - shadow: const Color(0xFF000000), + shadow: primaryScale.primary.darken(80), //scrim: primaryScale.background, // inverseSurface: primaryScale.subtleText, // onInverseSurface: primaryScale.subtleBackground, @@ -109,16 +112,24 @@ class ScaleScheme extends ThemeExtension { class ScaleConfig extends ThemeExtension { ScaleConfig({ required this.useVisualIndicators, + required this.preferBorders, + required this.borderRadiusScale, }); final bool useVisualIndicators; + final bool preferBorders; + final double borderRadiusScale; @override ScaleConfig copyWith({ bool? useVisualIndicators, + bool? preferBorders, + double? borderRadiusScale, }) => ScaleConfig( useVisualIndicators: useVisualIndicators ?? this.useVisualIndicators, + preferBorders: preferBorders ?? this.preferBorders, + borderRadiusScale: borderRadiusScale ?? this.borderRadiusScale, ); @override @@ -127,8 +138,10 @@ class ScaleConfig extends ThemeExtension { return this; } return ScaleConfig( - useVisualIndicators: - t < .5 ? useVisualIndicators : other.useVisualIndicators, - ); + useVisualIndicators: + t < .5 ? useVisualIndicators : other.useVisualIndicators, + preferBorders: t < .5 ? preferBorders : other.preferBorders, + borderRadiusScale: + lerpDouble(borderRadiusScale, other.borderRadiusScale, t) ?? 1); } } diff --git a/lib/theme/models/slider_tile.dart b/lib/theme/models/slider_tile.dart index 251581e..11b4199 100644 --- a/lib/theme/models/slider_tile.dart +++ b/lib/theme/models/slider_tile.dart @@ -64,13 +64,13 @@ class SliderTile extends StatelessWidget { final theme = Theme.of(context); final scale = theme.extension()!; final tileColor = scale.scale(!disabled ? tileScale : ScaleKind.gray); - final scalecfg = theme.extension()!; + final scaleConfig = theme.extension()!; final borderColor = selected ? tileColor.hoverBorder : tileColor.border; - final backgroundColor = scalecfg.useVisualIndicators && !selected + final backgroundColor = scaleConfig.useVisualIndicators && !selected ? tileColor.borderText : borderColor; - final textColor = scalecfg.useVisualIndicators && !selected + final textColor = scaleConfig.useVisualIndicators && !selected ? borderColor : tileColor.borderText; @@ -79,10 +79,11 @@ class SliderTile extends StatelessWidget { decoration: ShapeDecoration( color: backgroundColor, shape: RoundedRectangleBorder( - side: scalecfg.useVisualIndicators + side: scaleConfig.useVisualIndicators ? BorderSide(width: 2, color: borderColor, strokeAlign: 0) : BorderSide.none, - borderRadius: BorderRadius.circular(8), + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale), )), child: Slidable( // Specify a key if the Slidable is dismissible. @@ -95,12 +96,12 @@ class SliderTile extends StatelessWidget { .map( (a) => SlidableAction( onPressed: disabled ? null : a.onPressed, - backgroundColor: scalecfg.useVisualIndicators + backgroundColor: scaleConfig.useVisualIndicators ? (selected ? tileColor.borderText : tileColor.border) : scale.scale(a.actionScale).primary, - foregroundColor: scalecfg.useVisualIndicators + foregroundColor: scaleConfig.useVisualIndicators ? (selected ? tileColor.border : tileColor.borderText) @@ -118,12 +119,12 @@ class SliderTile extends StatelessWidget { .map( (a) => SlidableAction( onPressed: disabled ? null : a.onPressed, - backgroundColor: scalecfg.useVisualIndicators + backgroundColor: scaleConfig.useVisualIndicators ? (selected ? tileColor.borderText : tileColor.border) : scale.scale(a.actionScale).primary, - foregroundColor: scalecfg.useVisualIndicators + foregroundColor: scaleConfig.useVisualIndicators ? (selected ? tileColor.border : tileColor.borderText) @@ -134,7 +135,7 @@ class SliderTile extends StatelessWidget { ) .toList()), child: Padding( - padding: scalecfg.useVisualIndicators + padding: scaleConfig.useVisualIndicators ? EdgeInsets.zero : const EdgeInsets.fromLTRB(0, 2, 0, 2), child: ListTile( diff --git a/lib/theme/models/theme_preference.dart b/lib/theme/models/theme_preference.dart index 334bbba..0accd8f 100644 --- a/lib/theme/models/theme_preference.dart +++ b/lib/theme/models/theme_preference.dart @@ -5,6 +5,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import '../views/widget_helpers.dart'; import 'contrast_generator.dart'; import 'radix_generator.dart'; +import 'scale_scheme.dart'; part 'theme_preference.freezed.dart'; part 'theme_preference.g.dart'; @@ -37,6 +38,7 @@ enum ColorPreference { lime, grim, // Accessible Colors + elite, contrast; factory ColorPreference.fromJson(dynamic j) => @@ -63,7 +65,7 @@ class ThemePreferences with _$ThemePreferences { } extension ThemePreferencesExt on ThemePreferences { - /// Get material 'ThemeData' for existinb + /// Get material 'ThemeData' for existing theme ThemeData themeData() { late final Brightness brightness; switch (brightnessPreference) { @@ -83,8 +85,60 @@ extension ThemePreferencesExt on ThemePreferences { switch (colorPreference) { // Special cases case ColorPreference.contrast: - // xxx do contrastGenerator - themeData = contrastGenerator(brightness); + themeData = contrastGenerator( + brightness: brightness, + scaleConfig: ScaleConfig( + useVisualIndicators: true, + preferBorders: false, + borderRadiusScale: 1), + primaryFront: Colors.black, + primaryBack: Colors.white, + secondaryFront: Colors.black, + secondaryBack: Colors.white, + tertiaryFront: Colors.black, + tertiaryBack: Colors.white, + grayFront: Colors.black, + grayBack: Colors.white, + errorFront: Colors.black, + errorBack: Colors.white, + ); + case ColorPreference.elite: + themeData = brightness == Brightness.light + ? contrastGenerator( + brightness: Brightness.light, + scaleConfig: ScaleConfig( + useVisualIndicators: true, + preferBorders: true, + borderRadiusScale: 0.2), + primaryFront: const Color(0xFF000000), + primaryBack: const Color(0xFF00FF00), + secondaryFront: const Color(0xFF000000), + secondaryBack: const Color(0xFF00FFFF), + tertiaryFront: const Color(0xFF000000), + tertiaryBack: const Color(0xFFFF00FF), + grayFront: const Color(0xFF000000), + grayBack: const Color(0xFFFFFFFF), + errorFront: const Color(0xFFC0C0C0), + errorBack: const Color(0xFF0000FF), + customTextTheme: makeMonoSpaceTextTheme(Brightness.light)) + : contrastGenerator( + brightness: Brightness.dark, + scaleConfig: ScaleConfig( + useVisualIndicators: true, + preferBorders: true, + borderRadiusScale: 0.5), + primaryFront: const Color(0xFF000000), + primaryBack: const Color(0xFF00FF00), + secondaryFront: const Color(0xFF000000), + secondaryBack: const Color(0xFF00FFFF), + tertiaryFront: const Color(0xFF000000), + tertiaryBack: const Color(0xFFFF00FF), + grayFront: const Color(0xFF000000), + grayBack: const Color(0xFFFFFFFF), + errorFront: const Color(0xFF0000FF), + errorBack: const Color(0xFFC0C0C0), + customTextTheme: makeMonoSpaceTextTheme(Brightness.dark), + ); // Generate from Radix case ColorPreference.scarlet: themeData = radixGenerator(brightness, RadixThemeColor.scarlet); diff --git a/lib/theme/views/color_preferences.dart b/lib/theme/views/color_preferences.dart index 4a5219d..228c2fb 100644 --- a/lib/theme/views/color_preferences.dart +++ b/lib/theme/views/color_preferences.dart @@ -22,6 +22,7 @@ List> _getThemeDropdownItems() { ColorPreference.eggplant: translate('themes.eggplant'), ColorPreference.lime: translate('themes.lime'), ColorPreference.grim: translate('themes.grim'), + ColorPreference.elite: translate('themes.elite'), ColorPreference.contrast: translate('themes.contrast') }; diff --git a/lib/theme/views/styled_dialog.dart b/lib/theme/views/styled_dialog.dart index 0fd079c..b48a8fb 100644 --- a/lib/theme/views/styled_dialog.dart +++ b/lib/theme/views/styled_dialog.dart @@ -11,12 +11,14 @@ class StyledDialog extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); final scale = theme.extension()!; + final scaleConfig = theme.extension()!; final textTheme = theme.textTheme; return AlertDialog( elevation: 0, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(16)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(16 * scaleConfig.borderRadiusScale)), ), contentPadding: const EdgeInsets.all(4), backgroundColor: scale.primaryScale.dialogBorder, @@ -31,12 +33,14 @@ class StyledDialog extends StatelessWidget { decoration: ShapeDecoration( color: scale.primaryScale.border, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16))), + borderRadius: BorderRadius.circular( + 16 * scaleConfig.borderRadiusScale))), child: DecoratedBox( decoration: ShapeDecoration( color: scale.primaryScale.appBackground, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12))), + borderRadius: BorderRadius.circular( + 12 * scaleConfig.borderRadiusScale))), child: child.paddingAll(0)))); } diff --git a/lib/theme/views/styled_scaffold.dart b/lib/theme/views/styled_scaffold.dart new file mode 100644 index 0000000..6be898c --- /dev/null +++ b/lib/theme/views/styled_scaffold.dart @@ -0,0 +1,27 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:flutter/material.dart'; + +import '../theme.dart'; + +class StyledScaffold extends StatelessWidget { + const StyledScaffold({required this.appBar, required this.body, super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + + return clipBorder( + clipEnabled: true, + borderEnabled: scaleConfig.useVisualIndicators, + borderRadius: 16 * scaleConfig.borderRadiusScale, + borderColor: scale.primaryScale.border, + child: Scaffold(appBar: appBar, body: body, key: key)) + .paddingAll(32); + } + + //////////////////////////////////////////////////////////////////////////// + final PreferredSizeWidget? appBar; + final Widget? body; +} diff --git a/lib/theme/views/views.dart b/lib/theme/views/views.dart index 6f6d7ac..0bdf87b 100644 --- a/lib/theme/views/views.dart +++ b/lib/theme/views/views.dart @@ -2,4 +2,5 @@ export 'brightness_preferences.dart'; export 'color_preferences.dart'; export 'scanner_error_widget.dart'; export 'styled_dialog.dart'; +export 'styled_scaffold.dart'; export 'widget_helpers.dart'; diff --git a/lib/theme/views/widget_helpers.dart b/lib/theme/views/widget_helpers.dart index e3dfd94..3577d66 100644 --- a/lib/theme/views/widget_helpers.dart +++ b/lib/theme/views/widget_helpers.dart @@ -132,7 +132,7 @@ void showErrorToast(BuildContext context, String message) { contentPadding: const EdgeInsets.all(16), primaryColor: scale.errorScale.elementBackground, secondaryColor: scale.errorScale.calloutBackground, - borderRadius: 16, + borderRadius: 16 * scaleConfig.borderRadiusScale, toastDuration: const Duration(seconds: 4), animationDuration: const Duration(milliseconds: 1000), displayBorder: scaleConfig.useVisualIndicators, @@ -152,7 +152,7 @@ void showInfoToast(BuildContext context, String message) { contentPadding: const EdgeInsets.all(16), primaryColor: scale.tertiaryScale.elementBackground, secondaryColor: scale.tertiaryScale.calloutBackground, - borderRadius: 16, + borderRadius: 16 * scaleConfig.borderRadiusScale, toastDuration: const Duration(seconds: 2), animationDuration: const Duration(milliseconds: 500), displayBorder: scaleConfig.useVisualIndicators, @@ -170,13 +170,15 @@ Widget styledTitleContainer({ }) { final theme = Theme.of(context); final scale = theme.extension()!; + final scaleConfig = theme.extension()!; final textTheme = theme.textTheme; return DecoratedBox( decoration: ShapeDecoration( color: borderColor ?? scale.primaryScale.border, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: + BorderRadius.circular(16 * scaleConfig.borderRadiusScale), )), child: Column(children: [ Text( @@ -189,7 +191,8 @@ Widget styledTitleContainer({ color: backgroundColor ?? scale.primaryScale.subtleBackground, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular( + 16 * scaleConfig.borderRadiusScale), )), child: child) .paddingAll(4) @@ -207,15 +210,17 @@ Widget styledBottomSheet({ }) { final theme = Theme.of(context); final scale = theme.extension()!; + final scaleConfig = theme.extension()!; final textTheme = theme.textTheme; return DecoratedBox( decoration: ShapeDecoration( color: borderColor ?? scale.primaryScale.dialogBorder, - shape: const RoundedRectangleBorder( + shape: RoundedRectangleBorder( borderRadius: BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16)))), + topLeft: Radius.circular(16 * scaleConfig.borderRadiusScale), + topRight: + Radius.circular(16 * scaleConfig.borderRadiusScale)))), child: Column(mainAxisSize: MainAxisSize.min, children: [ Text( title, @@ -226,10 +231,12 @@ Widget styledBottomSheet({ decoration: ShapeDecoration( color: backgroundColor ?? scale.primaryScale.subtleBackground, - shape: const RoundedRectangleBorder( + shape: RoundedRectangleBorder( borderRadius: BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16)))), + topLeft: Radius.circular( + 16 * scaleConfig.borderRadiusScale), + topRight: Radius.circular( + 16 * scaleConfig.borderRadiusScale)))), child: child) .paddingLTRB(4, 4, 4, 0) ])); @@ -261,3 +268,25 @@ const grayColorFilter = ColorFilter.matrix([ 1, 0, ]); + +Widget clipBorder({ + required bool clipEnabled, + required bool borderEnabled, + required double borderRadius, + required Color borderColor, + required Widget child, +}) => + ClipRRect( + borderRadius: clipEnabled + ? BorderRadius.circular(borderRadius) + : BorderRadius.zero, + child: DecoratedBox( + decoration: BoxDecoration(boxShadow: [ + if (borderEnabled) BoxShadow(color: borderColor, spreadRadius: 2) + ]), + child: ClipRRect( + borderRadius: clipEnabled + ? BorderRadius.circular(borderRadius) + : BorderRadius.zero, + child: child, + )).paddingAll(clipEnabled && borderEnabled ? 2 : 0)); diff --git a/lib/tools/enter_pin.dart b/lib/tools/enter_pin.dart index d0a21ec..5d476fb 100644 --- a/lib/tools/enter_pin.dart +++ b/lib/tools/enter_pin.dart @@ -51,6 +51,7 @@ class _EnterPinDialogState extends State { Widget build(BuildContext context) { final theme = Theme.of(context); final scale = theme.extension()!; + final scaleConfig = theme.extension()!; final focusedBorderColor = scale.primaryScale.hoverBorder; final fillColor = scale.primaryScale.elementBackground; final borderColor = scale.primaryScale.border; @@ -61,7 +62,7 @@ class _EnterPinDialogState extends State { textStyle: TextStyle(fontSize: 22, color: scale.primaryScale.appText), decoration: BoxDecoration( color: fillColor, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(8 * scaleConfig.borderRadiusScale), border: Border.all(color: borderColor), ), ); diff --git a/lib/veilid_processor/views/developer.dart b/lib/veilid_processor/views/developer.dart index bfcaa35..85914ab 100644 --- a/lib/veilid_processor/views/developer.dart +++ b/lib/veilid_processor/views/developer.dart @@ -140,6 +140,7 @@ class _DeveloperPageState extends State { final theme = Theme.of(context); final textTheme = theme.textTheme; final scale = theme.extension()!; + final scaleConfig = theme.extension()!; // WidgetsBinding.instance.addPostFrameCallback((_) { // if (!_isScrolling && _wantsBottom) { @@ -225,12 +226,22 @@ class _DeveloperPageState extends State { .copyWith(color: scale.primaryScale.primaryText), padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), openBoxDecoration: BoxDecoration( - color: scale.primaryScale.border, - borderRadius: BorderRadius.circular(8), + //color: scale.primaryScale.border, + border: Border.all( + color: scaleConfig.useVisualIndicators + ? scale.primaryScale.hoverBorder + : scale.primaryScale.borderText), + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale), ), boxDecoration: BoxDecoration( - color: scale.primaryScale.hoverBorder, - borderRadius: BorderRadius.circular(8), + //color: scale.primaryScale.hoverBorder, + border: Border.all( + color: scaleConfig.useVisualIndicators + ? scale.primaryScale.hoverBorder + : scale.primaryScale.borderText), + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale), ), ), dropdownOptions: DropdownOptions( @@ -239,7 +250,8 @@ class _DeveloperPageState extends State { duration: 150.ms, color: scale.primaryScale.elementBackground, borderSide: BorderSide(color: scale.primaryScale.border), - borderRadius: BorderRadius.circular(8), + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale), padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), ), dropdownTriangleOptions: const DropdownTriangleOptions( @@ -283,10 +295,12 @@ class _DeveloperPageState extends State { filled: true, contentPadding: const EdgeInsets.fromLTRB(8, 2, 8, 2), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular( + 8 * scaleConfig.borderRadiusScale), borderSide: BorderSide.none), border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular( + 8 * scaleConfig.borderRadiusScale), ), fillColor: scale.primaryScale.subtleBackground, hintText: translate('developer.command'), diff --git a/pubspec.lock b/pubspec.lock index 2e743b1..736e2bc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.4.1" + animated_bottom_navigation_bar: + dependency: "direct main" + description: + name: animated_bottom_navigation_bar + sha256: "2b04a2ae4b0742669e60ddf309467d6a354cefd2d0cd20f4737b1efaf9834cda" + url: "https://pub.dev" + source: hosted + version: "1.3.3" animated_switcher_transitions: dependency: "direct main" description: @@ -481,11 +489,9 @@ packages: flutter_chat_ui: dependency: "direct main" description: - path: "." - ref: main - resolved-ref: "0d8ac2fcafe24eba1adff9290a9ccd41f7718480" - url: "https://gitlab.com/veilid/flutter-chat-ui.git" - source: git + path: "../flutter_chat_ui" + relative: true + source: path version: "1.6.14" flutter_form_builder: dependency: "direct main" @@ -1351,14 +1357,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" - stylish_bottom_bar: - dependency: "direct main" - description: - name: stylish_bottom_bar - sha256: ca72557a5bd8f44caae9017eb3a73002e9189d7a9d2fac598fa55be13724f32b - url: "https://pub.dev" - source: hosted - version: "1.1.0" synchronized: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a1f097b..c5c6a3f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,6 +8,7 @@ environment: flutter: '>=3.22.1' dependencies: + animated_bottom_navigation_bar: ^1.3.3 animated_switcher_transitions: ^1.0.0 animated_theme_switcher: ^2.0.10 ansicolor: ^2.0.2 @@ -83,7 +84,6 @@ dependencies: split_view: ^3.2.1 stack_trace: ^1.11.1 stream_transform: ^2.1.0 - stylish_bottom_bar: ^1.1.0 transitioned_indexed_stack: ^1.0.2 uuid: ^4.4.0 veilid: @@ -95,13 +95,13 @@ dependencies: xterm: ^4.0.0 zxing2: ^0.2.3 -# dependency_overrides: +dependency_overrides: # async_tools: # path: ../dart_async_tools # bloc_advanced_tools: # path: ../bloc_advanced_tools -# flutter_chat_ui: -# path: ../flutter_chat_ui + flutter_chat_ui: + path: ../flutter_chat_ui dev_dependencies: build_runner: ^2.4.11 From 71f4d37efa9e26670806ca36a4c8d63a4a28c02e Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 6 Jul 2024 23:01:24 -0400 Subject: [PATCH 157/270] recovery key work --- assets/i18n/en.json | 11 +- .../views/show_recovery_key_page.dart | 168 ++++++++++++++++-- 2 files changed, 163 insertions(+), 16 deletions(-) diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 7c47707..8d64c5b 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -54,11 +54,16 @@ }, "show_recovery_key_page": { "titlebar": "Save Recovery Key", - "instructions": "You must save this recovery key somewhere safe. This key is the ONLY way to recover your VeilidChat account in the event of a forgotton password or a lost, stolen, or compromised device.", + "recovery_key": "Recovery Key", + "instructions": "Your recovery key is important!", + "instructions_details": "This key is the ONLY way to recover your VeilidChat account in the event of a forgotton password or a lost, stolen, or compromised device.", "instructions_options": "Here are some options for your recovery key:", "instructions_print": "Print the recovery key and keep it somewhere safe", - "instructions_write": "View the recovery key and write it down on paper", - "instructions_send": "Send the recovery key to another app to save it" + "instructions_view": "View the recovery key and write it down on paper", + "instructions_share": "Share the recovery key to another app to save it", + "print": "Print", + "view": "View", + "share": "Share" }, "button": { "ok": "Ok", diff --git a/lib/account_manager/views/show_recovery_key_page.dart b/lib/account_manager/views/show_recovery_key_page.dart index e22e0e1..6230a85 100644 --- a/lib/account_manager/views/show_recovery_key_page.dart +++ b/lib/account_manager/views/show_recovery_key_page.dart @@ -1,3 +1,6 @@ +import 'dart:math'; + +import 'package:async_tools/async_tools.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter_translate/flutter_translate.dart'; @@ -5,6 +8,7 @@ import 'package:go_router/go_router.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'; @@ -29,12 +33,89 @@ class ShowRecoveryKeyPageState extends State { }); } + Widget _recoveryKeyWidget(SecretKey _secretKey) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + final scaleConfig = theme.extension()!; + + final cardsize = + min(MediaQuery.of(context).size.shortestSide - 48.0, 400); + + final phonoString = prettyPhonoString( + encodePhono(_secretKey.decode()), + wordsPerLine: 2, + ); + return Dialog( + shape: RoundedRectangleBorder( + side: const BorderSide(width: 2), + borderRadius: + BorderRadius.circular(16 * scaleConfig.borderRadiusScale)), + backgroundColor: Colors.white, + child: ConstrainedBox( + constraints: BoxConstraints( + minWidth: cardsize, + maxWidth: cardsize, + minHeight: cardsize, + maxHeight: cardsize), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Text( + style: textTheme.headlineSmall!.copyWith( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + translate('show_recovery_key_page.recovery_key')) + .paddingAll(32), + Text( + style: textTheme.headlineSmall!.copyWith( + color: Colors.black, fontFamily: 'Source Code Pro'), + phonoString) + ]))); + } + + Widget _optionBox( + {required String instructions, + required Icon buttonIcon, + required String buttonText, + required void Function() onClick}) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + + return Container( + constraints: const BoxConstraints(maxWidth: 400), + decoration: BoxDecoration( + color: scale.primaryScale.subtleBackground, + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale), + border: Border.all(color: scale.primaryScale.border)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + style: theme.textTheme.labelMedium! + .copyWith(color: scale.primaryScale.appText), + softWrap: true, + textAlign: TextAlign.center, + instructions), + ElevatedButton( + onPressed: onClick, + child: Row(mainAxisSize: MainAxisSize.min, children: [ + buttonIcon.paddingLTRB(0, 8, 12, 8), + Text(textAlign: TextAlign.center, buttonText) + ])).paddingLTRB(0, 12, 0, 0).toCenter() + ]).paddingAll(12)) + .paddingLTRB(24, 0, 24, 12); + } + @override // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { final secretKey = widget._secretKey; + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; - return Scaffold( + return StyledScaffold( // resizeToAvoidBottomInset: false, appBar: DefaultAppBar( title: Text(translate('show_recovery_key_page.titlebar')), @@ -47,17 +128,78 @@ class ShowRecoveryKeyPageState extends State { await GoRouterHelper(context).push('/settings'); }) ]), - body: Column(children: [ - Text('ASS: $secretKey'), - ElevatedButton( - onPressed: () { - if (context.mounted) { - Navigator.canPop(context) - ? GoRouterHelper(context).pop() - : GoRouterHelper(context).go('/'); - } - }, - child: Text(translate('button.finish'))) - ]).paddingSymmetric(horizontal: 24, vertical: 8)); + body: SingleChildScrollView( + child: Column(children: [ + Text( + style: theme.textTheme.headlineSmall, + textAlign: TextAlign.center, + translate('show_recovery_key_page.instructions')) + .paddingAll(24), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: Text( + softWrap: true, + textAlign: TextAlign.center, + translate('show_recovery_key_page.instructions_details'))) + .toCenter() + .paddingLTRB(24, 0, 24, 24), + Text( + textAlign: TextAlign.center, + translate('show_recovery_key_page.instructions_options')) + .paddingLTRB(12, 0, 12, 12), + _optionBox( + instructions: + translate('show_recovery_key_page.instructions_print'), + buttonIcon: const Icon(Icons.print), + buttonText: translate('show_recovery_key_page.print'), + onClick: () { + // + setState(() { + _codeHandled = true; + }); + }), + _optionBox( + instructions: + translate('show_recovery_key_page.instructions_view'), + buttonIcon: const Icon(Icons.edit_document), + buttonText: translate('show_recovery_key_page.view'), + onClick: () { + // + singleFuture(this, () async { + await showDialog( + context: context, + builder: (context) => _recoveryKeyWidget(secretKey)); + }); + + setState(() { + _codeHandled = true; + }); + }), + _optionBox( + instructions: + translate('show_recovery_key_page.instructions_share'), + buttonIcon: const Icon(Icons.ios_share), + buttonText: translate('show_recovery_key_page.share'), + onClick: () { + // + setState(() { + _codeHandled = true; + }); + }), + Offstage( + offstage: !_codeHandled, + child: ElevatedButton( + onPressed: () { + if (context.mounted) { + Navigator.canPop(context) + ? GoRouterHelper(context).pop() + : GoRouterHelper(context).go('/'); + } + }, + child: Text(translate('button.finish')).paddingAll(8)) + .paddingAll(12)) + ]))); } + + bool _codeHandled = false; } From 216aef8173783f6aefb47313ed240692db7b980a Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 8 Jul 2024 21:29:52 -0400 Subject: [PATCH 158/270] layout fixes --- assets/i18n/en.json | 2 +- assets/js/pdf/3.2.146/pdf.min.js | 22 ++ ios/Podfile.lock | 12 + .../repository/account_repository.dart | 5 +- .../views/edit_account_page.dart | 48 ++-- .../views/new_account_page.dart | 12 +- .../views/profile_edit_form.dart | 2 +- lib/account_manager/views/profile_widget.dart | 24 +- .../views/show_recovery_key_page.dart | 224 +++++++++++------- lib/app.dart | 4 +- lib/chat/views/no_conversation_widget.dart | 8 +- .../views/contact_invitation_display.dart | 1 - .../views/contact_invitation_list_widget.dart | 3 +- .../views/create_invitation_dialog.dart | 1 - .../views/invitation_dialog.dart | 11 +- .../views/paste_invitation_dialog.dart | 1 - .../views/scan_invitation_dialog.dart | 1 - lib/contacts/views/contact_list_widget.dart | 79 +++--- .../views/empty_contact_list_widget.dart | 2 + lib/layout/home/drawer_menu/drawer_menu.dart | 95 ++++---- .../home/drawer_menu/menu_item_widget.dart | 2 +- .../home_account_ready_chat.dart | 1 + .../home_account_ready_main.dart | 11 +- .../main_pager/account_page.dart | 5 +- .../bottom_sheet_action_button.dart | 1 + .../main_pager/main_pager.dart | 2 +- lib/layout/home/home_screen.dart | 4 + lib/router/cubit/router_cubit.dart | 12 +- lib/theme/models/chat_theme.dart | 6 +- lib/theme/models/slider_tile.dart | 9 +- .../views}/enter_password.dart | 2 +- lib/{tools => theme/views}/enter_pin.dart | 2 +- lib/theme/views/option_box.dart | 54 +++++ lib/{tools => theme/views}/pop_control.dart | 0 lib/theme/views/recovery_key_widget.dart | 0 lib/{tools => theme/views}/responsive.dart | 2 +- lib/theme/views/styled_scaffold.dart | 16 +- lib/theme/views/views.dart | 6 + lib/theme/views/widget_helpers.dart | 4 +- lib/tools/loggy.dart | 2 +- lib/tools/tools.dart | 5 - lib/tools/window_control.dart | 4 +- lib/veilid_processor/views/developer.dart | 106 +++++---- linux/flutter/generated_plugin_registrant.cc | 8 + linux/flutter/generated_plugins.cmake | 2 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 + macos/Podfile.lock | 12 + macos/Runner/DebugProfile.entitlements | 2 + macos/Runner/Release.entitlements | 2 + .../identity_support/identity_instance.dart | 4 +- .../writable_super_identity.dart | 10 +- pubspec.lock | 114 +++++++-- pubspec.yaml | 12 +- web/index.html | 5 +- .../flutter/generated_plugin_registrant.cc | 6 + windows/flutter/generated_plugins.cmake | 2 + 56 files changed, 654 insertions(+), 342 deletions(-) create mode 100644 assets/js/pdf/3.2.146/pdf.min.js rename lib/{tools => theme/views}/enter_password.dart (99%) rename lib/{tools => theme/views}/enter_pin.dart (99%) create mode 100644 lib/theme/views/option_box.dart rename lib/{tools => theme/views}/pop_control.dart (100%) create mode 100644 lib/theme/views/recovery_key_widget.dart rename lib/{tools => theme/views}/responsive.dart (96%) diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 8d64c5b..577d6bb 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -59,7 +59,7 @@ "instructions_details": "This key is the ONLY way to recover your VeilidChat account in the event of a forgotton password or a lost, stolen, or compromised device.", "instructions_options": "Here are some options for your recovery key:", "instructions_print": "Print the recovery key and keep it somewhere safe", - "instructions_view": "View the recovery key and write it down on paper", + "instructions_view": "View the recovery key and take a screenshot", "instructions_share": "Share the recovery key to another app to save it", "print": "Print", "view": "View", diff --git a/assets/js/pdf/3.2.146/pdf.min.js b/assets/js/pdf/3.2.146/pdf.min.js new file mode 100644 index 0000000..4d43020 --- /dev/null +++ b/assets/js/pdf/3.2.146/pdf.min.js @@ -0,0 +1,22 @@ +/** + * @licstart The following is the entire license notice for the + * JavaScript code in this page + * + * Copyright 2022 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @licend The above is the entire license notice for the + * JavaScript code in this page + */ +!function webpackUniversalModuleDefinition(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define("pdfjs-dist/build/pdf",[],e):"object"==typeof exports?exports["pdfjs-dist/build/pdf"]=e():t["pdfjs-dist/build/pdf"]=t.pdfjsLib=e()}(globalThis,(()=>(()=>{"use strict";var __webpack_modules__=[,(t,e)=>{Object.defineProperty(e,"__esModule",{value:!0});e.VerbosityLevel=e.Util=e.UnknownErrorException=e.UnexpectedResponseException=e.UNSUPPORTED_FEATURES=e.TextRenderingMode=e.StreamType=e.RenderingIntentFlag=e.PermissionFlag=e.PasswordResponses=e.PasswordException=e.PageActionEventType=e.OPS=e.MissingPDFException=e.LINE_FACTOR=e.LINE_DESCENT_FACTOR=e.InvalidPDFException=e.ImageKind=e.IDENTITY_MATRIX=e.FormatError=e.FontType=e.FeatureTest=e.FONT_IDENTITY_MATRIX=e.DocumentActionEventType=e.CMapCompressionType=e.BaseException=e.BASELINE_FACTOR=e.AnnotationType=e.AnnotationStateModelType=e.AnnotationReviewState=e.AnnotationReplyType=e.AnnotationMode=e.AnnotationMarkedState=e.AnnotationFlag=e.AnnotationFieldFlag=e.AnnotationEditorType=e.AnnotationEditorPrefix=e.AnnotationEditorParamsType=e.AnnotationBorderStyleType=e.AnnotationActionEventType=e.AbortException=void 0;e.arrayByteLength=arrayByteLength;e.arraysToBytes=function arraysToBytes(t){const e=t.length;if(1===e&&t[0]instanceof Uint8Array)return t[0];let s=0;for(let n=0;ne});t.promise=new Promise((function(s,n){t.resolve=function(t){e=!0;s(t)};t.reject=function(t){e=!0;n(t)}}));return t};e.createValidAbsoluteUrl=function createValidAbsoluteUrl(t,e=null,s=null){if(!t)return null;try{if(s&&"string"==typeof t){if(s.addDefaultProtocol&&t.startsWith("www.")){const e=t.match(/\./g);e&&e.length>=2&&(t=`http://${t}`)}if(s.tryConvertEncoding)try{t=stringToUTF8String(t)}catch(t){}}const n=e?new URL(t,e):new URL(t);if(function _isValidProtocol(t){if(!t)return!1;switch(t.protocol){case"http:":case"https:":case"ftp:":case"mailto:":case"tel:":return!0;default:return!1}}(n))return n}catch(t){}return null};e.getModificationDate=function getModificationDate(t=new Date){return[t.getUTCFullYear().toString(),(t.getUTCMonth()+1).toString().padStart(2,"0"),t.getUTCDate().toString().padStart(2,"0"),t.getUTCHours().toString().padStart(2,"0"),t.getUTCMinutes().toString().padStart(2,"0"),t.getUTCSeconds().toString().padStart(2,"0")].join("")};e.getVerbosityLevel=function getVerbosityLevel(){return n};e.info=function info(t){n>=s.INFOS&&console.log(`Info: ${t}`)};e.isArrayBuffer=function isArrayBuffer(t){return"object"==typeof t&&null!==t&&void 0!==t.byteLength};e.isArrayEqual=function isArrayEqual(t,e){if(t.length!==e.length)return!1;for(let s=0,n=t.length;s>24&255,t>>16&255,t>>8&255,255&t)};e.stringToBytes=stringToBytes;e.stringToPDFString=function stringToPDFString(t){if(t[0]>="ï"){let e;"þ"===t[0]&&"ÿ"===t[1]?e="utf-16be":"ÿ"===t[0]&&"þ"===t[1]?e="utf-16le":"ï"===t[0]&&"»"===t[1]&&"¿"===t[2]&&(e="utf-8");if(e)try{const s=new TextDecoder(e,{fatal:!0}),n=stringToBytes(t);return s.decode(n)}catch(t){warn(`stringToPDFString: "${t}".`)}}const e=[];for(let s=0,n=t.length;s=s.WARNINGS&&console.log(`Warning: ${t}`)}function unreachable(t){throw new Error(t)}function shadow(t,e,s,n=!1){Object.defineProperty(t,e,{value:s,enumerable:!n,configurable:!0,writable:!1});return s}const i=function BaseExceptionClosure(){function BaseException(t,e){this.constructor===BaseException&&unreachable("Cannot initialize BaseException.");this.message=t;this.name=e}BaseException.prototype=new Error;BaseException.constructor=BaseException;return BaseException}();e.BaseException=i;e.PasswordException=class PasswordException extends i{constructor(t,e){super(t,"PasswordException");this.code=e}};e.UnknownErrorException=class UnknownErrorException extends i{constructor(t,e){super(t,"UnknownErrorException");this.details=e}};e.InvalidPDFException=class InvalidPDFException extends i{constructor(t){super(t,"InvalidPDFException")}};e.MissingPDFException=class MissingPDFException extends i{constructor(t){super(t,"MissingPDFException")}};e.UnexpectedResponseException=class UnexpectedResponseException extends i{constructor(t,e){super(t,"UnexpectedResponseException");this.status=e}};e.FormatError=class FormatError extends i{constructor(t){super(t,"FormatError")}};e.AbortException=class AbortException extends i{constructor(t){super(t,"AbortException")}};function stringToBytes(t){"string"!=typeof t&&unreachable("Invalid argument for stringToBytes");const e=t.length,s=new Uint8Array(e);for(let n=0;nt.toString(16).padStart(2,"0")));class Util{static makeHexColor(t,e,s){return`#${a[t]}${a[e]}${a[s]}`}static scaleMinMax(t,e){let s;if(t[0]){if(t[0]<0){s=e[0];e[0]=e[1];e[1]=s}e[0]*=t[0];e[1]*=t[0];if(t[3]<0){s=e[2];e[2]=e[3];e[3]=s}e[2]*=t[3];e[3]*=t[3]}else{s=e[0];e[0]=e[2];e[2]=s;s=e[1];e[1]=e[3];e[3]=s;if(t[1]<0){s=e[2];e[2]=e[3];e[3]=s}e[2]*=t[1];e[3]*=t[1];if(t[2]<0){s=e[0];e[0]=e[1];e[1]=s}e[0]*=t[2];e[1]*=t[2]}e[0]+=t[4];e[1]+=t[4];e[2]+=t[5];e[3]+=t[5]}static transform(t,e){return[t[0]*e[0]+t[2]*e[1],t[1]*e[0]+t[3]*e[1],t[0]*e[2]+t[2]*e[3],t[1]*e[2]+t[3]*e[3],t[0]*e[4]+t[2]*e[5]+t[4],t[1]*e[4]+t[3]*e[5]+t[5]]}static applyTransform(t,e){return[t[0]*e[0]+t[1]*e[2]+e[4],t[0]*e[1]+t[1]*e[3]+e[5]]}static applyInverseTransform(t,e){const s=e[0]*e[3]-e[1]*e[2];return[(t[0]*e[3]-t[1]*e[2]+e[2]*e[5]-e[4]*e[3])/s,(-t[0]*e[1]+t[1]*e[0]+e[4]*e[1]-e[5]*e[0])/s]}static getAxialAlignedBoundingBox(t,e){const s=Util.applyTransform(t,e),n=Util.applyTransform(t.slice(2,4),e),i=Util.applyTransform([t[0],t[3]],e),a=Util.applyTransform([t[2],t[1]],e);return[Math.min(s[0],n[0],i[0],a[0]),Math.min(s[1],n[1],i[1],a[1]),Math.max(s[0],n[0],i[0],a[0]),Math.max(s[1],n[1],i[1],a[1])]}static inverseTransform(t){const e=t[0]*t[3]-t[1]*t[2];return[t[3]/e,-t[1]/e,-t[2]/e,t[0]/e,(t[2]*t[5]-t[4]*t[3])/e,(t[4]*t[1]-t[5]*t[0])/e]}static singularValueDecompose2dScale(t){const e=[t[0],t[2],t[1],t[3]],s=t[0]*e[0]+t[1]*e[2],n=t[0]*e[1]+t[1]*e[3],i=t[2]*e[0]+t[3]*e[2],a=t[2]*e[1]+t[3]*e[3],r=(s+a)/2,o=Math.sqrt((s+a)**2-4*(s*a-i*n))/2,l=r+o||1,c=r-o||1;return[Math.sqrt(l),Math.sqrt(c)]}static normalizeRect(t){const e=t.slice(0);if(t[0]>t[2]){e[0]=t[2];e[2]=t[0]}if(t[1]>t[3]){e[1]=t[3];e[3]=t[1]}return e}static intersect(t,e){const s=Math.max(Math.min(t[0],t[2]),Math.min(e[0],e[2])),n=Math.min(Math.max(t[0],t[2]),Math.max(e[0],e[2]));if(s>n)return null;const i=Math.max(Math.min(t[1],t[3]),Math.min(e[1],e[3])),a=Math.min(Math.max(t[1],t[3]),Math.max(e[1],e[3]));return i>a?null:[s,i,n,a]}static bezierBoundingBox(t,e,s,n,i,a,r,o){const l=[],c=[[],[]];let h,d,u,p,g,m,f,b;for(let c=0;c<2;++c){if(0===c){d=6*t-12*s+6*i;h=-3*t+9*s-9*i+3*r;u=3*s-3*t}else{d=6*e-12*n+6*a;h=-3*e+9*n-9*a+3*o;u=3*n-3*e}if(Math.abs(h)<1e-12){if(Math.abs(d)<1e-12)continue;p=-u/d;0{Object.defineProperty(exports,"__esModule",{value:!0});exports.build=exports.RenderTask=exports.PDFWorkerUtil=exports.PDFWorker=exports.PDFPageProxy=exports.PDFDocumentProxy=exports.PDFDocumentLoadingTask=exports.PDFDataRangeTransport=exports.LoopbackPort=exports.DefaultStandardFontDataFactory=exports.DefaultCanvasFactory=exports.DefaultCMapReaderFactory=void 0;exports.getDocument=getDocument;exports.setPDFNetworkStreamFactory=setPDFNetworkStreamFactory;exports.version=void 0;var _util=__w_pdfjs_require__(1),_annotation_storage=__w_pdfjs_require__(3),_display_utils=__w_pdfjs_require__(6),_font_loader=__w_pdfjs_require__(9),_canvas=__w_pdfjs_require__(11),_worker_options=__w_pdfjs_require__(14),_is_node=__w_pdfjs_require__(10),_message_handler=__w_pdfjs_require__(15),_metadata=__w_pdfjs_require__(16),_optional_content_config=__w_pdfjs_require__(17),_transport_stream=__w_pdfjs_require__(18),_xfa_text=__w_pdfjs_require__(19);const DEFAULT_RANGE_CHUNK_SIZE=65536,RENDERING_CANCELLED_TIMEOUT=100;let DefaultCanvasFactory=_display_utils.DOMCanvasFactory;exports.DefaultCanvasFactory=DefaultCanvasFactory;let DefaultCMapReaderFactory=_display_utils.DOMCMapReaderFactory;exports.DefaultCMapReaderFactory=DefaultCMapReaderFactory;let DefaultStandardFontDataFactory=_display_utils.DOMStandardFontDataFactory,createPDFNetworkStream;exports.DefaultStandardFontDataFactory=DefaultStandardFontDataFactory;if(_is_node.isNodeJS){const{NodeCanvasFactory:t,NodeCMapReaderFactory:e,NodeStandardFontDataFactory:s}=__w_pdfjs_require__(20);exports.DefaultCanvasFactory=DefaultCanvasFactory=t;exports.DefaultCMapReaderFactory=DefaultCMapReaderFactory=e;exports.DefaultStandardFontDataFactory=DefaultStandardFontDataFactory=s}function setPDFNetworkStreamFactory(t){createPDFNetworkStream=t}function getDocument(t){const e=new PDFDocumentLoadingTask;let s;if("string"==typeof t||t instanceof URL)s={url:t};else if((0,_util.isArrayBuffer)(t))s={data:t};else if(t instanceof PDFDataRangeTransport)s={range:t};else{if("object"!=typeof t)throw new Error("Invalid parameter in getDocument, need either string, URL, TypedArray, or parameter object.");if(!t.url&&!t.data&&!t.range)throw new Error("Invalid parameter object: need either .data, .range or .url");s=t}const n=Object.create(null);let i=null,a=null;for(const t in s){const e=s[t];switch(t){case"url":if("undefined"!=typeof window)try{n[t]=new URL(e,window.location).href;continue}catch(t){(0,_util.warn)(`Cannot create valid URL: "${t}".`)}else if("string"==typeof e||e instanceof URL){n[t]=e.toString();continue}throw new Error("Invalid PDF url data: either string or URL-object is expected in the url property.");case"range":i=e;continue;case"worker":a=e;continue;case"data":if(_is_node.isNodeJS&&"undefined"!=typeof Buffer&&e instanceof Buffer)n[t]=new Uint8Array(e);else{if(e instanceof Uint8Array)break;if("string"==typeof e)n[t]=(0,_util.stringToBytes)(e);else if("object"!=typeof e||null===e||isNaN(e.length)){if(!(0,_util.isArrayBuffer)(e))throw new Error("Invalid PDF binary data: either TypedArray, string, or array-like object is expected in the data property.");n[t]=new Uint8Array(e)}else n[t]=new Uint8Array(e)}continue}n[t]=e}n.CMapReaderFactory=n.CMapReaderFactory||DefaultCMapReaderFactory;n.StandardFontDataFactory=n.StandardFontDataFactory||DefaultStandardFontDataFactory;n.ignoreErrors=!0!==n.stopAtErrors;n.fontExtraProperties=!0===n.fontExtraProperties;n.pdfBug=!0===n.pdfBug;n.enableXfa=!0===n.enableXfa;(!Number.isInteger(n.rangeChunkSize)||n.rangeChunkSize<1)&&(n.rangeChunkSize=DEFAULT_RANGE_CHUNK_SIZE);("string"!=typeof n.docBaseUrl||(0,_display_utils.isDataScheme)(n.docBaseUrl))&&(n.docBaseUrl=null);(!Number.isInteger(n.maxImageSize)||n.maxImageSize<-1)&&(n.maxImageSize=-1);"string"!=typeof n.cMapUrl&&(n.cMapUrl=null);"string"!=typeof n.standardFontDataUrl&&(n.standardFontDataUrl=null);"boolean"!=typeof n.useWorkerFetch&&(n.useWorkerFetch=n.CMapReaderFactory===_display_utils.DOMCMapReaderFactory&&n.StandardFontDataFactory===_display_utils.DOMStandardFontDataFactory);"boolean"!=typeof n.isEvalSupported&&(n.isEvalSupported=!0);"boolean"!=typeof n.isOffscreenCanvasSupported&&(n.isOffscreenCanvasSupported=!_is_node.isNodeJS);"boolean"!=typeof n.disableFontFace&&(n.disableFontFace=_is_node.isNodeJS);"boolean"!=typeof n.useSystemFonts&&(n.useSystemFonts=!_is_node.isNodeJS&&!n.disableFontFace);"object"==typeof n.ownerDocument&&null!==n.ownerDocument||(n.ownerDocument=globalThis.document);"boolean"!=typeof n.disableRange&&(n.disableRange=!1);"boolean"!=typeof n.disableStream&&(n.disableStream=!1);"boolean"!=typeof n.disableAutoFetch&&(n.disableAutoFetch=!1);(0,_util.setVerbosityLevel)(n.verbosity);if(!a){const t={verbosity:n.verbosity,port:_worker_options.GlobalWorkerOptions.workerPort};a=t.port?PDFWorker.fromPort(t):new PDFWorker(t);e._worker=a}const r=e.docId;a.promise.then((function(){if(e.destroyed)throw new Error("Loading aborted");const t=_fetchDocument(a,n,i,r),s=new Promise((function(t){let e;i?e=new _transport_stream.PDFDataTransportStream({length:n.length,initialData:n.initialData,progressiveDone:n.progressiveDone,contentDispositionFilename:n.contentDispositionFilename,disableRange:n.disableRange,disableStream:n.disableStream},i):n.data||(e=createPDFNetworkStream({url:n.url,length:n.length,httpHeaders:n.httpHeaders,withCredentials:n.withCredentials,rangeChunkSize:n.rangeChunkSize,disableRange:n.disableRange,disableStream:n.disableStream}));t(e)}));return Promise.all([t,s]).then((function([t,s]){if(e.destroyed)throw new Error("Loading aborted");const i=new _message_handler.MessageHandler(r,t,a.port),o=new WorkerTransport(i,e,s,n);e._transport=o;i.send("Ready",null)}))})).catch(e._capability.reject);return e}async function _fetchDocument(t,e,s,n){if(t.destroyed)throw new Error("Worker was destroyed");if(s){e.length=s.length;e.initialData=s.initialData;e.progressiveDone=s.progressiveDone;e.contentDispositionFilename=s.contentDispositionFilename}const i=await t.messageHandler.sendWithPromise("GetDocRequest",{docId:n,apiVersion:"3.2.146",data:e.data,password:e.password,disableAutoFetch:e.disableAutoFetch,rangeChunkSize:e.rangeChunkSize,length:e.length,docBaseUrl:e.docBaseUrl,enableXfa:e.enableXfa,evaluatorOptions:{maxImageSize:e.maxImageSize,disableFontFace:e.disableFontFace,ignoreErrors:e.ignoreErrors,isEvalSupported:e.isEvalSupported,isOffscreenCanvasSupported:e.isOffscreenCanvasSupported,fontExtraProperties:e.fontExtraProperties,useSystemFonts:e.useSystemFonts,cMapUrl:e.useWorkerFetch?e.cMapUrl:null,standardFontDataUrl:e.useWorkerFetch?e.standardFontDataUrl:null}});e.data&&(e.data=null);if(t.destroyed)throw new Error("Worker was destroyed");return i}class PDFDocumentLoadingTask{static#t=0;#e=null;constructor(){this._capability=(0,_util.createPromiseCapability)();this._transport=null;this._worker=null;this.docId="d"+PDFDocumentLoadingTask.#t++;this.destroyed=!1;this.onPassword=null;this.onProgress=null}get onUnsupportedFeature(){return this.#e}set onUnsupportedFeature(t){(0,_display_utils.deprecated)("The PDFDocumentLoadingTask onUnsupportedFeature property will be removed in the future.");this.#e=t}get promise(){return this._capability.promise}async destroy(){this.destroyed=!0;await(this._transport?.destroy());this._transport=null;if(this._worker){this._worker.destroy();this._worker=null}}}exports.PDFDocumentLoadingTask=PDFDocumentLoadingTask;class PDFDataRangeTransport{constructor(t,e,s=!1,n=null){this.length=t;this.initialData=e;this.progressiveDone=s;this.contentDispositionFilename=n;this._rangeListeners=[];this._progressListeners=[];this._progressiveReadListeners=[];this._progressiveDoneListeners=[];this._readyCapability=(0,_util.createPromiseCapability)()}addRangeListener(t){this._rangeListeners.push(t)}addProgressListener(t){this._progressListeners.push(t)}addProgressiveReadListener(t){this._progressiveReadListeners.push(t)}addProgressiveDoneListener(t){this._progressiveDoneListeners.push(t)}onDataRange(t,e){for(const s of this._rangeListeners)s(t,e)}onDataProgress(t,e){this._readyCapability.promise.then((()=>{for(const s of this._progressListeners)s(t,e)}))}onDataProgressiveRead(t){this._readyCapability.promise.then((()=>{for(const e of this._progressiveReadListeners)e(t)}))}onDataProgressiveDone(){this._readyCapability.promise.then((()=>{for(const t of this._progressiveDoneListeners)t()}))}transportReady(){this._readyCapability.resolve()}requestDataRange(t,e){(0,_util.unreachable)("Abstract method PDFDataRangeTransport.requestDataRange")}abort(){}}exports.PDFDataRangeTransport=PDFDataRangeTransport;class PDFDocumentProxy{constructor(t,e){this._pdfInfo=t;this._transport=e}get annotationStorage(){return this._transport.annotationStorage}get numPages(){return this._pdfInfo.numPages}get fingerprints(){return this._pdfInfo.fingerprints}get stats(){(0,_display_utils.deprecated)("The PDFDocumentProxy stats property will be removed in the future.");return this._transport.stats}get isPureXfa(){return(0,_util.shadow)(this,"isPureXfa",!!this._transport._htmlForXfa)}get allXfaHtml(){return this._transport._htmlForXfa}getPage(t){return this._transport.getPage(t)}getPageIndex(t){return this._transport.getPageIndex(t)}getDestinations(){return this._transport.getDestinations()}getDestination(t){return this._transport.getDestination(t)}getPageLabels(){return this._transport.getPageLabels()}getPageLayout(){return this._transport.getPageLayout()}getPageMode(){return this._transport.getPageMode()}getViewerPreferences(){return this._transport.getViewerPreferences()}getOpenAction(){return this._transport.getOpenAction()}getAttachments(){return this._transport.getAttachments()}getJavaScript(){return this._transport.getJavaScript()}getJSActions(){return this._transport.getDocJSActions()}getOutline(){return this._transport.getOutline()}getOptionalContentConfig(){return this._transport.getOptionalContentConfig()}getPermissions(){return this._transport.getPermissions()}getMetadata(){return this._transport.getMetadata()}getMarkInfo(){return this._transport.getMarkInfo()}getData(){return this._transport.getData()}saveDocument(){return this._transport.saveDocument()}getDownloadInfo(){return this._transport.downloadInfoCapability.promise}cleanup(t=!1){return this._transport.startCleanup(t||this.isPureXfa)}destroy(){return this.loadingTask.destroy()}get loadingParams(){return this._transport.loadingParams}get loadingTask(){return this._transport.loadingTask}getFieldObjects(){return this._transport.getFieldObjects()}hasJSActions(){return this._transport.hasJSActions()}getCalculationOrderIds(){return this._transport.getCalculationOrderIds()}}exports.PDFDocumentProxy=PDFDocumentProxy;class PDFPageProxy{constructor(t,e,s,n,i=!1){this._pageIndex=t;this._pageInfo=e;this._ownerDocument=n;this._transport=s;this._stats=i?new _display_utils.StatTimer:null;this._pdfBug=i;this.commonObjs=s.commonObjs;this.objs=new PDFObjects;this._bitmaps=new Set;this.cleanupAfterRender=!1;this.pendingCleanup=!1;this._intentStates=new Map;this.destroyed=!1}get pageNumber(){return this._pageIndex+1}get rotate(){return this._pageInfo.rotate}get ref(){return this._pageInfo.ref}get userUnit(){return this._pageInfo.userUnit}get view(){return this._pageInfo.view}getViewport({scale:t,rotation:e=this.rotate,offsetX:s=0,offsetY:n=0,dontFlip:i=!1}={}){return new _display_utils.PageViewport({viewBox:this.view,scale:t,rotation:e,offsetX:s,offsetY:n,dontFlip:i})}getAnnotations({intent:t="display"}={}){const e=this._transport.getRenderingIntent(t);return this._transport.getAnnotations(this._pageIndex,e.renderingIntent)}getJSActions(){return this._transport.getPageJSActions(this._pageIndex)}get isPureXfa(){return(0,_util.shadow)(this,"isPureXfa",!!this._transport._htmlForXfa)}async getXfa(){return this._transport._htmlForXfa?.children[this._pageIndex]||null}render({canvasContext:t,viewport:e,intent:s="display",annotationMode:n=_util.AnnotationMode.ENABLE,transform:i=null,canvasFactory:a=null,background:r=null,optionalContentConfigPromise:o=null,annotationCanvasMap:l=null,pageColors:c=null,printAnnotationStorage:h=null}){this._stats?.time("Overall");const d=this._transport.getRenderingIntent(s,n,h);this.pendingCleanup=!1;o||(o=this._transport.getOptionalContentConfig());let u=this._intentStates.get(d.cacheKey);if(!u){u=Object.create(null);this._intentStates.set(d.cacheKey,u)}if(u.streamReaderCancelTimeout){clearTimeout(u.streamReaderCancelTimeout);u.streamReaderCancelTimeout=null}const p=a||new DefaultCanvasFactory({ownerDocument:this._ownerDocument}),g=!!(d.renderingIntent&_util.RenderingIntentFlag.PRINT);if(!u.displayReadyCapability){u.displayReadyCapability=(0,_util.createPromiseCapability)();u.operatorList={fnArray:[],argsArray:[],lastChunk:!1,separateAnnots:null};this._stats?.time("Page Request");this._pumpOperatorList(d)}const complete=t=>{u.renderTasks.delete(m);(this.cleanupAfterRender||g)&&(this.pendingCleanup=!0);this._tryCleanup();if(t){m.capability.reject(t);this._abortOperatorList({intentState:u,reason:t instanceof Error?t:new Error(t)})}else m.capability.resolve();this._stats?.timeEnd("Rendering");this._stats?.timeEnd("Overall")},m=new InternalRenderTask({callback:complete,params:{canvasContext:t,viewport:e,transform:i,background:r},objs:this.objs,commonObjs:this.commonObjs,annotationCanvasMap:l,operatorList:u.operatorList,pageIndex:this._pageIndex,canvasFactory:p,useRequestAnimationFrame:!g,pdfBug:this._pdfBug,pageColors:c});(u.renderTasks||=new Set).add(m);const f=m.task;Promise.all([u.displayReadyCapability.promise,o]).then((([t,e])=>{if(this.pendingCleanup)complete();else{this._stats?.time("Rendering");m.initializeGraphics({transparency:t,optionalContentConfig:e});m.operatorListChanged()}})).catch(complete);return f}getOperatorList({intent:t="display",annotationMode:e=_util.AnnotationMode.ENABLE,printAnnotationStorage:s=null}={}){const n=this._transport.getRenderingIntent(t,e,s,!0);let i,a=this._intentStates.get(n.cacheKey);if(!a){a=Object.create(null);this._intentStates.set(n.cacheKey,a)}if(!a.opListReadCapability){i=Object.create(null);i.operatorListChanged=function operatorListChanged(){if(a.operatorList.lastChunk){a.opListReadCapability.resolve(a.operatorList);a.renderTasks.delete(i)}};a.opListReadCapability=(0,_util.createPromiseCapability)();(a.renderTasks||=new Set).add(i);a.operatorList={fnArray:[],argsArray:[],lastChunk:!1,separateAnnots:null};this._stats?.time("Page Request");this._pumpOperatorList(n)}return a.opListReadCapability.promise}streamTextContent({disableCombineTextItems:t=!1,includeMarkedContent:e=!1}={}){return this._transport.messageHandler.sendWithStream("GetTextContent",{pageIndex:this._pageIndex,combineTextItems:!0!==t,includeMarkedContent:!0===e},{highWaterMark:100,size:t=>t.items.length})}getTextContent(t={}){if(this._transport._htmlForXfa)return this.getXfa().then((t=>_xfa_text.XfaText.textContent(t)));const e=this.streamTextContent(t);return new Promise((function(t,s){const n=e.getReader(),i={items:[],styles:Object.create(null)};!function pump(){n.read().then((function({value:e,done:s}){if(s)t(i);else{Object.assign(i.styles,e.styles);i.items.push(...e.items);pump()}}),s)}()}))}getStructTree(){return this._transport.getStructTree(this._pageIndex)}_destroy(){this.destroyed=!0;const t=[];for(const e of this._intentStates.values()){this._abortOperatorList({intentState:e,reason:new Error("Page was destroyed."),force:!0});if(!e.opListReadCapability)for(const s of e.renderTasks){t.push(s.completed);s.cancel()}}this.objs.clear();for(const t of this._bitmaps)t.close();this._bitmaps.clear();this.pendingCleanup=!1;return Promise.all(t)}cleanup(t=!1){this.pendingCleanup=!0;return this._tryCleanup(t)}_tryCleanup(t=!1){if(!this.pendingCleanup)return!1;for(const{renderTasks:t,operatorList:e}of this._intentStates.values())if(t.size>0||!e.lastChunk)return!1;this._intentStates.clear();this.objs.clear();t&&this._stats&&(this._stats=new _display_utils.StatTimer);for(const t of this._bitmaps)t.close();this._bitmaps.clear();this.pendingCleanup=!1;return!0}_startRenderPage(t,e){const s=this._intentStates.get(e);if(s){this._stats?.timeEnd("Page Request");s.displayReadyCapability?.resolve(t)}}_renderPageChunk(t,e){for(let s=0,n=t.length;s{n.read().then((({value:t,done:e})=>{if(e)i.streamReader=null;else if(!this._transport.destroyed){this._renderPageChunk(t,i);pump()}}),(t=>{i.streamReader=null;if(!this._transport.destroyed){if(i.operatorList){i.operatorList.lastChunk=!0;for(const t of i.renderTasks)t.operatorListChanged();this._tryCleanup()}if(i.displayReadyCapability)i.displayReadyCapability.reject(t);else{if(!i.opListReadCapability)throw t;i.opListReadCapability.reject(t)}}}))};pump()}_abortOperatorList({intentState:t,reason:e,force:s=!1}){if(t.streamReader){if(t.streamReaderCancelTimeout){clearTimeout(t.streamReaderCancelTimeout);t.streamReaderCancelTimeout=null}if(!s){if(t.renderTasks.size>0)return;if(e instanceof _display_utils.RenderingCancelledException){let s=RENDERING_CANCELLED_TIMEOUT;e.extraDelay>0&&e.extraDelay<1e3&&(s+=e.extraDelay);t.streamReaderCancelTimeout=setTimeout((()=>{t.streamReaderCancelTimeout=null;this._abortOperatorList({intentState:t,reason:e,force:!0})}),s);return}}t.streamReader.cancel(new _util.AbortException(e.message)).catch((()=>{}));t.streamReader=null;if(!this._transport.destroyed){for(const[e,s]of this._intentStates)if(s===t){this._intentStates.delete(e);break}this.cleanup()}}}get stats(){return this._stats}}exports.PDFPageProxy=PDFPageProxy;class LoopbackPort{#s=[];#n=Promise.resolve();postMessage(t,e){const s={data:structuredClone(t,e)};this.#n.then((()=>{for(const t of this.#s)t.call(this,s)}))}addEventListener(t,e){this.#s.push(e)}removeEventListener(t,e){const s=this.#s.indexOf(e);this.#s.splice(s,1)}terminate(){this.#s.length=0}}exports.LoopbackPort=LoopbackPort;const PDFWorkerUtil={isWorkerDisabled:!1,fallbackWorkerSrc:null,fakeWorkerId:0};exports.PDFWorkerUtil=PDFWorkerUtil;if(_is_node.isNodeJS&&"function"==typeof require){PDFWorkerUtil.isWorkerDisabled=!0;PDFWorkerUtil.fallbackWorkerSrc="./pdf.worker.js"}else if("object"==typeof document){const t=document?.currentScript?.src;t&&(PDFWorkerUtil.fallbackWorkerSrc=t.replace(/(\.(?:min\.)?js)(\?.*)?$/i,".worker$1$2"))}PDFWorkerUtil.isSameOrigin=function(t,e){let s;try{s=new URL(t);if(!s.origin||"null"===s.origin)return!1}catch(t){return!1}const n=new URL(e,s);return s.origin===n.origin};PDFWorkerUtil.createCDNWrapper=function(t){const e=`importScripts("${t}");`;return URL.createObjectURL(new Blob([e]))};class PDFWorker{static#i=new WeakMap;constructor({name:t=null,port:e=null,verbosity:s=(0,_util.getVerbosityLevel)()}={}){if(e&&PDFWorker.#i.has(e))throw new Error("Cannot use more than one PDFWorker per port.");this.name=t;this.destroyed=!1;this.verbosity=s;this._readyCapability=(0,_util.createPromiseCapability)();this._port=null;this._webWorker=null;this._messageHandler=null;if(e){PDFWorker.#i.set(e,this);this._initializeFromPort(e)}else this._initialize()}get promise(){return this._readyCapability.promise}get port(){return this._port}get messageHandler(){return this._messageHandler}_initializeFromPort(t){this._port=t;this._messageHandler=new _message_handler.MessageHandler("main","worker",t);this._messageHandler.on("ready",(function(){}));this._readyCapability.resolve();this._messageHandler.send("configure",{verbosity:this.verbosity})}_initialize(){if(!PDFWorkerUtil.isWorkerDisabled&&!PDFWorker._mainThreadWorkerMessageHandler){let{workerSrc:t}=PDFWorker;try{PDFWorkerUtil.isSameOrigin(window.location.href,t)||(t=PDFWorkerUtil.createCDNWrapper(new URL(t,window.location).href));const e=new Worker(t),s=new _message_handler.MessageHandler("main","worker",e),terminateEarly=()=>{e.removeEventListener("error",onWorkerError);s.destroy();e.terminate();this.destroyed?this._readyCapability.reject(new Error("Worker was destroyed")):this._setupFakeWorker()},onWorkerError=()=>{this._webWorker||terminateEarly()};e.addEventListener("error",onWorkerError);s.on("test",(t=>{e.removeEventListener("error",onWorkerError);if(this.destroyed)terminateEarly();else if(t){this._messageHandler=s;this._port=e;this._webWorker=e;this._readyCapability.resolve();s.send("configure",{verbosity:this.verbosity})}else{this._setupFakeWorker();s.destroy();e.terminate()}}));s.on("ready",(t=>{e.removeEventListener("error",onWorkerError);if(this.destroyed)terminateEarly();else try{sendTest()}catch(t){this._setupFakeWorker()}}));const sendTest=()=>{const t=new Uint8Array;s.send("test",t,[t.buffer])};sendTest();return}catch(t){(0,_util.info)("The worker has been disabled.")}}this._setupFakeWorker()}_setupFakeWorker(){if(!PDFWorkerUtil.isWorkerDisabled){(0,_util.warn)("Setting up fake worker.");PDFWorkerUtil.isWorkerDisabled=!0}PDFWorker._setupFakeWorkerGlobal.then((t=>{if(this.destroyed){this._readyCapability.reject(new Error("Worker was destroyed"));return}const e=new LoopbackPort;this._port=e;const s="fake"+PDFWorkerUtil.fakeWorkerId++,n=new _message_handler.MessageHandler(s+"_worker",s,e);t.setup(n,e);const i=new _message_handler.MessageHandler(s,s+"_worker",e);this._messageHandler=i;this._readyCapability.resolve();i.send("configure",{verbosity:this.verbosity})})).catch((t=>{this._readyCapability.reject(new Error(`Setting up fake worker failed: "${t.message}".`))}))}destroy(){this.destroyed=!0;if(this._webWorker){this._webWorker.terminate();this._webWorker=null}PDFWorker.#i.delete(this._port);this._port=null;if(this._messageHandler){this._messageHandler.destroy();this._messageHandler=null}}static fromPort(t){if(!t?.port)throw new Error("PDFWorker.fromPort - invalid method signature.");return this.#i.has(t.port)?this.#i.get(t.port):new PDFWorker(t)}static get workerSrc(){if(_worker_options.GlobalWorkerOptions.workerSrc)return _worker_options.GlobalWorkerOptions.workerSrc;if(null!==PDFWorkerUtil.fallbackWorkerSrc){_is_node.isNodeJS||(0,_display_utils.deprecated)('No "GlobalWorkerOptions.workerSrc" specified.');return PDFWorkerUtil.fallbackWorkerSrc}throw new Error('No "GlobalWorkerOptions.workerSrc" specified.')}static get _mainThreadWorkerMessageHandler(){try{return globalThis.pdfjsWorker?.WorkerMessageHandler||null}catch(t){return null}}static get _setupFakeWorkerGlobal(){const loader=async()=>{const mainWorkerMessageHandler=this._mainThreadWorkerMessageHandler;if(mainWorkerMessageHandler)return mainWorkerMessageHandler;if(_is_node.isNodeJS&&"function"==typeof require){const worker=eval("require")(this.workerSrc);return worker.WorkerMessageHandler}await(0,_display_utils.loadScript)(this.workerSrc);return window.pdfjsWorker.WorkerMessageHandler};return(0,_util.shadow)(this,"_setupFakeWorkerGlobal",loader())}}exports.PDFWorker=PDFWorker;class WorkerTransport{#a=null;#r=new Map;#o=new Map;#l=null;constructor(t,e,s,n){this.messageHandler=t;this.loadingTask=e;this.commonObjs=new PDFObjects;this.fontLoader=new _font_loader.FontLoader({onUnsupportedFeature:this._onUnsupportedFeature.bind(this),ownerDocument:n.ownerDocument,styleElement:n.styleElement});this._params=n;if(!n.useWorkerFetch){this.CMapReaderFactory=new n.CMapReaderFactory({baseUrl:n.cMapUrl,isCompressed:n.cMapPacked});this.StandardFontDataFactory=new n.StandardFontDataFactory({baseUrl:n.standardFontDataUrl})}this.destroyed=!1;this.destroyCapability=null;this._passwordCapability=null;this._networkStream=s;this._fullReader=null;this._lastProgress=null;this.downloadInfoCapability=(0,_util.createPromiseCapability)();this.setupMessageHandler()}get annotationStorage(){return(0,_util.shadow)(this,"annotationStorage",new _annotation_storage.AnnotationStorage)}get stats(){return this.#a}getRenderingIntent(t,e=_util.AnnotationMode.ENABLE,s=null,n=!1){let i=_util.RenderingIntentFlag.DISPLAY,a=null;switch(t){case"any":i=_util.RenderingIntentFlag.ANY;break;case"display":break;case"print":i=_util.RenderingIntentFlag.PRINT;break;default:(0,_util.warn)(`getRenderingIntent - invalid intent: ${t}`)}switch(e){case _util.AnnotationMode.DISABLE:i+=_util.RenderingIntentFlag.ANNOTATIONS_DISABLE;break;case _util.AnnotationMode.ENABLE:break;case _util.AnnotationMode.ENABLE_FORMS:i+=_util.RenderingIntentFlag.ANNOTATIONS_FORMS;break;case _util.AnnotationMode.ENABLE_STORAGE:i+=_util.RenderingIntentFlag.ANNOTATIONS_STORAGE;a=(i&_util.RenderingIntentFlag.PRINT&&s instanceof _annotation_storage.PrintAnnotationStorage?s:this.annotationStorage).serializable;break;default:(0,_util.warn)(`getRenderingIntent - invalid annotationMode: ${e}`)}n&&(i+=_util.RenderingIntentFlag.OPLIST);return{renderingIntent:i,cacheKey:`${i}_${_annotation_storage.AnnotationStorage.getHash(a)}`,annotationStorageMap:a}}destroy(){if(this.destroyCapability)return this.destroyCapability.promise;this.destroyed=!0;this.destroyCapability=(0,_util.createPromiseCapability)();this._passwordCapability&&this._passwordCapability.reject(new Error("Worker was destroyed during onPassword callback"));const t=[];for(const e of this.#r.values())t.push(e._destroy());this.#r.clear();this.#o.clear();this.hasOwnProperty("annotationStorage")&&this.annotationStorage.resetModified();const e=this.messageHandler.sendWithPromise("Terminate",null);t.push(e);Promise.all(t).then((()=>{this.commonObjs.clear();this.fontLoader.clear();this.#l=null;this._getFieldObjectsPromise=null;this._hasJSActionsPromise=null;this._networkStream&&this._networkStream.cancelAllRequests(new _util.AbortException("Worker was terminated."));if(this.messageHandler){this.messageHandler.destroy();this.messageHandler=null}this.destroyCapability.resolve()}),this.destroyCapability.reject);return this.destroyCapability.promise}setupMessageHandler(){const{messageHandler:t,loadingTask:e}=this;t.on("GetReader",((t,e)=>{(0,_util.assert)(this._networkStream,"GetReader - no `IPDFStream` instance available.");this._fullReader=this._networkStream.getFullReader();this._fullReader.onProgress=t=>{this._lastProgress={loaded:t.loaded,total:t.total}};e.onPull=()=>{this._fullReader.read().then((function({value:t,done:s}){if(s)e.close();else{(0,_util.assert)((0,_util.isArrayBuffer)(t),"GetReader - expected an ArrayBuffer.");e.enqueue(new Uint8Array(t),1,[t])}})).catch((t=>{e.error(t)}))};e.onCancel=t=>{this._fullReader.cancel(t);e.ready.catch((t=>{if(!this.destroyed)throw t}))}}));t.on("ReaderHeadersReady",(t=>{const s=(0,_util.createPromiseCapability)(),n=this._fullReader;n.headersReady.then((()=>{if(!n.isStreamingSupported||!n.isRangeSupported){this._lastProgress&&e.onProgress?.(this._lastProgress);n.onProgress=t=>{e.onProgress?.({loaded:t.loaded,total:t.total})}}s.resolve({isStreamingSupported:n.isStreamingSupported,isRangeSupported:n.isRangeSupported,contentLength:n.contentLength})}),s.reject);return s.promise}));t.on("GetRangeReader",((t,e)=>{(0,_util.assert)(this._networkStream,"GetRangeReader - no `IPDFStream` instance available.");const s=this._networkStream.getRangeReader(t.begin,t.end);if(s){e.onPull=()=>{s.read().then((function({value:t,done:s}){if(s)e.close();else{(0,_util.assert)((0,_util.isArrayBuffer)(t),"GetRangeReader - expected an ArrayBuffer.");e.enqueue(new Uint8Array(t),1,[t])}})).catch((t=>{e.error(t)}))};e.onCancel=t=>{s.cancel(t);e.ready.catch((t=>{if(!this.destroyed)throw t}))}}else e.close()}));t.on("GetDoc",(({pdfInfo:t})=>{this._numPages=t.numPages;this._htmlForXfa=t.htmlForXfa;delete t.htmlForXfa;e._capability.resolve(new PDFDocumentProxy(t,this))}));t.on("DocException",(function(t){let s;switch(t.name){case"PasswordException":s=new _util.PasswordException(t.message,t.code);break;case"InvalidPDFException":s=new _util.InvalidPDFException(t.message);break;case"MissingPDFException":s=new _util.MissingPDFException(t.message);break;case"UnexpectedResponseException":s=new _util.UnexpectedResponseException(t.message,t.status);break;case"UnknownErrorException":s=new _util.UnknownErrorException(t.message,t.details);break;default:(0,_util.unreachable)("DocException - expected a valid Error.")}e._capability.reject(s)}));t.on("PasswordRequest",(t=>{this._passwordCapability=(0,_util.createPromiseCapability)();if(e.onPassword){const updatePassword=t=>{t instanceof Error?this._passwordCapability.reject(t):this._passwordCapability.resolve({password:t})};try{e.onPassword(updatePassword,t.code)}catch(t){this._passwordCapability.reject(t)}}else this._passwordCapability.reject(new _util.PasswordException(t.message,t.code));return this._passwordCapability.promise}));t.on("DataLoaded",(t=>{e.onProgress?.({loaded:t.length,total:t.length});this.downloadInfoCapability.resolve(t)}));t.on("StartRenderPage",(t=>{if(this.destroyed)return;this.#r.get(t.pageIndex)._startRenderPage(t.transparency,t.cacheKey)}));t.on("commonobj",(([e,s,n])=>{if(!this.destroyed&&!this.commonObjs.has(e))switch(s){case"Font":const i=this._params;if("error"in n){const t=n.error;(0,_util.warn)(`Error during font loading: ${t}`);this.commonObjs.resolve(e,t);break}let a=null;i.pdfBug&&globalThis.FontInspector?.enabled&&(a={registerFont(t,e){globalThis.FontInspector.fontAdded(t,e)}});const r=new _font_loader.FontFaceObject(n,{isEvalSupported:i.isEvalSupported,disableFontFace:i.disableFontFace,ignoreErrors:i.ignoreErrors,onUnsupportedFeature:this._onUnsupportedFeature.bind(this),fontRegistry:a});this.fontLoader.bind(r).catch((s=>t.sendWithPromise("FontFallback",{id:e}))).finally((()=>{!i.fontExtraProperties&&r.data&&(r.data=null);this.commonObjs.resolve(e,r)}));break;case"FontPath":case"Image":this.commonObjs.resolve(e,n);break;default:throw new Error(`Got unknown common object type ${s}`)}}));t.on("obj",(([t,e,s,n])=>{if(this.destroyed)return;const i=this.#r.get(e);if(!i.objs.has(t))switch(s){case"Image":i.objs.resolve(t,n);const e=8e6;if(n){let t;if(n.bitmap){const{bitmap:e,width:s,height:a}=n;t=s*a*4;i._bitmaps.add(e)}else t=n.data?.length||0;t>e&&(i.cleanupAfterRender=!0)}break;case"Pattern":i.objs.resolve(t,n);break;default:throw new Error(`Got unknown object type ${s}`)}}));t.on("DocProgress",(t=>{this.destroyed||e.onProgress?.({loaded:t.loaded,total:t.total})}));t.on("DocStats",(t=>{this.destroyed||(this.#a=Object.freeze({streamTypes:Object.freeze(t.streamTypes),fontTypes:Object.freeze(t.fontTypes)}))}));t.on("UnsupportedFeature",this._onUnsupportedFeature.bind(this));t.on("FetchBuiltInCMap",(t=>this.destroyed?Promise.reject(new Error("Worker was destroyed.")):this.CMapReaderFactory?this.CMapReaderFactory.fetch(t):Promise.reject(new Error("CMapReaderFactory not initialized, see the `useWorkerFetch` parameter."))));t.on("FetchStandardFontData",(t=>this.destroyed?Promise.reject(new Error("Worker was destroyed.")):this.StandardFontDataFactory?this.StandardFontDataFactory.fetch(t):Promise.reject(new Error("StandardFontDataFactory not initialized, see the `useWorkerFetch` parameter."))))}_onUnsupportedFeature({featureId:t}){this.destroyed||this.loadingTask.onUnsupportedFeature?.(t)}getData(){return this.messageHandler.sendWithPromise("GetData",null)}saveDocument(){this.annotationStorage.size<=0&&(0,_util.warn)("saveDocument called while `annotationStorage` is empty, please use the getData-method instead.");return this.messageHandler.sendWithPromise("SaveDocument",{isPureXfa:!!this._htmlForXfa,numPages:this._numPages,annotationStorage:this.annotationStorage.serializable,filename:this._fullReader?.filename??null}).finally((()=>{this.annotationStorage.resetModified()}))}getPage(t){if(!Number.isInteger(t)||t<=0||t>this._numPages)return Promise.reject(new Error("Invalid page request."));const e=t-1,s=this.#o.get(e);if(s)return s;const n=this.messageHandler.sendWithPromise("GetPage",{pageIndex:e}).then((t=>{if(this.destroyed)throw new Error("Transport destroyed");const s=new PDFPageProxy(e,t,this,this._params.ownerDocument,this._params.pdfBug);this.#r.set(e,s);return s}));this.#o.set(e,n);return n}getPageIndex(t){return"object"!=typeof t||null===t||!Number.isInteger(t.num)||t.num<0||!Number.isInteger(t.gen)||t.gen<0?Promise.reject(new Error("Invalid pageIndex request.")):this.messageHandler.sendWithPromise("GetPageIndex",{num:t.num,gen:t.gen})}getAnnotations(t,e){return this.messageHandler.sendWithPromise("GetAnnotations",{pageIndex:t,intent:e})}getFieldObjects(){return this._getFieldObjectsPromise||=this.messageHandler.sendWithPromise("GetFieldObjects",null)}hasJSActions(){return this._hasJSActionsPromise||=this.messageHandler.sendWithPromise("HasJSActions",null)}getCalculationOrderIds(){return this.messageHandler.sendWithPromise("GetCalculationOrderIds",null)}getDestinations(){return this.messageHandler.sendWithPromise("GetDestinations",null)}getDestination(t){return"string"!=typeof t?Promise.reject(new Error("Invalid destination request.")):this.messageHandler.sendWithPromise("GetDestination",{id:t})}getPageLabels(){return this.messageHandler.sendWithPromise("GetPageLabels",null)}getPageLayout(){return this.messageHandler.sendWithPromise("GetPageLayout",null)}getPageMode(){return this.messageHandler.sendWithPromise("GetPageMode",null)}getViewerPreferences(){return this.messageHandler.sendWithPromise("GetViewerPreferences",null)}getOpenAction(){return this.messageHandler.sendWithPromise("GetOpenAction",null)}getAttachments(){return this.messageHandler.sendWithPromise("GetAttachments",null)}getJavaScript(){return this.messageHandler.sendWithPromise("GetJavaScript",null)}getDocJSActions(){return this.messageHandler.sendWithPromise("GetDocJSActions",null)}getPageJSActions(t){return this.messageHandler.sendWithPromise("GetPageJSActions",{pageIndex:t})}getStructTree(t){return this.messageHandler.sendWithPromise("GetStructTree",{pageIndex:t})}getOutline(){return this.messageHandler.sendWithPromise("GetOutline",null)}getOptionalContentConfig(){return this.messageHandler.sendWithPromise("GetOptionalContentConfig",null).then((t=>new _optional_content_config.OptionalContentConfig(t)))}getPermissions(){return this.messageHandler.sendWithPromise("GetPermissions",null)}getMetadata(){return this.#l||=this.messageHandler.sendWithPromise("GetMetadata",null).then((t=>({info:t[0],metadata:t[1]?new _metadata.Metadata(t[1]):null,contentDispositionFilename:this._fullReader?.filename??null,contentLength:this._fullReader?.contentLength??null})))}getMarkInfo(){return this.messageHandler.sendWithPromise("GetMarkInfo",null)}async startCleanup(t=!1){if(!this.destroyed){await this.messageHandler.sendWithPromise("Cleanup",null);for(const t of this.#r.values()){if(!t.cleanup())throw new Error(`startCleanup: Page ${t.pageNumber} is currently rendering.`)}this.commonObjs.clear();t||this.fontLoader.clear();this.#l=null;this._getFieldObjectsPromise=null;this._hasJSActionsPromise=null}}get loadingParams(){const t=this._params;return(0,_util.shadow)(this,"loadingParams",{disableAutoFetch:t.disableAutoFetch,enableXfa:t.enableXfa})}}class PDFObjects{#c=Object.create(null);#h(t){const e=this.#c[t];return e||(this.#c[t]={capability:(0,_util.createPromiseCapability)(),data:null})}get(t,e=null){if(e){const s=this.#h(t);s.capability.promise.then((()=>e(s.data)));return null}const s=this.#c[t];if(!s?.capability.settled)throw new Error(`Requesting object that isn't resolved yet ${t}.`);return s.data}has(t){return this.#c[t]?.capability.settled||!1}resolve(t,e=null){const s=this.#h(t);s.data=e;s.capability.resolve()}clear(){this.#c=Object.create(null)}}class RenderTask{#d=null;constructor(t){this.#d=t;this.onContinue=null}get promise(){return this.#d.capability.promise}cancel(t=0){this.#d.cancel(null,t)}get separateAnnots(){const{separateAnnots:t}=this.#d.operatorList;if(!t)return!1;const{annotationCanvasMap:e}=this.#d;return t.form||t.canvas&&e?.size>0}}exports.RenderTask=RenderTask;class InternalRenderTask{static#u=new WeakSet;constructor({callback:t,params:e,objs:s,commonObjs:n,annotationCanvasMap:i,operatorList:a,pageIndex:r,canvasFactory:o,useRequestAnimationFrame:l=!1,pdfBug:c=!1,pageColors:h=null}){this.callback=t;this.params=e;this.objs=s;this.commonObjs=n;this.annotationCanvasMap=i;this.operatorListIdx=null;this.operatorList=a;this._pageIndex=r;this.canvasFactory=o;this._pdfBug=c;this.pageColors=h;this.running=!1;this.graphicsReadyCallback=null;this.graphicsReady=!1;this._useRequestAnimationFrame=!0===l&&"undefined"!=typeof window;this.cancelled=!1;this.capability=(0,_util.createPromiseCapability)();this.task=new RenderTask(this);this._cancelBound=this.cancel.bind(this);this._continueBound=this._continue.bind(this);this._scheduleNextBound=this._scheduleNext.bind(this);this._nextBound=this._next.bind(this);this._canvas=e.canvasContext.canvas}get completed(){return this.capability.promise.catch((function(){}))}initializeGraphics({transparency:t=!1,optionalContentConfig:e}){if(this.cancelled)return;if(this._canvas){if(InternalRenderTask.#u.has(this._canvas))throw new Error("Cannot use the same canvas during multiple render() operations. Use different canvas or ensure previous operations were cancelled or completed.");InternalRenderTask.#u.add(this._canvas)}if(this._pdfBug&&globalThis.StepperManager?.enabled){this.stepper=globalThis.StepperManager.create(this._pageIndex);this.stepper.init(this.operatorList);this.stepper.nextBreakPoint=this.stepper.getNextBreakPoint()}const{canvasContext:s,viewport:n,transform:i,background:a}=this.params;this.gfx=new _canvas.CanvasGraphics(s,this.commonObjs,this.objs,this.canvasFactory,{optionalContentConfig:e},this.annotationCanvasMap,this.pageColors);this.gfx.beginDrawing({transform:i,viewport:n,transparency:t,background:a});this.operatorListIdx=0;this.graphicsReady=!0;this.graphicsReadyCallback?.()}cancel(t=null,e=0){this.running=!1;this.cancelled=!0;this.gfx?.endDrawing();this._canvas&&InternalRenderTask.#u.delete(this._canvas);this.callback(t||new _display_utils.RenderingCancelledException(`Rendering cancelled, page ${this._pageIndex+1}`,"canvas",e))}operatorListChanged(){if(this.graphicsReady){this.stepper?.updateOperatorList(this.operatorList);this.running||this._continue()}else this.graphicsReadyCallback||(this.graphicsReadyCallback=this._continueBound)}_continue(){this.running=!0;this.cancelled||(this.task.onContinue?this.task.onContinue(this._scheduleNextBound):this._scheduleNext())}_scheduleNext(){this._useRequestAnimationFrame?window.requestAnimationFrame((()=>{this._nextBound().catch(this._cancelBound)})):Promise.resolve().then(this._nextBound).catch(this._cancelBound)}async _next(){if(!this.cancelled){this.operatorListIdx=this.gfx.executeOperatorList(this.operatorList,this.operatorListIdx,this._continueBound,this.stepper);if(this.operatorListIdx===this.operatorList.argsArray.length){this.running=!1;if(this.operatorList.lastChunk){this.gfx.endDrawing();this._canvas&&InternalRenderTask.#u.delete(this._canvas);this.callback()}}}}}const version="3.2.146";exports.version=version;const build="3fd2a3548";exports.build=build},(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0});e.PrintAnnotationStorage=e.AnnotationStorage=void 0;var n=s(1),i=s(4),a=s(8);class AnnotationStorage{#p=!1;#g=new Map;constructor(){this.onSetModified=null;this.onResetModified=null;this.onAnnotationEditor=null}getValue(t,e){const s=this.#g.get(t);return void 0===s?e:Object.assign(e,s)}getRawValue(t){return this.#g.get(t)}remove(t){this.#g.delete(t);0===this.#g.size&&this.resetModified();if("function"==typeof this.onAnnotationEditor){for(const t of this.#g.values())if(t instanceof i.AnnotationEditor)return;this.onAnnotationEditor(null)}}setValue(t,e){const s=this.#g.get(t);let n=!1;if(void 0!==s){for(const[t,i]of Object.entries(e))if(s[t]!==i){n=!0;s[t]=i}}else{n=!0;this.#g.set(t,e)}n&&this.#m();e instanceof i.AnnotationEditor&&"function"==typeof this.onAnnotationEditor&&this.onAnnotationEditor(e.constructor._type)}has(t){return this.#g.has(t)}getAll(){return this.#g.size>0?(0,n.objectFromMap)(this.#g):null}get size(){return this.#g.size}#m(){if(!this.#p){this.#p=!0;"function"==typeof this.onSetModified&&this.onSetModified()}}resetModified(){if(this.#p){this.#p=!1;"function"==typeof this.onResetModified&&this.onResetModified()}}get print(){return new PrintAnnotationStorage(this)}get serializable(){if(0===this.#g.size)return null;const t=new Map;for(const[e,s]of this.#g){const n=s instanceof i.AnnotationEditor?s.serialize():s;n&&t.set(e,n)}return t}static getHash(t){if(!t)return"";const e=new a.MurmurHash3_64;for(const[s,n]of t)e.update(`${s}:${JSON.stringify(n)}`);return e.hexdigest()}}e.AnnotationStorage=AnnotationStorage;class PrintAnnotationStorage extends AnnotationStorage{#f=null;constructor(t){super();this.#f=structuredClone(t.serializable)}get print(){(0,n.unreachable)("Should not call PrintAnnotationStorage.print")}get serializable(){return this.#f}}e.PrintAnnotationStorage=PrintAnnotationStorage},(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0});e.AnnotationEditor=void 0;var n=s(5),i=s(1);class AnnotationEditor{#b=this.focusin.bind(this);#A=this.focusout.bind(this);#_=!1;#y=!1;#v=!1;_uiManager=null;#S=AnnotationEditor._zIndex++;static _colorManager=new n.ColorManager;static _zIndex=1;constructor(t){this.constructor===AnnotationEditor&&(0,i.unreachable)("Cannot initialize AnnotationEditor.");this.parent=t.parent;this.id=t.id;this.width=this.height=null;this.pageIndex=t.parent.pageIndex;this.name=t.name;this.div=null;this._uiManager=t.uiManager;const{rotation:e,rawDims:{pageWidth:s,pageHeight:n,pageX:a,pageY:r}}=this.parent.viewport;this.rotation=e;this.pageDimensions=[s,n];this.pageTranslation=[a,r];const[o,l]=this.parentDimensions;this.x=t.x/o;this.y=t.y/l;this.isAttachedToDOM=!1}static get _defaultLineColor(){return(0,i.shadow)(this,"_defaultLineColor",this._colorManager.getHexCode("CanvasText"))}addCommands(t){this._uiManager.addCommands(t)}get currentLayer(){return this._uiManager.currentLayer}setInBackground(){this.div.style.zIndex=0}setInForeground(){this.div.style.zIndex=this.#S}setParent(t){if(null!==t){this.pageIndex=t.pageIndex;this.pageDimensions=t.pageDimensions}this.parent=t}focusin(t){this.#_?this.#_=!1:this.parent.setSelected(this)}focusout(t){if(!this.isAttachedToDOM)return;if(!t.relatedTarget?.closest(`#${this.id}`)){t.preventDefault();this.parent?.isMultipleSelection||this.commitOrRemove()}}commitOrRemove(){this.isEmpty()?this.remove():this.commit()}commit(){this.addToAnnotationStorage()}addToAnnotationStorage(){this._uiManager.addToAnnotationStorage(this)}dragstart(t){const e=this.parent.div.getBoundingClientRect();this.startX=t.clientX-e.x;this.startY=t.clientY-e.y;t.dataTransfer.setData("text/plain",this.id);t.dataTransfer.effectAllowed="move"}setAt(t,e,s,n){const[i,a]=this.parentDimensions;[s,n]=this.screenToPageTranslation(s,n);this.x=(t+s)/i;this.y=(e+n)/a;this.div.style.left=100*this.x+"%";this.div.style.top=100*this.y+"%"}translate(t,e){const[s,n]=this.parentDimensions;[t,e]=this.screenToPageTranslation(t,e);this.x+=t/s;this.y+=e/n;this.div.style.left=100*this.x+"%";this.div.style.top=100*this.y+"%"}screenToPageTranslation(t,e){switch(this.parentRotation){case 90:return[e,-t];case 180:return[-t,-e];case 270:return[-e,t];default:return[t,e]}}get parentScale(){return this._uiManager.viewParameters.realScale}get parentRotation(){return this._uiManager.viewParameters.rotation}get parentDimensions(){const{realScale:t}=this._uiManager.viewParameters,[e,s]=this.pageDimensions;return[e*t,s*t]}setDims(t,e){const[s,n]=this.parentDimensions;this.div.style.width=100*t/s+"%";this.div.style.height=100*e/n+"%"}fixDims(){const{style:t}=this.div,{height:e,width:s}=t,n=s.endsWith("%"),i=e.endsWith("%");if(n&&i)return;const[a,r]=this.parentDimensions;n||(t.width=100*parseFloat(s)/a+"%");i||(t.height=100*parseFloat(e)/r+"%")}getInitialTranslation(){return[0,0]}render(){this.div=document.createElement("div");this.div.setAttribute("data-editor-rotation",(360-this.rotation)%360);this.div.className=this.name;this.div.setAttribute("id",this.id);this.div.setAttribute("tabIndex",0);this.setInForeground();this.div.addEventListener("focusin",this.#b);this.div.addEventListener("focusout",this.#A);const[t,e]=this.getInitialTranslation();this.translate(t,e);(0,n.bindEvents)(this,this.div,["dragstart","pointerdown"]);return this.div}pointerdown(t){const{isMac:e}=i.FeatureTest.platform;if(0!==t.button||t.ctrlKey&&e)t.preventDefault();else{t.ctrlKey&&!e||t.shiftKey||t.metaKey&&e?this.parent.toggleSelected(this):this.parent.setSelected(this);this.#_=!0}}getRect(t,e){const s=this.parentScale,[n,i]=this.pageDimensions,[a,r]=this.pageTranslation,o=t/s,l=e/s,c=this.x*n,h=this.y*i,d=this.width*n,u=this.height*i;switch(this.rotation){case 0:return[c+o+a,i-h-l-u+r,c+o+d+a,i-h-l+r];case 90:return[c+l+a,i-h+o+r,c+l+u+a,i-h+o+d+r];case 180:return[c-o-d+a,i-h+l+r,c-o+a,i-h+l+u+r];case 270:return[c-l-u+a,i-h-o-d+r,c-l+a,i-h-o+r];default:throw new Error("Invalid rotation")}}getRectInCurrentCoords(t,e){const[s,n,i,a]=t,r=i-s,o=a-n;switch(this.rotation){case 0:return[s,e-a,r,o];case 90:return[s,e-n,o,r];case 180:return[i,e-n,r,o];case 270:return[i,e-a,o,r];default:throw new Error("Invalid rotation")}}onceAdded(){}isEmpty(){return!1}enableEditMode(){this.#v=!0}disableEditMode(){this.#v=!1}isInEditMode(){return this.#v}shouldGetKeyboardEvents(){return!1}needsToBeRebuilt(){return this.div&&!this.isAttachedToDOM}rebuild(){this.div?.addEventListener("focusin",this.#b)}serialize(){(0,i.unreachable)("An editor must be serializable")}static deserialize(t,e,s){const n=new this.prototype.constructor({parent:e,id:e.getNextId(),uiManager:s});n.rotation=t.rotation;const[i,a]=n.pageDimensions,[r,o,l,c]=n.getRectInCurrentCoords(t.rect,a);n.x=r/i;n.y=o/a;n.width=l/i;n.height=c/a;return n}remove(){this.div.removeEventListener("focusin",this.#b);this.div.removeEventListener("focusout",this.#A);this.isEmpty()||this.commit();this.parent.remove(this)}select(){this.div?.classList.add("selectedEditor")}unselect(){this.div?.classList.remove("selectedEditor")}updateParams(t,e){}disableEditing(){}enableEditing(){}get propertiesToUpdate(){return{}}get contentDiv(){return this.div}get isEditing(){return this.#y}set isEditing(t){this.#y=t;if(t){this.parent.setSelected(this);this.parent.setActiveEditor(this)}else this.parent.setActiveEditor(null)}}e.AnnotationEditor=AnnotationEditor},(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0});e.KeyboardManager=e.CommandManager=e.ColorManager=e.AnnotationEditorUIManager=void 0;e.bindEvents=function bindEvents(t,e,s){for(const n of s)e.addEventListener(n,t[n].bind(t))};e.opacityToHex=function opacityToHex(t){return Math.round(Math.min(255,Math.max(1,255*t))).toString(16).padStart(2,"0")};var n=s(1),i=s(6);class IdManager{#x=0;getId(){return`${n.AnnotationEditorPrefix}${this.#x++}`}}class CommandManager{#E=[];#C=!1;#P;#T=-1;constructor(t=128){this.#P=t}add({cmd:t,undo:e,mustExec:s,type:n=NaN,overwriteIfSameType:i=!1,keepUndo:a=!1}){s&&t();if(this.#C)return;const r={cmd:t,undo:e,type:n};if(-1===this.#T){this.#E.length>0&&(this.#E.length=0);this.#T=0;this.#E.push(r);return}if(i&&this.#E[this.#T].type===n){a&&(r.undo=this.#E[this.#T].undo);this.#E[this.#T]=r;return}const o=this.#T+1;if(o===this.#P)this.#E.splice(0,1);else{this.#T=o;ot===e[s])))return ColorManager._colorsMapping.get(t);return e}getHexCode(t){const e=this._colors.get(t);return e?n.Util.makeHexColor(...e):t}}e.ColorManager=ColorManager;class AnnotationEditorUIManager{#k=null;#F=new Map;#R=new Map;#M=null;#D=new CommandManager;#I=0;#O=null;#L=new Set;#N=null;#j=new IdManager;#U=!1;#B=n.AnnotationEditorType.NONE;#q=new Set;#W=this.copy.bind(this);#H=this.cut.bind(this);#G=this.paste.bind(this);#z=this.keydown.bind(this);#V=this.onEditingAction.bind(this);#X=this.onPageChanging.bind(this);#$=this.onScaleChanging.bind(this);#Y=this.onRotationChanging.bind(this);#K={isEditing:!1,isEmpty:!0,hasSomethingToUndo:!1,hasSomethingToRedo:!1,hasSelectedEditor:!1};#J=null;static _keyboardManager=new KeyboardManager([[["ctrl+a","mac+meta+a"],AnnotationEditorUIManager.prototype.selectAll],[["ctrl+z","mac+meta+z"],AnnotationEditorUIManager.prototype.undo],[["ctrl+y","ctrl+shift+Z","mac+meta+shift+Z"],AnnotationEditorUIManager.prototype.redo],[["Backspace","alt+Backspace","ctrl+Backspace","shift+Backspace","mac+Backspace","mac+alt+Backspace","mac+ctrl+Backspace","Delete","ctrl+Delete","shift+Delete"],AnnotationEditorUIManager.prototype.delete],[["Escape","mac+Escape"],AnnotationEditorUIManager.prototype.unselectAll]]);constructor(t,e,s){this.#J=t;this.#N=e;this.#N._on("editingaction",this.#V);this.#N._on("pagechanging",this.#X);this.#N._on("scalechanging",this.#$);this.#N._on("rotationchanging",this.#Y);this.#M=s;this.viewParameters={realScale:i.PixelsPerInch.PDF_TO_CSS_UNITS,rotation:0}}destroy(){this.#Q();this.#N._off("editingaction",this.#V);this.#N._off("pagechanging",this.#X);this.#N._off("scalechanging",this.#$);this.#N._off("rotationchanging",this.#Y);for(const t of this.#R.values())t.destroy();this.#R.clear();this.#F.clear();this.#L.clear();this.#k=null;this.#q.clear();this.#D.destroy()}onPageChanging({pageNumber:t}){this.#I=t-1}focusMainContainer(){this.#J.focus()}addShouldRescale(t){this.#L.add(t)}removeShouldRescale(t){this.#L.delete(t)}onScaleChanging({scale:t}){this.commitOrRemove();this.viewParameters.realScale=t*i.PixelsPerInch.PDF_TO_CSS_UNITS;for(const t of this.#L)t.onScaleChanging()}onRotationChanging({pagesRotation:t}){this.commitOrRemove();this.viewParameters.rotation=t}addToAnnotationStorage(t){t.isEmpty()||!this.#M||this.#M.has(t.id)||this.#M.setValue(t.id,t)}#Z(){this.#J.addEventListener("keydown",this.#z)}#Q(){this.#J.removeEventListener("keydown",this.#z)}#tt(){document.addEventListener("copy",this.#W);document.addEventListener("cut",this.#H);document.addEventListener("paste",this.#G)}#et(){document.removeEventListener("copy",this.#W);document.removeEventListener("cut",this.#H);document.removeEventListener("paste",this.#G)}copy(t){t.preventDefault();this.#k&&this.#k.commitOrRemove();if(!this.hasSelection)return;const e=[];for(const t of this.#q)t.isEmpty()||e.push(t.serialize());0!==e.length&&t.clipboardData.setData("application/pdfjs",JSON.stringify(e))}cut(t){this.copy(t);this.delete()}paste(t){t.preventDefault();let e=t.clipboardData.getData("application/pdfjs");if(!e)return;try{e=JSON.parse(e)}catch(t){(0,n.warn)(`paste: "${t.message}".`);return}if(!Array.isArray(e))return;this.unselectAll();const s=this.#R.get(this.#I);try{const t=[];for(const n of e){const e=s.deserialize(n);if(!e)return;t.push(e)}const cmd=()=>{for(const e of t)this.#st(e);this.#nt(t)},undo=()=>{for(const e of t)e.remove()};this.addCommands({cmd:cmd,undo:undo,mustExec:!0})}catch(t){(0,n.warn)(`paste: "${t.message}".`)}}keydown(t){this.getActive()?.shouldGetKeyboardEvents()||AnnotationEditorUIManager._keyboardManager.exec(this,t)}onEditingAction(t){["undo","redo","delete","selectAll"].includes(t.name)&&this[t.name]()}#it(t){Object.entries(t).some((([t,e])=>this.#K[t]!==e))&&this.#N.dispatch("annotationeditorstateschanged",{source:this,details:Object.assign(this.#K,t)})}#at(t){this.#N.dispatch("annotationeditorparamschanged",{source:this,details:t})}setEditingState(t){if(t){this.#Z();this.#tt();this.#it({isEditing:this.#B!==n.AnnotationEditorType.NONE,isEmpty:this.#rt(),hasSomethingToUndo:this.#D.hasSomethingToUndo(),hasSomethingToRedo:this.#D.hasSomethingToRedo(),hasSelectedEditor:!1})}else{this.#Q();this.#et();this.#it({isEditing:!1})}}registerEditorTypes(t){if(!this.#O){this.#O=t;for(const t of this.#O)this.#at(t.defaultPropertiesToUpdate)}}getId(){return this.#j.getId()}get currentLayer(){return this.#R.get(this.#I)}get currentPageIndex(){return this.#I}addLayer(t){this.#R.set(t.pageIndex,t);this.#U?t.enable():t.disable()}removeLayer(t){this.#R.delete(t.pageIndex)}updateMode(t){this.#B=t;if(t===n.AnnotationEditorType.NONE){this.setEditingState(!1);this.#ot()}else{this.setEditingState(!0);this.#lt();for(const e of this.#R.values())e.updateMode(t)}}updateToolbar(t){t!==this.#B&&this.#N.dispatch("switchannotationeditormode",{source:this,mode:t})}updateParams(t,e){if(this.#O){for(const s of this.#q)s.updateParams(t,e);for(const s of this.#O)s.updateDefaultParams(t,e)}}#lt(){if(!this.#U){this.#U=!0;for(const t of this.#R.values())t.enable()}}#ot(){this.unselectAll();if(this.#U){this.#U=!1;for(const t of this.#R.values())t.disable()}}getEditors(t){const e=[];for(const s of this.#F.values())s.pageIndex===t&&e.push(s);return e}getEditor(t){return this.#F.get(t)}addEditor(t){this.#F.set(t.id,t)}removeEditor(t){this.#F.delete(t.id);this.unselect(t);this.#M?.remove(t.id)}#st(t){const e=this.#R.get(t.pageIndex);e?e.addOrRebuild(t):this.addEditor(t)}setActiveEditor(t){if(this.#k!==t){this.#k=t;t&&this.#at(t.propertiesToUpdate)}}toggleSelected(t){if(this.#q.has(t)){this.#q.delete(t);t.unselect();this.#it({hasSelectedEditor:this.hasSelection})}else{this.#q.add(t);t.select();this.#at(t.propertiesToUpdate);this.#it({hasSelectedEditor:!0})}}setSelected(t){for(const e of this.#q)e!==t&&e.unselect();this.#q.clear();this.#q.add(t);t.select();this.#at(t.propertiesToUpdate);this.#it({hasSelectedEditor:!0})}isSelected(t){return this.#q.has(t)}unselect(t){t.unselect();this.#q.delete(t);this.#it({hasSelectedEditor:this.hasSelection})}get hasSelection(){return 0!==this.#q.size}undo(){this.#D.undo();this.#it({hasSomethingToUndo:this.#D.hasSomethingToUndo(),hasSomethingToRedo:!0,isEmpty:this.#rt()})}redo(){this.#D.redo();this.#it({hasSomethingToUndo:!0,hasSomethingToRedo:this.#D.hasSomethingToRedo(),isEmpty:this.#rt()})}addCommands(t){this.#D.add(t);this.#it({hasSomethingToUndo:!0,hasSomethingToRedo:!1,isEmpty:this.#rt()})}#rt(){if(0===this.#F.size)return!0;if(1===this.#F.size)for(const t of this.#F.values())return t.isEmpty();return!1}delete(){this.commitOrRemove();if(!this.hasSelection)return;const t=[...this.#q];this.addCommands({cmd:()=>{for(const e of t)e.remove()},undo:()=>{for(const e of t)this.#st(e)},mustExec:!0})}commitOrRemove(){this.#k?.commitOrRemove()}#nt(t){this.#q.clear();for(const e of t)if(!e.isEmpty()){this.#q.add(e);e.select()}this.#it({hasSelectedEditor:!0})}selectAll(){for(const t of this.#q)t.commit();this.#nt(this.#F.values())}unselectAll(){if(this.#k)this.#k.commitOrRemove();else if(0!==this.#q.size){for(const t of this.#q)t.unselect();this.#q.clear();this.#it({hasSelectedEditor:!1})}}isActive(t){return this.#k===t}getActive(){return this.#k}getMode(){return this.#B}}e.AnnotationEditorUIManager=AnnotationEditorUIManager},(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0});e.StatTimer=e.RenderingCancelledException=e.PixelsPerInch=e.PageViewport=e.PDFDateString=e.DOMStandardFontDataFactory=e.DOMSVGFactory=e.DOMCanvasFactory=e.DOMCMapReaderFactory=e.AnnotationPrefix=void 0;e.deprecated=function deprecated(t){console.log("Deprecated API usage: "+t)};e.getColorValues=function getColorValues(t){const e=document.createElement("span");e.style.visibility="hidden";document.body.append(e);for(const s of t.keys()){e.style.color=s;const n=window.getComputedStyle(e).color;t.set(s,getRGB(n))}e.remove()};e.getCurrentTransform=function getCurrentTransform(t){const{a:e,b:s,c:n,d:i,e:a,f:r}=t.getTransform();return[e,s,n,i,a,r]};e.getCurrentTransformInverse=function getCurrentTransformInverse(t){const{a:e,b:s,c:n,d:i,e:a,f:r}=t.getTransform().invertSelf();return[e,s,n,i,a,r]};e.getFilenameFromUrl=function getFilenameFromUrl(t,e=!1){e||([t]=t.split(/[#?]/,1));return t.substring(t.lastIndexOf("/")+1)};e.getPdfFilenameFromUrl=function getPdfFilenameFromUrl(t,e="document.pdf"){if("string"!=typeof t)return e;if(isDataScheme(t)){(0,i.warn)('getPdfFilenameFromUrl: ignore "data:"-URL for performance reasons.');return e}const s=/[^/?#=]+\.pdf\b(?!.*\.pdf\b)/i,n=/^(?:(?:[^:]+:)?\/\/[^/]+)?([^?#]*)(\?[^#]*)?(#.*)?$/.exec(t);let a=s.exec(n[1])||s.exec(n[2])||s.exec(n[3]);if(a){a=a[0];if(a.includes("%"))try{a=s.exec(decodeURIComponent(a))[0]}catch(t){}}return a||e};e.getRGB=getRGB;e.getXfaPageViewport=function getXfaPageViewport(t,{scale:e=1,rotation:s=0}){const{width:n,height:i}=t.attributes.style,a=[0,0,parseInt(n),parseInt(i)];return new PageViewport({viewBox:a,scale:e,rotation:s})};e.isDataScheme=isDataScheme;e.isPdfFile=function isPdfFile(t){return"string"==typeof t&&/\.pdf$/i.test(t)};e.isValidFetchUrl=isValidFetchUrl;e.loadScript=function loadScript(t,e=!1){return new Promise(((s,n)=>{const i=document.createElement("script");i.src=t;i.onload=function(t){e&&i.remove();s(t)};i.onerror=function(){n(new Error(`Cannot load script at: ${i.src}`))};(document.head||document.documentElement).append(i)}))};e.setLayerDimensions=function setLayerDimensions(t,e,s=!1,n=!0){if(e instanceof PageViewport){const{pageWidth:n,pageHeight:i}=e.rawDims,{style:a}=t,r=`calc(var(--scale-factor) * ${n}px)`,o=`calc(var(--scale-factor) * ${i}px)`;if(s&&e.rotation%180!=0){a.width=o;a.height=r}else{a.width=r;a.height=o}}n&&t.setAttribute("data-main-rotation",e.rotation)};var n=s(7),i=s(1);e.AnnotationPrefix="pdfjs_internal_id_";class PixelsPerInch{static CSS=96;static PDF=72;static PDF_TO_CSS_UNITS=this.CSS/this.PDF}e.PixelsPerInch=PixelsPerInch;class DOMCanvasFactory extends n.BaseCanvasFactory{constructor({ownerDocument:t=globalThis.document}={}){super();this._document=t}_createCanvas(t,e){const s=this._document.createElement("canvas");s.width=t;s.height=e;return s}}e.DOMCanvasFactory=DOMCanvasFactory;async function fetchData(t,e=!1){if(isValidFetchUrl(t,document.baseURI)){const s=await fetch(t);if(!s.ok)throw new Error(s.statusText);return e?new Uint8Array(await s.arrayBuffer()):(0,i.stringToBytes)(await s.text())}return new Promise(((s,n)=>{const a=new XMLHttpRequest;a.open("GET",t,!0);e&&(a.responseType="arraybuffer");a.onreadystatechange=()=>{if(a.readyState===XMLHttpRequest.DONE){if(200===a.status||0===a.status){let t;e&&a.response?t=new Uint8Array(a.response):!e&&a.responseText&&(t=(0,i.stringToBytes)(a.responseText));if(t){s(t);return}}n(new Error(a.statusText))}};a.send(null)}))}class DOMCMapReaderFactory extends n.BaseCMapReaderFactory{_fetchData(t,e){return fetchData(t,this.isCompressed).then((t=>({cMapData:t,compressionType:e})))}}e.DOMCMapReaderFactory=DOMCMapReaderFactory;class DOMStandardFontDataFactory extends n.BaseStandardFontDataFactory{_fetchData(t){return fetchData(t,!0)}}e.DOMStandardFontDataFactory=DOMStandardFontDataFactory;class DOMSVGFactory extends n.BaseSVGFactory{_createSVG(t){return document.createElementNS("http://www.w3.org/2000/svg",t)}}e.DOMSVGFactory=DOMSVGFactory;class PageViewport{constructor({viewBox:t,scale:e,rotation:s,offsetX:n=0,offsetY:i=0,dontFlip:a=!1}){this.viewBox=t;this.scale=e;this.rotation=s;this.offsetX=n;this.offsetY=i;const r=(t[2]+t[0])/2,o=(t[3]+t[1])/2;let l,c,h,d,u,p,g,m;(s%=360)<0&&(s+=360);switch(s){case 180:l=-1;c=0;h=0;d=1;break;case 90:l=0;c=1;h=1;d=0;break;case 270:l=0;c=-1;h=-1;d=0;break;case 0:l=1;c=0;h=0;d=-1;break;default:throw new Error("PageViewport: Invalid rotation, must be a multiple of 90 degrees.")}if(a){h=-h;d=-d}if(0===l){u=Math.abs(o-t[1])*e+n;p=Math.abs(r-t[0])*e+i;g=(t[3]-t[1])*e;m=(t[2]-t[0])*e}else{u=Math.abs(r-t[0])*e+n;p=Math.abs(o-t[1])*e+i;g=(t[2]-t[0])*e;m=(t[3]-t[1])*e}this.transform=[l*e,c*e,h*e,d*e,u-l*e*r-h*e*o,p-c*e*r-d*e*o];this.width=g;this.height=m}get rawDims(){const{viewBox:t}=this;return(0,i.shadow)(this,"rawDims",{pageWidth:t[2]-t[0],pageHeight:t[3]-t[1],pageX:t[0],pageY:t[1]})}clone({scale:t=this.scale,rotation:e=this.rotation,offsetX:s=this.offsetX,offsetY:n=this.offsetY,dontFlip:i=!1}={}){return new PageViewport({viewBox:this.viewBox.slice(),scale:t,rotation:e,offsetX:s,offsetY:n,dontFlip:i})}convertToViewportPoint(t,e){return i.Util.applyTransform([t,e],this.transform)}convertToViewportRectangle(t){const e=i.Util.applyTransform([t[0],t[1]],this.transform),s=i.Util.applyTransform([t[2],t[3]],this.transform);return[e[0],e[1],s[0],s[1]]}convertToPdfPoint(t,e){return i.Util.applyInverseTransform([t,e],this.transform)}}e.PageViewport=PageViewport;class RenderingCancelledException extends i.BaseException{constructor(t,e,s=0){super(t,"RenderingCancelledException");this.type=e;this.extraDelay=s}}e.RenderingCancelledException=RenderingCancelledException;function isDataScheme(t){const e=t.length;let s=0;for(;s=1&&n<=12?n-1:0;let i=parseInt(e[3],10);i=i>=1&&i<=31?i:1;let r=parseInt(e[4],10);r=r>=0&&r<=23?r:0;let o=parseInt(e[5],10);o=o>=0&&o<=59?o:0;let l=parseInt(e[6],10);l=l>=0&&l<=59?l:0;const c=e[7]||"Z";let h=parseInt(e[8],10);h=h>=0&&h<=23?h:0;let d=parseInt(e[9],10)||0;d=d>=0&&d<=59?d:0;if("-"===c){r+=h;o+=d}else if("+"===c){r-=h;o-=d}return new Date(Date.UTC(s,n,i,r,o,l))}};function getRGB(t){if(t.startsWith("#")){const e=parseInt(t.slice(1),16);return[(16711680&e)>>16,(65280&e)>>8,255&e]}if(t.startsWith("rgb("))return t.slice(4,-1).split(",").map((t=>parseInt(t)));if(t.startsWith("rgba("))return t.slice(5,-1).split(",").map((t=>parseInt(t))).slice(0,3);(0,i.warn)(`Not a valid color format: "${t}"`);return[0,0,0]}},(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0});e.BaseStandardFontDataFactory=e.BaseSVGFactory=e.BaseCanvasFactory=e.BaseCMapReaderFactory=void 0;var n=s(1);class BaseCanvasFactory{constructor(){this.constructor===BaseCanvasFactory&&(0,n.unreachable)("Cannot initialize BaseCanvasFactory.")}create(t,e){if(t<=0||e<=0)throw new Error("Invalid canvas size");const s=this._createCanvas(t,e);return{canvas:s,context:s.getContext("2d")}}reset(t,e,s){if(!t.canvas)throw new Error("Canvas is not specified");if(e<=0||s<=0)throw new Error("Invalid canvas size");t.canvas.width=e;t.canvas.height=s}destroy(t){if(!t.canvas)throw new Error("Canvas is not specified");t.canvas.width=0;t.canvas.height=0;t.canvas=null;t.context=null}_createCanvas(t,e){(0,n.unreachable)("Abstract method `_createCanvas` called.")}}e.BaseCanvasFactory=BaseCanvasFactory;class BaseCMapReaderFactory{constructor({baseUrl:t=null,isCompressed:e=!1}){this.constructor===BaseCMapReaderFactory&&(0,n.unreachable)("Cannot initialize BaseCMapReaderFactory.");this.baseUrl=t;this.isCompressed=e}async fetch({name:t}){if(!this.baseUrl)throw new Error('The CMap "baseUrl" parameter must be specified, ensure that the "cMapUrl" and "cMapPacked" API parameters are provided.');if(!t)throw new Error("CMap name must be specified.");const e=this.baseUrl+t+(this.isCompressed?".bcmap":""),s=this.isCompressed?n.CMapCompressionType.BINARY:n.CMapCompressionType.NONE;return this._fetchData(e,s).catch((t=>{throw new Error(`Unable to load ${this.isCompressed?"binary ":""}CMap at: ${e}`)}))}_fetchData(t,e){(0,n.unreachable)("Abstract method `_fetchData` called.")}}e.BaseCMapReaderFactory=BaseCMapReaderFactory;class BaseStandardFontDataFactory{constructor({baseUrl:t=null}){this.constructor===BaseStandardFontDataFactory&&(0,n.unreachable)("Cannot initialize BaseStandardFontDataFactory.");this.baseUrl=t}async fetch({filename:t}){if(!this.baseUrl)throw new Error('The standard font "baseUrl" parameter must be specified, ensure that the "standardFontDataUrl" API parameter is provided.');if(!t)throw new Error("Font filename must be specified.");const e=`${this.baseUrl}${t}`;return this._fetchData(e).catch((t=>{throw new Error(`Unable to load font data at: ${e}`)}))}_fetchData(t){(0,n.unreachable)("Abstract method `_fetchData` called.")}}e.BaseStandardFontDataFactory=BaseStandardFontDataFactory;class BaseSVGFactory{constructor(){this.constructor===BaseSVGFactory&&(0,n.unreachable)("Cannot initialize BaseSVGFactory.")}create(t,e,s=!1){if(t<=0||e<=0)throw new Error("Invalid SVG dimensions");const n=this._createSVG("svg:svg");n.setAttribute("version","1.1");if(!s){n.setAttribute("width",`${t}px`);n.setAttribute("height",`${e}px`)}n.setAttribute("preserveAspectRatio","none");n.setAttribute("viewBox",`0 0 ${t} ${e}`);return n}createElement(t){if("string"!=typeof t)throw new Error("Invalid SVG element type");return this._createSVG(t)}_createSVG(t){(0,n.unreachable)("Abstract method `_createSVG` called.")}}e.BaseSVGFactory=BaseSVGFactory},(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0});e.MurmurHash3_64=void 0;var n=s(1);const i=3285377520,a=4294901760,r=65535;e.MurmurHash3_64=class MurmurHash3_64{constructor(t){this.h1=t?4294967295&t:i;this.h2=t?4294967295&t:i}update(t){let e,s;if("string"==typeof t){e=new Uint8Array(2*t.length);s=0;for(let n=0,i=t.length;n>>8;e[s++]=255&i}}}else{if(!(0,n.isArrayBuffer)(t))throw new Error("Wrong data format in MurmurHash3_64_update. Input must be a string or array.");e=t.slice();s=e.byteLength}const i=s>>2,o=s-4*i,l=new Uint32Array(e.buffer,0,i);let c=0,h=0,d=this.h1,u=this.h2;const p=3432918353,g=461845907,m=11601,f=13715;for(let t=0;t>>17;c=c*g&a|c*f&r;d^=c;d=d<<13|d>>>19;d=5*d+3864292196}else{h=l[t];h=h*p&a|h*m&r;h=h<<15|h>>>17;h=h*g&a|h*f&r;u^=h;u=u<<13|u>>>19;u=5*u+3864292196}c=0;switch(o){case 3:c^=e[4*i+2]<<16;case 2:c^=e[4*i+1]<<8;case 1:c^=e[4*i];c=c*p&a|c*m&r;c=c<<15|c>>>17;c=c*g&a|c*f&r;1&i?d^=c:u^=c}this.h1=d;this.h2=u}hexdigest(){let t=this.h1,e=this.h2;t^=e>>>1;t=3981806797*t&a|36045*t&r;e=4283543511*e&a|(2950163797*(e<<16|t>>>16)&a)>>>16;t^=e>>>1;t=444984403*t&a|60499*t&r;e=3301882366*e&a|(3120437893*(e<<16|t>>>16)&a)>>>16;t^=e>>>1;return(t>>>0).toString(16).padStart(8,"0")+(e>>>0).toString(16).padStart(8,"0")}}},(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0});e.FontLoader=e.FontFaceObject=void 0;var n=s(1),i=s(10);e.FontLoader=class FontLoader{constructor({onUnsupportedFeature:t,ownerDocument:e=globalThis.document,styleElement:s=null}){this._onUnsupportedFeature=t;this._document=e;this.nativeFontFaces=[];this.styleElement=null;this.loadingRequests=[];this.loadTestFontId=0}addNativeFontFace(t){this.nativeFontFaces.push(t);this._document.fonts.add(t)}insertRule(t){if(!this.styleElement){this.styleElement=this._document.createElement("style");this._document.documentElement.getElementsByTagName("head")[0].append(this.styleElement)}const e=this.styleElement.sheet;e.insertRule(t,e.cssRules.length)}clear(){for(const t of this.nativeFontFaces)this._document.fonts.delete(t);this.nativeFontFaces.length=0;if(this.styleElement){this.styleElement.remove();this.styleElement=null}}async bind(t){if(t.attached||t.missingFile)return;t.attached=!0;if(this.isFontLoadingAPISupported){const e=t.createNativeFontFace();if(e){this.addNativeFontFace(e);try{await e.loaded}catch(s){this._onUnsupportedFeature({featureId:n.UNSUPPORTED_FEATURES.errorFontLoadNative});(0,n.warn)(`Failed to load font '${e.family}': '${s}'.`);t.disableFontFace=!0;throw s}}return}const e=t.createFontFaceRule();if(e){this.insertRule(e);if(this.isSyncFontLoadingSupported)return;await new Promise((e=>{const s=this._queueLoadingCallback(e);this._prepareFontLoadEvent(t,s)}))}}get isFontLoadingAPISupported(){const t=!!this._document?.fonts;return(0,n.shadow)(this,"isFontLoadingAPISupported",t)}get isSyncFontLoadingSupported(){let t=!1;(i.isNodeJS||"undefined"!=typeof navigator&&/Mozilla\/5.0.*?rv:\d+.*? Gecko/.test(navigator.userAgent))&&(t=!0);return(0,n.shadow)(this,"isSyncFontLoadingSupported",t)}_queueLoadingCallback(t){const{loadingRequests:e}=this,s={done:!1,complete:function completeRequest(){(0,n.assert)(!s.done,"completeRequest() cannot be called twice.");s.done=!0;for(;e.length>0&&e[0].done;){const t=e.shift();setTimeout(t.callback,0)}},callback:t};e.push(s);return s}get _loadTestFont(){const t=atob("T1RUTwALAIAAAwAwQ0ZGIDHtZg4AAAOYAAAAgUZGVE1lkzZwAAAEHAAAABxHREVGABQAFQAABDgAAAAeT1MvMlYNYwkAAAEgAAAAYGNtYXABDQLUAAACNAAAAUJoZWFk/xVFDQAAALwAAAA2aGhlYQdkA+oAAAD0AAAAJGhtdHgD6AAAAAAEWAAAAAZtYXhwAAJQAAAAARgAAAAGbmFtZVjmdH4AAAGAAAAAsXBvc3T/hgAzAAADeAAAACAAAQAAAAEAALZRFsRfDzz1AAsD6AAAAADOBOTLAAAAAM4KHDwAAAAAA+gDIQAAAAgAAgAAAAAAAAABAAADIQAAAFoD6AAAAAAD6AABAAAAAAAAAAAAAAAAAAAAAQAAUAAAAgAAAAQD6AH0AAUAAAKKArwAAACMAooCvAAAAeAAMQECAAACAAYJAAAAAAAAAAAAAQAAAAAAAAAAAAAAAFBmRWQAwAAuAC4DIP84AFoDIQAAAAAAAQAAAAAAAAAAACAAIAABAAAADgCuAAEAAAAAAAAAAQAAAAEAAAAAAAEAAQAAAAEAAAAAAAIAAQAAAAEAAAAAAAMAAQAAAAEAAAAAAAQAAQAAAAEAAAAAAAUAAQAAAAEAAAAAAAYAAQAAAAMAAQQJAAAAAgABAAMAAQQJAAEAAgABAAMAAQQJAAIAAgABAAMAAQQJAAMAAgABAAMAAQQJAAQAAgABAAMAAQQJAAUAAgABAAMAAQQJAAYAAgABWABYAAAAAAAAAwAAAAMAAAAcAAEAAAAAADwAAwABAAAAHAAEACAAAAAEAAQAAQAAAC7//wAAAC7////TAAEAAAAAAAABBgAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAD/gwAyAAAAAQAAAAAAAAAAAAAAAAAAAAABAAQEAAEBAQJYAAEBASH4DwD4GwHEAvgcA/gXBIwMAYuL+nz5tQXkD5j3CBLnEQACAQEBIVhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYAAABAQAADwACAQEEE/t3Dov6fAH6fAT+fPp8+nwHDosMCvm1Cvm1DAz6fBQAAAAAAAABAAAAAMmJbzEAAAAAzgTjFQAAAADOBOQpAAEAAAAAAAAADAAUAAQAAAABAAAAAgABAAAAAAAAAAAD6AAAAAAAAA==");return(0,n.shadow)(this,"_loadTestFont",t)}_prepareFontLoadEvent(t,e){function int32(t,e){return t.charCodeAt(e)<<24|t.charCodeAt(e+1)<<16|t.charCodeAt(e+2)<<8|255&t.charCodeAt(e+3)}function spliceString(t,e,s,n){return t.substring(0,e)+n+t.substring(e+s)}let s,i;const a=this._document.createElement("canvas");a.width=1;a.height=1;const r=a.getContext("2d");let o=0;const l=`lt${Date.now()}${this.loadTestFontId++}`;let c=this._loadTestFont;c=spliceString(c,976,l.length,l);const h=1482184792;let d=int32(c,16);for(s=0,i=l.length-3;s30){(0,n.warn)("Load test font never loaded.");e();return}r.font="30px "+t;r.fillText(".",0,20);r.getImageData(0,0,1,1).data[3]>0?e():setTimeout(isFontReady.bind(null,t,e))}(l,(()=>{p.remove();e.complete()}))}};e.FontFaceObject=class FontFaceObject{constructor(t,{isEvalSupported:e=!0,disableFontFace:s=!1,ignoreErrors:n=!1,onUnsupportedFeature:i,fontRegistry:a=null}){this.compiledGlyphs=Object.create(null);for(const e in t)this[e]=t[e];this.isEvalSupported=!1!==e;this.disableFontFace=!0===s;this.ignoreErrors=!0===n;this._onUnsupportedFeature=i;this.fontRegistry=a}createNativeFontFace(){if(!this.data||this.disableFontFace)return null;let t;if(this.cssFontInfo){const e={weight:this.cssFontInfo.fontWeight};this.cssFontInfo.italicAngle&&(e.style=`oblique ${this.cssFontInfo.italicAngle}deg`);t=new FontFace(this.cssFontInfo.fontFamily,this.data,e)}else t=new FontFace(this.loadedName,this.data,{});this.fontRegistry?.registerFont(this);return t}createFontFaceRule(){if(!this.data||this.disableFontFace)return null;const t=(0,n.bytesToString)(this.data),e=`url(data:${this.mimetype};base64,${btoa(t)});`;let s;if(this.cssFontInfo){let t=`font-weight: ${this.cssFontInfo.fontWeight};`;this.cssFontInfo.italicAngle&&(t+=`font-style: oblique ${this.cssFontInfo.italicAngle}deg;`);s=`@font-face {font-family:"${this.cssFontInfo.fontFamily}";${t}src:${e}}`}else s=`@font-face {font-family:"${this.loadedName}";src:${e}}`;this.fontRegistry?.registerFont(this,e);return s}getPathGenerator(t,e){if(void 0!==this.compiledGlyphs[e])return this.compiledGlyphs[e];let s;try{s=t.get(this.loadedName+"_path_"+e)}catch(t){if(!this.ignoreErrors)throw t;this._onUnsupportedFeature({featureId:n.UNSUPPORTED_FEATURES.errorFontGetPath});(0,n.warn)(`getPathGenerator - ignoring character: "${t}".`);return this.compiledGlyphs[e]=function(t,e){}}if(this.isEvalSupported&&n.FeatureTest.isEvalSupported){const t=[];for(const e of s){const s=void 0!==e.args?e.args.join(","):"";t.push("c.",e.cmd,"(",s,");\n")}return this.compiledGlyphs[e]=new Function("c","size",t.join(""))}return this.compiledGlyphs[e]=function(t,e){for(const n of s){"scale"===n.cmd&&(n.args=[e,-e]);t[n.cmd].apply(t,n.args)}}}}},(t,e)=>{Object.defineProperty(e,"__esModule",{value:!0});e.isNodeJS=void 0;const s=!("object"!=typeof process||process+""!="[object process]"||process.versions.nw||process.versions.electron&&process.type&&"browser"!==process.type);e.isNodeJS=s},(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0});e.CanvasGraphics=void 0;var n=s(1),i=s(6),a=s(12),r=s(13),o=s(10);const l=4096,c=o.isNodeJS&&"undefined"==typeof Path2D?-1:1e3,h=16;class CachedCanvases{constructor(t){this.canvasFactory=t;this.cache=Object.create(null)}getCanvas(t,e,s){let n;if(void 0!==this.cache[t]){n=this.cache[t];this.canvasFactory.reset(n,e,s)}else{n=this.canvasFactory.create(e,s);this.cache[t]=n}return n}delete(t){delete this.cache[t]}clear(){for(const t in this.cache){const e=this.cache[t];this.canvasFactory.destroy(e);delete this.cache[t]}}}function drawImageAtIntegerCoords(t,e,s,n,a,r,o,l,c,h){const[d,u,p,g,m,f]=(0,i.getCurrentTransform)(t);if(0===u&&0===p){const i=o*d+m,b=Math.round(i),A=l*g+f,_=Math.round(A),y=(o+c)*d+m,v=Math.abs(Math.round(y)-b)||1,S=(l+h)*g+f,x=Math.abs(Math.round(S)-_)||1;t.setTransform(Math.sign(d),0,0,Math.sign(g),b,_);t.drawImage(e,s,n,a,r,0,0,v,x);t.setTransform(d,u,p,g,m,f);return[v,x]}if(0===d&&0===g){const i=l*p+m,b=Math.round(i),A=o*u+f,_=Math.round(A),y=(l+h)*p+m,v=Math.abs(Math.round(y)-b)||1,S=(o+c)*u+f,x=Math.abs(Math.round(S)-_)||1;t.setTransform(0,Math.sign(u),Math.sign(p),0,b,_);t.drawImage(e,s,n,a,r,0,0,x,v);t.setTransform(d,u,p,g,m,f);return[x,v]}t.drawImage(e,s,n,a,r,o,l,c,h);return[Math.hypot(d,u)*c,Math.hypot(p,g)*h]}class CanvasExtraState{constructor(t,e){this.alphaIsShape=!1;this.fontSize=0;this.fontSizeScale=1;this.textMatrix=n.IDENTITY_MATRIX;this.textMatrixScale=1;this.fontMatrix=n.FONT_IDENTITY_MATRIX;this.leading=0;this.x=0;this.y=0;this.lineX=0;this.lineY=0;this.charSpacing=0;this.wordSpacing=0;this.textHScale=1;this.textRenderingMode=n.TextRenderingMode.FILL;this.textRise=0;this.fillColor="#000000";this.strokeColor="#000000";this.patternFill=!1;this.fillAlpha=1;this.strokeAlpha=1;this.lineWidth=1;this.activeSMask=null;this.transferMaps=null;this.startNewPathAndClipBox([0,0,t,e])}clone(){const t=Object.create(this);t.clipBox=this.clipBox.slice();return t}setCurrentPoint(t,e){this.x=t;this.y=e}updatePathMinMax(t,e,s){[e,s]=n.Util.applyTransform([e,s],t);this.minX=Math.min(this.minX,e);this.minY=Math.min(this.minY,s);this.maxX=Math.max(this.maxX,e);this.maxY=Math.max(this.maxY,s)}updateRectMinMax(t,e){const s=n.Util.applyTransform(e,t),i=n.Util.applyTransform(e.slice(2),t);this.minX=Math.min(this.minX,s[0],i[0]);this.minY=Math.min(this.minY,s[1],i[1]);this.maxX=Math.max(this.maxX,s[0],i[0]);this.maxY=Math.max(this.maxY,s[1],i[1])}updateScalingPathMinMax(t,e){n.Util.scaleMinMax(t,e);this.minX=Math.min(this.minX,e[0]);this.maxX=Math.max(this.maxX,e[1]);this.minY=Math.min(this.minY,e[2]);this.maxY=Math.max(this.maxY,e[3])}updateCurvePathMinMax(t,e,s,i,a,r,o,l,c,h){const d=n.Util.bezierBoundingBox(e,s,i,a,r,o,l,c);if(h){h[0]=Math.min(h[0],d[0],d[2]);h[1]=Math.max(h[1],d[0],d[2]);h[2]=Math.min(h[2],d[1],d[3]);h[3]=Math.max(h[3],d[1],d[3])}else this.updateRectMinMax(t,d)}getPathBoundingBox(t=a.PathType.FILL,e=null){const s=[this.minX,this.minY,this.maxX,this.maxY];if(t===a.PathType.STROKE){e||(0,n.unreachable)("Stroke bounding box must include transform.");const t=n.Util.singularValueDecompose2dScale(e),i=t[0]*this.lineWidth/2,a=t[1]*this.lineWidth/2;s[0]-=i;s[1]-=a;s[2]+=i;s[3]+=a}return s}updateClipFromPath(){const t=n.Util.intersect(this.clipBox,this.getPathBoundingBox());this.startNewPathAndClipBox(t||[0,0,0,0])}isEmptyClip(){return this.minX===1/0}startNewPathAndClipBox(t){this.clipBox=t;this.minX=1/0;this.minY=1/0;this.maxX=0;this.maxY=0}getClippedPathBoundingBox(t=a.PathType.FILL,e=null){return n.Util.intersect(this.clipBox,this.getPathBoundingBox(t,e))}}function putBinaryImageData(t,e,s=null){if("undefined"!=typeof ImageData&&e instanceof ImageData){t.putImageData(e,0,0);return}const i=e.height,a=e.width,r=i%h,o=(i-r)/h,l=0===r?o:o+1,c=t.createImageData(a,h);let d,u=0;const p=e.data,g=c.data;let m,f,b,A,_,y,v,S;if(s)switch(s.length){case 1:_=s[0];y=s[0];v=s[0];S=s[0];break;case 4:_=s[0];y=s[1];v=s[2];S=s[3]}if(e.kind===n.ImageKind.GRAYSCALE_1BPP){const e=p.byteLength,s=new Uint32Array(g.buffer,0,g.byteLength>>2),i=s.length,A=a+7>>3;let _=4294967295,y=n.FeatureTest.isLittleEndian?4278190080:255;S&&255===S[0]&&0===S[255]&&([_,y]=[y,_]);for(m=0;mA?a:8*t-7,r=-8&i;let o=0,l=0;for(;n>=1}}for(;d=o){b=r;A=a*b}d=0;for(f=A;f--;){g[d++]=p[u++];g[d++]=p[u++];g[d++]=p[u++];g[d++]=255}if(e)for(let t=0;t>8;t[a-2]=t[a-2]*i+s*r>>8;t[a-1]=t[a-1]*i+n*r>>8}}}function composeSMaskAlpha(t,e,s){const n=t.length;for(let i=3;i>8]>>8:e[i]*n>>16}}function composeSMask(t,e,s,n){const i=n[0],a=n[1],r=n[2]-i,o=n[3]-a;if(0!==r&&0!==o){!function genericComposeSMask(t,e,s,n,i,a,r,o,l,c,h){const d=!!a,u=d?a[0]:0,p=d?a[1]:0,g=d?a[2]:0;let m;m="Luminosity"===i?composeSMaskLuminosity:composeSMaskAlpha;const f=Math.min(n,Math.ceil(1048576/s));for(let i=0;i(t/=255)<=.03928?t/12.92:((t+.055)/1.055)**2.4,o=Math.round(.2126*newComp(s)+.7152*newComp(a)+.0722*newComp(r));this.selectColor=(s,n,i)=>{const a=.2126*newComp(s)+.7152*newComp(n)+.0722*newComp(i);return Math.round(a)===o?e:t}}}this.ctx.fillStyle=this.backgroundColor||o;this.ctx.fillRect(0,0,a,r);this.ctx.restore();if(s){const t=this.cachedCanvases.getCanvas("transparent",a,r);this.compositeCtx=this.ctx;this.transparentCanvas=t.canvas;this.ctx=t.context;this.ctx.save();this.ctx.transform(...(0,i.getCurrentTransform)(this.compositeCtx))}this.ctx.save();resetCtxToDefault(this.ctx,this.foregroundColor);if(t){this.ctx.transform(...t);this.outputScaleX=t[0];this.outputScaleY=t[0]}this.ctx.transform(...e.transform);this.viewportScale=e.scale;this.baseTransform=(0,i.getCurrentTransform)(this.ctx)}executeOperatorList(t,e,s,i){const a=t.argsArray,r=t.fnArray;let o=e||0;const l=a.length;if(l===o)return o;const c=l-o>10&&"function"==typeof s,h=c?Date.now()+15:0;let d=0;const u=this.commonObjs,p=this.objs;let g;for(;;){if(void 0!==i&&o===i.nextBreakPoint){i.breakIt(o,s);return o}g=r[o];if(g!==n.OPS.dependency)this[g].apply(this,a[o]);else for(const t of a[o]){const e=t.startsWith("g_")?u:p;if(!e.has(t)){e.get(t,s);return o}}o++;if(o===l)return o;if(c&&++d>10){if(Date.now()>h){s();return o}d=0}}}#ct(){for(;this.stateStack.length||this.inSMaskMode;)this.restore();this.ctx.restore();if(this.transparentCanvas){this.ctx=this.compositeCtx;this.ctx.save();this.ctx.setTransform(1,0,0,1,0,0);this.ctx.drawImage(this.transparentCanvas,0,0);this.ctx.restore();this.transparentCanvas=null}}endDrawing(){this.#ct();this.cachedCanvases.clear();this.cachedPatterns.clear();for(const t of this._cachedBitmapsMap.values()){for(const e of t.values())"undefined"!=typeof HTMLCanvasElement&&e instanceof HTMLCanvasElement&&(e.width=e.height=0);t.clear()}this._cachedBitmapsMap.clear()}_scaleImage(t,e){const s=t.width,n=t.height;let i,a,r=Math.max(Math.hypot(e[0],e[1]),1),o=Math.max(Math.hypot(e[2],e[3]),1),l=s,c=n,h="prescale1";for(;r>2&&l>1||o>2&&c>1;){let e=l,s=c;if(r>2&&l>1){e=Math.ceil(l/2);r/=l/e}if(o>2&&c>1){s=Math.ceil(c/2);o/=c/s}i=this.cachedCanvases.getCanvas(h,e,s);a=i.context;a.clearRect(0,0,e,s);a.drawImage(t,0,0,l,c,0,0,e,s);t=i.canvas;l=e;c=s;h="prescale1"===h?"prescale2":"prescale1"}return{img:t,paintWidth:l,paintHeight:c}}_createMaskCanvas(t){const e=this.ctx,{width:s,height:r}=t,o=this.current.fillColor,l=this.current.patternFill,c=(0,i.getCurrentTransform)(e);let h,d,u,p;if((t.bitmap||t.data)&&t.count>1){const e=t.bitmap||t.data.buffer;d=JSON.stringify(l?c:[c.slice(0,4),o]);h=this._cachedBitmapsMap.get(e);if(!h){h=new Map;this._cachedBitmapsMap.set(e,h)}const s=h.get(d);if(s&&!l){return{canvas:s,offsetX:Math.round(Math.min(c[0],c[2])+c[4]),offsetY:Math.round(Math.min(c[1],c[3])+c[5])}}u=s}if(!u){p=this.cachedCanvases.getCanvas("maskCanvas",s,r);putBinaryImageMask(p.context,t)}let g=n.Util.transform(c,[1/s,0,0,-1/r,0,0]);g=n.Util.transform(g,[1,0,0,1,0,-r]);const m=n.Util.applyTransform([0,0],g),f=n.Util.applyTransform([s,r],g),b=n.Util.normalizeRect([m[0],m[1],f[0],f[1]]),A=Math.round(b[2]-b[0])||1,_=Math.round(b[3]-b[1])||1,y=this.cachedCanvases.getCanvas("fillCanvas",A,_),v=y.context,S=Math.min(m[0],f[0]),x=Math.min(m[1],f[1]);v.translate(-S,-x);v.transform(...g);if(!u){u=this._scaleImage(p.canvas,(0,i.getCurrentTransformInverse)(v));u=u.img;h&&l&&h.set(d,u)}v.imageSmoothingEnabled=getImageSmoothingEnabled((0,i.getCurrentTransform)(v),t.interpolate);drawImageAtIntegerCoords(v,u,0,0,u.width,u.height,0,0,s,r);v.globalCompositeOperation="source-in";const E=n.Util.transform((0,i.getCurrentTransformInverse)(v),[1,0,0,1,-S,-x]);v.fillStyle=l?o.getPattern(e,this,E,a.PathType.FILL):o;v.fillRect(0,0,s,r);if(h&&!l){this.cachedCanvases.delete("fillCanvas");h.set(d,y.canvas)}return{canvas:y.canvas,offsetX:Math.round(S),offsetY:Math.round(x)}}setLineWidth(t){t!==this.current.lineWidth&&(this._cachedScaleForStroking=null);this.current.lineWidth=t;this.ctx.lineWidth=t}setLineCap(t){this.ctx.lineCap=d[t]}setLineJoin(t){this.ctx.lineJoin=u[t]}setMiterLimit(t){this.ctx.miterLimit=t}setDash(t,e){const s=this.ctx;if(void 0!==s.setLineDash){s.setLineDash(t);s.lineDashOffset=e}}setRenderingIntent(t){}setFlatness(t){}setGState(t){for(const[e,s]of t)switch(e){case"LW":this.setLineWidth(s);break;case"LC":this.setLineCap(s);break;case"LJ":this.setLineJoin(s);break;case"ML":this.setMiterLimit(s);break;case"D":this.setDash(s[0],s[1]);break;case"RI":this.setRenderingIntent(s);break;case"FL":this.setFlatness(s);break;case"Font":this.setFont(s[0],s[1]);break;case"CA":this.current.strokeAlpha=s;break;case"ca":this.current.fillAlpha=s;this.ctx.globalAlpha=s;break;case"BM":this.ctx.globalCompositeOperation=s;break;case"SMask":this.current.activeSMask=s?this.tempSMask:null;this.tempSMask=null;this.checkSMaskState();break;case"TR":this.current.transferMaps=s}}get inSMaskMode(){return!!this.suspendedCtx}checkSMaskState(){const t=this.inSMaskMode;this.current.activeSMask&&!t?this.beginSMaskMode():!this.current.activeSMask&&t&&this.endSMaskMode()}beginSMaskMode(){if(this.inSMaskMode)throw new Error("beginSMaskMode called while already in smask mode");const t=this.ctx.canvas.width,e=this.ctx.canvas.height,s="smaskGroupAt"+this.groupLevel,n=this.cachedCanvases.getCanvas(s,t,e);this.suspendedCtx=this.ctx;this.ctx=n.context;const a=this.ctx;a.setTransform(...(0,i.getCurrentTransform)(this.suspendedCtx));copyCtxState(this.suspendedCtx,a);!function mirrorContextOperations(t,e){if(t._removeMirroring)throw new Error("Context is already forwarding operations.");t.__originalSave=t.save;t.__originalRestore=t.restore;t.__originalRotate=t.rotate;t.__originalScale=t.scale;t.__originalTranslate=t.translate;t.__originalTransform=t.transform;t.__originalSetTransform=t.setTransform;t.__originalResetTransform=t.resetTransform;t.__originalClip=t.clip;t.__originalMoveTo=t.moveTo;t.__originalLineTo=t.lineTo;t.__originalBezierCurveTo=t.bezierCurveTo;t.__originalRect=t.rect;t.__originalClosePath=t.closePath;t.__originalBeginPath=t.beginPath;t._removeMirroring=()=>{t.save=t.__originalSave;t.restore=t.__originalRestore;t.rotate=t.__originalRotate;t.scale=t.__originalScale;t.translate=t.__originalTranslate;t.transform=t.__originalTransform;t.setTransform=t.__originalSetTransform;t.resetTransform=t.__originalResetTransform;t.clip=t.__originalClip;t.moveTo=t.__originalMoveTo;t.lineTo=t.__originalLineTo;t.bezierCurveTo=t.__originalBezierCurveTo;t.rect=t.__originalRect;t.closePath=t.__originalClosePath;t.beginPath=t.__originalBeginPath;delete t._removeMirroring};t.save=function ctxSave(){e.save();this.__originalSave()};t.restore=function ctxRestore(){e.restore();this.__originalRestore()};t.translate=function ctxTranslate(t,s){e.translate(t,s);this.__originalTranslate(t,s)};t.scale=function ctxScale(t,s){e.scale(t,s);this.__originalScale(t,s)};t.transform=function ctxTransform(t,s,n,i,a,r){e.transform(t,s,n,i,a,r);this.__originalTransform(t,s,n,i,a,r)};t.setTransform=function ctxSetTransform(t,s,n,i,a,r){e.setTransform(t,s,n,i,a,r);this.__originalSetTransform(t,s,n,i,a,r)};t.resetTransform=function ctxResetTransform(){e.resetTransform();this.__originalResetTransform()};t.rotate=function ctxRotate(t){e.rotate(t);this.__originalRotate(t)};t.clip=function ctxRotate(t){e.clip(t);this.__originalClip(t)};t.moveTo=function(t,s){e.moveTo(t,s);this.__originalMoveTo(t,s)};t.lineTo=function(t,s){e.lineTo(t,s);this.__originalLineTo(t,s)};t.bezierCurveTo=function(t,s,n,i,a,r){e.bezierCurveTo(t,s,n,i,a,r);this.__originalBezierCurveTo(t,s,n,i,a,r)};t.rect=function(t,s,n,i){e.rect(t,s,n,i);this.__originalRect(t,s,n,i)};t.closePath=function(){e.closePath();this.__originalClosePath()};t.beginPath=function(){e.beginPath();this.__originalBeginPath()}}(a,this.suspendedCtx);this.setGState([["BM","source-over"],["ca",1],["CA",1]])}endSMaskMode(){if(!this.inSMaskMode)throw new Error("endSMaskMode called while not in smask mode");this.ctx._removeMirroring();copyCtxState(this.ctx,this.suspendedCtx);this.ctx=this.suspendedCtx;this.suspendedCtx=null}compose(t){if(!this.current.activeSMask)return;if(t){t[0]=Math.floor(t[0]);t[1]=Math.floor(t[1]);t[2]=Math.ceil(t[2]);t[3]=Math.ceil(t[3])}else t=[0,0,this.ctx.canvas.width,this.ctx.canvas.height];const e=this.current.activeSMask;composeSMask(this.suspendedCtx,e,this.ctx,t);this.ctx.save();this.ctx.setTransform(1,0,0,1,0,0);this.ctx.clearRect(0,0,this.ctx.canvas.width,this.ctx.canvas.height);this.ctx.restore()}save(){if(this.inSMaskMode){copyCtxState(this.ctx,this.suspendedCtx);this.suspendedCtx.save()}else this.ctx.save();const t=this.current;this.stateStack.push(t);this.current=t.clone()}restore(){0===this.stateStack.length&&this.inSMaskMode&&this.endSMaskMode();if(0!==this.stateStack.length){this.current=this.stateStack.pop();if(this.inSMaskMode){this.suspendedCtx.restore();copyCtxState(this.suspendedCtx,this.ctx)}else this.ctx.restore();this.checkSMaskState();this.pendingClip=null;this._cachedScaleForStroking=null;this._cachedGetSinglePixelWidth=null}}transform(t,e,s,n,i,a){this.ctx.transform(t,e,s,n,i,a);this._cachedScaleForStroking=null;this._cachedGetSinglePixelWidth=null}constructPath(t,e,s){const a=this.ctx,r=this.current;let o,l,c=r.x,h=r.y;const d=(0,i.getCurrentTransform)(a),u=0===d[0]&&0===d[3]||0===d[1]&&0===d[2],p=u?s.slice(0):null;for(let s=0,i=0,g=t.length;s100&&(c=100);this.current.fontSizeScale=e/c;this.ctx.font=`${o} ${r} ${c}px ${l}`}setTextRenderingMode(t){this.current.textRenderingMode=t}setTextRise(t){this.current.textRise=t}moveText(t,e){this.current.x=this.current.lineX+=t;this.current.y=this.current.lineY+=e}setLeadingMoveText(t,e){this.setLeading(-e);this.moveText(t,e)}setTextMatrix(t,e,s,n,i,a){this.current.textMatrix=[t,e,s,n,i,a];this.current.textMatrixScale=Math.hypot(t,e);this.current.x=this.current.lineX=0;this.current.y=this.current.lineY=0}nextLine(){this.moveText(0,this.current.leading)}paintChar(t,e,s,a){const r=this.ctx,o=this.current,l=o.font,c=o.textRenderingMode,h=o.fontSize/o.fontSizeScale,d=c&n.TextRenderingMode.FILL_STROKE_MASK,u=!!(c&n.TextRenderingMode.ADD_TO_PATH_FLAG),p=o.patternFill&&!l.missingFile;let g;(l.disableFontFace||u||p)&&(g=l.getPathGenerator(this.commonObjs,t));if(l.disableFontFace||p){r.save();r.translate(e,s);r.beginPath();g(r,h);a&&r.setTransform(...a);d!==n.TextRenderingMode.FILL&&d!==n.TextRenderingMode.FILL_STROKE||r.fill();d!==n.TextRenderingMode.STROKE&&d!==n.TextRenderingMode.FILL_STROKE||r.stroke();r.restore()}else{d!==n.TextRenderingMode.FILL&&d!==n.TextRenderingMode.FILL_STROKE||r.fillText(t,e,s);d!==n.TextRenderingMode.STROKE&&d!==n.TextRenderingMode.FILL_STROKE||r.strokeText(t,e,s)}if(u){(this.pendingTextPaths||(this.pendingTextPaths=[])).push({transform:(0,i.getCurrentTransform)(r),x:e,y:s,fontSize:h,addToPath:g})}}get isFontSubpixelAAEnabled(){const{context:t}=this.cachedCanvases.getCanvas("isFontSubpixelAAEnabled",10,10);t.scale(1.5,1);t.fillText("I",0,10);const e=t.getImageData(0,0,10,10).data;let s=!1;for(let t=3;t0&&e[t]<255){s=!0;break}return(0,n.shadow)(this,"isFontSubpixelAAEnabled",s)}showText(t){const e=this.current,s=e.font;if(s.isType3Font)return this.showType3Text(t);const r=e.fontSize;if(0===r)return;const o=this.ctx,l=e.fontSizeScale,c=e.charSpacing,h=e.wordSpacing,d=e.fontDirection,u=e.textHScale*d,p=t.length,g=s.vertical,m=g?1:-1,f=s.defaultVMetrics,b=r*e.fontMatrix[0],A=e.textRenderingMode===n.TextRenderingMode.FILL&&!s.disableFontFace&&!e.patternFill;o.save();o.transform(...e.textMatrix);o.translate(e.x,e.y+e.textRise);d>0?o.scale(u,-1):o.scale(u,1);let _;if(e.patternFill){o.save();const t=e.fillColor.getPattern(o,this,(0,i.getCurrentTransformInverse)(o),a.PathType.FILL);_=(0,i.getCurrentTransform)(o);o.restore();o.fillStyle=t}let y=e.lineWidth;const v=e.textMatrixScale;if(0===v||0===y){const t=e.textRenderingMode&n.TextRenderingMode.FILL_STROKE_MASK;t!==n.TextRenderingMode.STROKE&&t!==n.TextRenderingMode.FILL_STROKE||(y=this.getSinglePixelWidth())}else y/=v;if(1!==l){o.scale(l,l);y/=l}o.lineWidth=y;if(s.isInvalidPDFjsFont){const s=[];let n=0;for(const e of t){s.push(e.unicode);n+=e.width}o.fillText(s.join(""),0,0);e.x+=n*b*u;o.restore();this.compose();return}let S,x=0;for(S=0;S0){const t=1e3*o.measureText(a).width/r*l;if(Enew CanvasGraphics(t,this.commonObjs,this.objs,this.canvasFactory,{optionalContentConfig:this.optionalContentConfig,markedContentStack:this.markedContentStack})};e=new a.TilingPattern(t,s,this.ctx,r,n)}else e=this._getPattern(t[1],t[2]);return e}setStrokeColorN(){this.current.strokeColor=this.getColorN_Pattern(arguments)}setFillColorN(){this.current.fillColor=this.getColorN_Pattern(arguments);this.current.patternFill=!0}setStrokeRGBColor(t,e,s){const i=this.selectColor?.(t,e,s)||n.Util.makeHexColor(t,e,s);this.ctx.strokeStyle=i;this.current.strokeColor=i}setFillRGBColor(t,e,s){const i=this.selectColor?.(t,e,s)||n.Util.makeHexColor(t,e,s);this.ctx.fillStyle=i;this.current.fillColor=i;this.current.patternFill=!1}_getPattern(t,e=null){let s;if(this.cachedPatterns.has(t))s=this.cachedPatterns.get(t);else{s=(0,a.getShadingPattern)(this.objs.get(t));this.cachedPatterns.set(t,s)}e&&(s.matrix=e);return s}shadingFill(t){if(!this.contentVisible)return;const e=this.ctx;this.save();const s=this._getPattern(t);e.fillStyle=s.getPattern(e,this,(0,i.getCurrentTransformInverse)(e),a.PathType.SHADING);const r=(0,i.getCurrentTransformInverse)(e);if(r){const t=e.canvas,s=t.width,i=t.height,a=n.Util.applyTransform([0,0],r),o=n.Util.applyTransform([0,i],r),l=n.Util.applyTransform([s,0],r),c=n.Util.applyTransform([s,i],r),h=Math.min(a[0],o[0],l[0],c[0]),d=Math.min(a[1],o[1],l[1],c[1]),u=Math.max(a[0],o[0],l[0],c[0]),p=Math.max(a[1],o[1],l[1],c[1]);this.ctx.fillRect(h,d,u-h,p-d)}else this.ctx.fillRect(-1e10,-1e10,2e10,2e10);this.compose(this.current.getClippedPathBoundingBox());this.restore()}beginInlineImage(){(0,n.unreachable)("Should not call beginInlineImage")}beginImageData(){(0,n.unreachable)("Should not call beginImageData")}paintFormXObjectBegin(t,e){if(this.contentVisible){this.save();this.baseTransformStack.push(this.baseTransform);Array.isArray(t)&&6===t.length&&this.transform(...t);this.baseTransform=(0,i.getCurrentTransform)(this.ctx);if(e){const t=e[2]-e[0],s=e[3]-e[1];this.ctx.rect(e[0],e[1],t,s);this.current.updateRectMinMax((0,i.getCurrentTransform)(this.ctx),e);this.clip();this.endPath()}}}paintFormXObjectEnd(){if(this.contentVisible){this.restore();this.baseTransform=this.baseTransformStack.pop()}}beginGroup(t){if(!this.contentVisible)return;this.save();if(this.inSMaskMode){this.endSMaskMode();this.current.activeSMask=null}const e=this.ctx;t.isolated||(0,n.info)("TODO: Support non-isolated groups.");t.knockout&&(0,n.warn)("Knockout groups not supported.");const s=(0,i.getCurrentTransform)(e);t.matrix&&e.transform(...t.matrix);if(!t.bbox)throw new Error("Bounding box is required.");let a=n.Util.getAxialAlignedBoundingBox(t.bbox,(0,i.getCurrentTransform)(e));const r=[0,0,e.canvas.width,e.canvas.height];a=n.Util.intersect(a,r)||[0,0,0,0];const o=Math.floor(a[0]),c=Math.floor(a[1]);let h=Math.max(Math.ceil(a[2])-o,1),d=Math.max(Math.ceil(a[3])-c,1),u=1,p=1;if(h>l){u=h/l;h=l}if(d>l){p=d/l;d=l}this.current.startNewPathAndClipBox([0,0,h,d]);let g="groupAt"+this.groupLevel;t.smask&&(g+="_smask_"+this.smaskCounter++%2);const m=this.cachedCanvases.getCanvas(g,h,d),f=m.context;f.scale(1/u,1/p);f.translate(-o,-c);f.transform(...s);if(t.smask)this.smaskStack.push({canvas:m.canvas,context:f,offsetX:o,offsetY:c,scaleX:u,scaleY:p,subtype:t.smask.subtype,backdrop:t.smask.backdrop,transferMap:t.smask.transferMap||null,startTransformInverse:null});else{e.setTransform(1,0,0,1,0,0);e.translate(o,c);e.scale(u,p);e.save()}copyCtxState(e,f);this.ctx=f;this.setGState([["BM","source-over"],["ca",1],["CA",1]]);this.groupStack.push(e);this.groupLevel++}endGroup(t){if(!this.contentVisible)return;this.groupLevel--;const e=this.ctx,s=this.groupStack.pop();this.ctx=s;this.ctx.imageSmoothingEnabled=!1;if(t.smask){this.tempSMask=this.smaskStack.pop();this.restore()}else{this.ctx.restore();const t=(0,i.getCurrentTransform)(this.ctx);this.restore();this.ctx.save();this.ctx.setTransform(...t);const s=n.Util.getAxialAlignedBoundingBox([0,0,e.canvas.width,e.canvas.height],t);this.ctx.drawImage(e.canvas,0,0);this.ctx.restore();this.compose(s)}}beginAnnotation(t,e,s,a,r){this.#ct();resetCtxToDefault(this.ctx,this.foregroundColor);this.ctx.save();this.save();this.baseTransform&&this.ctx.setTransform(...this.baseTransform);if(Array.isArray(e)&&4===e.length){const a=e[2]-e[0],o=e[3]-e[1];if(r&&this.annotationCanvasMap){(s=s.slice())[4]-=e[0];s[5]-=e[1];(e=e.slice())[0]=e[1]=0;e[2]=a;e[3]=o;const[r,l]=n.Util.singularValueDecompose2dScale((0,i.getCurrentTransform)(this.ctx)),{viewportScale:c}=this,h=Math.ceil(a*this.outputScaleX*c),d=Math.ceil(o*this.outputScaleY*c);this.annotationCanvas=this.canvasFactory.create(h,d);const{canvas:u,context:p}=this.annotationCanvas;this.annotationCanvasMap.set(t,u);this.annotationCanvas.savedCtx=this.ctx;this.ctx=p;this.ctx.setTransform(r,0,0,-l,0,o*l);resetCtxToDefault(this.ctx,this.foregroundColor)}else{resetCtxToDefault(this.ctx,this.foregroundColor);this.ctx.rect(e[0],e[1],a,o);this.ctx.clip();this.endPath()}}this.current=new CanvasExtraState(this.ctx.canvas.width,this.ctx.canvas.height);this.transform(...s);this.transform(...a)}endAnnotation(){if(this.annotationCanvas){this.ctx=this.annotationCanvas.savedCtx;delete this.annotationCanvas.savedCtx;delete this.annotationCanvas}}paintImageMaskXObject(t){if(!this.contentVisible)return;const e=t.count;(t=this.getObject(t.data,t)).count=e;const s=this.ctx,n=this.processingType3;if(n){void 0===n.compiled&&(n.compiled=function compileType3Glyph(t){const{width:e,height:s}=t;if(e>c||s>c)return null;const n=new Uint8Array([0,2,4,0,1,0,5,4,8,10,0,8,0,2,1,0]),i=e+1;let a,r,o,l=new Uint8Array(i*(s+1));const h=e+7&-8;let d=new Uint8Array(h*s),u=0;for(const e of t.data){let t=128;for(;t>0;){d[u++]=e&t?0:255;t>>=1}}let p=0;u=0;if(0!==d[u]){l[0]=1;++p}for(r=1;r>2)+(d[u+1]?4:0)+(d[u-h+1]?8:0);if(n[t]){l[o+r]=n[t];++p}u++}if(d[u-h]!==d[u]){l[o+r]=d[u]?2:4;++p}if(p>1e3)return null}u=h*(s-1);o=a*i;if(0!==d[u]){l[o]=8;++p}for(r=1;r1e3)return null;const g=new Int32Array([0,i,-1,0,-i,0,0,0,1]),m=new Path2D;for(a=0;p&&a<=s;a++){let t=a*i;const s=t+e;for(;t>4;l[t]&=r>>2|r<<2}m.lineTo(t%i,t/i|0);l[t]||--p}while(n!==t);--a}d=null;l=null;return function(t){t.save();t.scale(1/e,-1/s);t.translate(0,-s);t.fill(m);t.beginPath();t.restore()}}(t));if(n.compiled){n.compiled(s);return}}const i=this._createMaskCanvas(t),a=i.canvas;s.save();s.setTransform(1,0,0,1,0,0);s.drawImage(a,i.offsetX,i.offsetY);s.restore();this.compose()}paintImageMaskXObjectRepeat(t,e,s=0,a=0,r,o){if(!this.contentVisible)return;t=this.getObject(t.data,t);const l=this.ctx;l.save();const c=(0,i.getCurrentTransform)(l);l.transform(e,s,a,r,0,0);const h=this._createMaskCanvas(t);l.setTransform(1,0,0,1,h.offsetX-c[4],h.offsetY-c[5]);for(let t=0,i=o.length;te?r/e:1;n=a>e?a/e:1}}this._cachedScaleForStroking=[s,n]}return this._cachedScaleForStroking}rescaleAndStroke(t){const{ctx:e}=this,{lineWidth:s}=this.current,[n,a]=this.getScaleForStroking();e.lineWidth=s||1;if(1===n&&1===a){e.stroke();return}let r,o,l;if(t){r=(0,i.getCurrentTransform)(e);o=e.getLineDash().slice();l=e.lineDashOffset}e.scale(n,a);const c=Math.max(n,a);e.setLineDash(e.getLineDash().map((t=>t/c)));e.lineDashOffset/=c;e.stroke();if(t){e.setTransform(...r);e.setLineDash(o);e.lineDashOffset=l}}isContentVisible(){for(let t=this.markedContentStack.length-1;t>=0;t--)if(!this.markedContentStack[t].visible)return!1;return!0}}e.CanvasGraphics=CanvasGraphics;for(const t in n.OPS)void 0!==CanvasGraphics.prototype[t]&&(CanvasGraphics.prototype[n.OPS[t]]=CanvasGraphics.prototype[t])},(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0});e.TilingPattern=e.PathType=void 0;e.getShadingPattern=function getShadingPattern(t){switch(t[0]){case"RadialAxial":return new RadialAxialShadingPattern(t);case"Mesh":return new MeshShadingPattern(t);case"Dummy":return new DummyShadingPattern}throw new Error(`Unknown IR type: ${t[0]}`)};var n=s(1),i=s(6),a=s(10);const r={FILL:"Fill",STROKE:"Stroke",SHADING:"Shading"};e.PathType=r;function applyBoundingBox(t,e){if(!e||a.isNodeJS)return;const s=e[2]-e[0],n=e[3]-e[1],i=new Path2D;i.rect(e[0],e[1],s,n);t.clip(i)}class BaseShadingPattern{constructor(){this.constructor===BaseShadingPattern&&(0,n.unreachable)("Cannot initialize BaseShadingPattern.")}getPattern(){(0,n.unreachable)("Abstract method `getPattern` called.")}}class RadialAxialShadingPattern extends BaseShadingPattern{constructor(t){super();this._type=t[1];this._bbox=t[2];this._colorStops=t[3];this._p0=t[4];this._p1=t[5];this._r0=t[6];this._r1=t[7];this.matrix=null}_createGradient(t){let e;"axial"===this._type?e=t.createLinearGradient(this._p0[0],this._p0[1],this._p1[0],this._p1[1]):"radial"===this._type&&(e=t.createRadialGradient(this._p0[0],this._p0[1],this._r0,this._p1[0],this._p1[1],this._r1));for(const t of this._colorStops)e.addColorStop(t[0],t[1]);return e}getPattern(t,e,s,a){let o;if(a===r.STROKE||a===r.FILL){const r=e.current.getClippedPathBoundingBox(a,(0,i.getCurrentTransform)(t))||[0,0,0,0],l=Math.ceil(r[2]-r[0])||1,c=Math.ceil(r[3]-r[1])||1,h=e.cachedCanvases.getCanvas("pattern",l,c,!0),d=h.context;d.clearRect(0,0,d.canvas.width,d.canvas.height);d.beginPath();d.rect(0,0,d.canvas.width,d.canvas.height);d.translate(-r[0],-r[1]);s=n.Util.transform(s,[1,0,0,1,r[0],r[1]]);d.transform(...e.baseTransform);this.matrix&&d.transform(...this.matrix);applyBoundingBox(d,this._bbox);d.fillStyle=this._createGradient(d);d.fill();o=t.createPattern(h.canvas,"no-repeat");const u=new DOMMatrix(s);o.setTransform(u)}else{applyBoundingBox(t,this._bbox);o=this._createGradient(t)}return o}}function drawTriangle(t,e,s,n,i,a,r,o){const l=e.coords,c=e.colors,h=t.data,d=4*t.width;let u;if(l[s+1]>l[n+1]){u=s;s=n;n=u;u=a;a=r;r=u}if(l[n+1]>l[i+1]){u=n;n=i;i=u;u=r;r=o;o=u}if(l[s+1]>l[n+1]){u=s;s=n;n=u;u=a;a=r;r=u}const p=(l[s]+e.offsetX)*e.scaleX,g=(l[s+1]+e.offsetY)*e.scaleY,m=(l[n]+e.offsetX)*e.scaleX,f=(l[n+1]+e.offsetY)*e.scaleY,b=(l[i]+e.offsetX)*e.scaleX,A=(l[i+1]+e.offsetY)*e.scaleY;if(g>=A)return;const _=c[a],y=c[a+1],v=c[a+2],S=c[r],x=c[r+1],E=c[r+2],C=c[o],P=c[o+1],T=c[o+2],w=Math.round(g),k=Math.round(A);let F,R,M,D,I,O,L,N;for(let t=w;t<=k;t++){if(tA?1:f===A?0:(f-t)/(f-A);F=m-(m-b)*e;R=S-(S-C)*e;M=x-(x-P)*e;D=E-(E-T)*e}let e;e=tA?1:(g-t)/(g-A);I=p-(p-b)*e;O=_-(_-C)*e;L=y-(y-P)*e;N=v-(v-T)*e;const s=Math.round(Math.min(F,I)),n=Math.round(Math.max(F,I));let i=d*t+4*s;for(let t=s;t<=n;t++){e=(F-t)/(F-I);e<0?e=0:e>1&&(e=1);h[i++]=R-(R-O)*e|0;h[i++]=M-(M-L)*e|0;h[i++]=D-(D-N)*e|0;h[i++]=255}}}function drawFigure(t,e,s){const n=e.coords,i=e.colors;let a,r;switch(e.type){case"lattice":const o=e.verticesPerRow,l=Math.floor(n.length/o)-1,c=o-1;for(a=0;a=n?i=n:s=i/t;return{scale:s,size:i}}clipBbox(t,e,s,n,a){const r=n-e,o=a-s;t.ctx.rect(e,s,r,o);t.current.updateRectMinMax((0,i.getCurrentTransform)(t.ctx),[e,s,n,a]);t.clip();t.endPath()}setFillAndStrokeStyleToContext(t,e,s){const i=t.ctx,a=t.current;switch(e){case o:const t=this.ctx;i.fillStyle=t.fillStyle;i.strokeStyle=t.strokeStyle;a.fillColor=t.fillStyle;a.strokeColor=t.strokeStyle;break;case l:const r=n.Util.makeHexColor(s[0],s[1],s[2]);i.fillStyle=r;i.strokeStyle=r;a.fillColor=r;a.strokeColor=r;break;default:throw new n.FormatError(`Unsupported paint type: ${e}`)}}getPattern(t,e,s,i){let a=s;if(i!==r.SHADING){a=n.Util.transform(a,e.baseTransform);this.matrix&&(a=n.Util.transform(a,this.matrix))}const o=this.createPatternCanvas(e);let l=new DOMMatrix(a);l=l.translate(o.offsetX,o.offsetY);l=l.scale(1/o.scaleX,1/o.scaleY);const c=t.createPattern(o.canvas,"repeat");c.setTransform(l);return c}}e.TilingPattern=TilingPattern},(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0});e.applyMaskImageData=function applyMaskImageData({src:t,srcPos:e=0,dest:s,destPos:i=0,width:a,height:r,inverseDecode:o=!1}){const l=n.FeatureTest.isLittleEndian?4278190080:255,[c,h]=o?[0,l]:[l,0],d=a>>3,u=7&a,p=t.length;s=new Uint32Array(s.buffer);for(let n=0;n{Object.defineProperty(e,"__esModule",{value:!0});e.GlobalWorkerOptions=void 0;const s=Object.create(null);e.GlobalWorkerOptions=s;s.workerPort=void 0===s.workerPort?null:s.workerPort;s.workerSrc=void 0===s.workerSrc?"":s.workerSrc},(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0});e.MessageHandler=void 0;var n=s(1);const i=1,a=2,r=1,o=2,l=3,c=4,h=5,d=6,u=7,p=8;function wrapReason(t){t instanceof Error||"object"==typeof t&&null!==t||(0,n.unreachable)('wrapReason: Expected "reason" to be a (possibly cloned) Error.');switch(t.name){case"AbortException":return new n.AbortException(t.message);case"MissingPDFException":return new n.MissingPDFException(t.message);case"PasswordException":return new n.PasswordException(t.message,t.code);case"UnexpectedResponseException":return new n.UnexpectedResponseException(t.message,t.status);case"UnknownErrorException":return new n.UnknownErrorException(t.message,t.details);default:return new n.UnknownErrorException(t.message,t.toString())}}e.MessageHandler=class MessageHandler{constructor(t,e,s){this.sourceName=t;this.targetName=e;this.comObj=s;this.callbackId=1;this.streamId=1;this.streamSinks=Object.create(null);this.streamControllers=Object.create(null);this.callbackCapabilities=Object.create(null);this.actionHandler=Object.create(null);this._onComObjOnMessage=t=>{const e=t.data;if(e.targetName!==this.sourceName)return;if(e.stream){this._processStreamMessage(e);return}if(e.callback){const t=e.callbackId,s=this.callbackCapabilities[t];if(!s)throw new Error(`Cannot resolve callback ${t}`);delete this.callbackCapabilities[t];if(e.callback===i)s.resolve(e.data);else{if(e.callback!==a)throw new Error("Unexpected callback case");s.reject(wrapReason(e.reason))}return}const n=this.actionHandler[e.action];if(!n)throw new Error(`Unknown action from worker: ${e.action}`);if(e.callbackId){const t=this.sourceName,r=e.sourceName;new Promise((function(t){t(n(e.data))})).then((function(n){s.postMessage({sourceName:t,targetName:r,callback:i,callbackId:e.callbackId,data:n})}),(function(n){s.postMessage({sourceName:t,targetName:r,callback:a,callbackId:e.callbackId,reason:wrapReason(n)})}))}else e.streamId?this._createStreamSink(e):n(e.data)};s.addEventListener("message",this._onComObjOnMessage)}on(t,e){const s=this.actionHandler;if(s[t])throw new Error(`There is already an actionName called "${t}"`);s[t]=e}send(t,e,s){this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:t,data:e},s)}sendWithPromise(t,e,s){const i=this.callbackId++,a=(0,n.createPromiseCapability)();this.callbackCapabilities[i]=a;try{this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:t,callbackId:i,data:e},s)}catch(t){a.reject(t)}return a.promise}sendWithStream(t,e,s,i){const a=this.streamId++,o=this.sourceName,l=this.targetName,c=this.comObj;return new ReadableStream({start:s=>{const r=(0,n.createPromiseCapability)();this.streamControllers[a]={controller:s,startCall:r,pullCall:null,cancelCall:null,isClosed:!1};c.postMessage({sourceName:o,targetName:l,action:t,streamId:a,data:e,desiredSize:s.desiredSize},i);return r.promise},pull:t=>{const e=(0,n.createPromiseCapability)();this.streamControllers[a].pullCall=e;c.postMessage({sourceName:o,targetName:l,stream:d,streamId:a,desiredSize:t.desiredSize});return e.promise},cancel:t=>{(0,n.assert)(t instanceof Error,"cancel must have a valid reason");const e=(0,n.createPromiseCapability)();this.streamControllers[a].cancelCall=e;this.streamControllers[a].isClosed=!0;c.postMessage({sourceName:o,targetName:l,stream:r,streamId:a,reason:wrapReason(t)});return e.promise}},s)}_createStreamSink(t){const e=t.streamId,s=this.sourceName,i=t.sourceName,a=this.comObj,r=this,o=this.actionHandler[t.action],d={enqueue(t,r=1,o){if(this.isCancelled)return;const l=this.desiredSize;this.desiredSize-=r;if(l>0&&this.desiredSize<=0){this.sinkCapability=(0,n.createPromiseCapability)();this.ready=this.sinkCapability.promise}a.postMessage({sourceName:s,targetName:i,stream:c,streamId:e,chunk:t},o)},close(){if(!this.isCancelled){this.isCancelled=!0;a.postMessage({sourceName:s,targetName:i,stream:l,streamId:e});delete r.streamSinks[e]}},error(t){(0,n.assert)(t instanceof Error,"error must have a valid reason");if(!this.isCancelled){this.isCancelled=!0;a.postMessage({sourceName:s,targetName:i,stream:h,streamId:e,reason:wrapReason(t)})}},sinkCapability:(0,n.createPromiseCapability)(),onPull:null,onCancel:null,isCancelled:!1,desiredSize:t.desiredSize,ready:null};d.sinkCapability.resolve();d.ready=d.sinkCapability.promise;this.streamSinks[e]=d;new Promise((function(e){e(o(t.data,d))})).then((function(){a.postMessage({sourceName:s,targetName:i,stream:p,streamId:e,success:!0})}),(function(t){a.postMessage({sourceName:s,targetName:i,stream:p,streamId:e,reason:wrapReason(t)})}))}_processStreamMessage(t){const e=t.streamId,s=this.sourceName,i=t.sourceName,a=this.comObj,g=this.streamControllers[e],m=this.streamSinks[e];switch(t.stream){case p:t.success?g.startCall.resolve():g.startCall.reject(wrapReason(t.reason));break;case u:t.success?g.pullCall.resolve():g.pullCall.reject(wrapReason(t.reason));break;case d:if(!m){a.postMessage({sourceName:s,targetName:i,stream:u,streamId:e,success:!0});break}m.desiredSize<=0&&t.desiredSize>0&&m.sinkCapability.resolve();m.desiredSize=t.desiredSize;new Promise((function(t){t(m.onPull&&m.onPull())})).then((function(){a.postMessage({sourceName:s,targetName:i,stream:u,streamId:e,success:!0})}),(function(t){a.postMessage({sourceName:s,targetName:i,stream:u,streamId:e,reason:wrapReason(t)})}));break;case c:(0,n.assert)(g,"enqueue should have stream controller");if(g.isClosed)break;g.controller.enqueue(t.chunk);break;case l:(0,n.assert)(g,"close should have stream controller");if(g.isClosed)break;g.isClosed=!0;g.controller.close();this._deleteStreamController(g,e);break;case h:(0,n.assert)(g,"error should have stream controller");g.controller.error(wrapReason(t.reason));this._deleteStreamController(g,e);break;case o:t.success?g.cancelCall.resolve():g.cancelCall.reject(wrapReason(t.reason));this._deleteStreamController(g,e);break;case r:if(!m)break;new Promise((function(e){e(m.onCancel&&m.onCancel(wrapReason(t.reason)))})).then((function(){a.postMessage({sourceName:s,targetName:i,stream:o,streamId:e,success:!0})}),(function(t){a.postMessage({sourceName:s,targetName:i,stream:o,streamId:e,reason:wrapReason(t)})}));m.sinkCapability.reject(wrapReason(t.reason));m.isCancelled=!0;delete this.streamSinks[e];break;default:throw new Error("Unexpected stream case")}}async _deleteStreamController(t,e){await Promise.allSettled([t.startCall&&t.startCall.promise,t.pullCall&&t.pullCall.promise,t.cancelCall&&t.cancelCall.promise]);delete this.streamControllers[e]}destroy(){this.comObj.removeEventListener("message",this._onComObjOnMessage)}}},(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0});e.Metadata=void 0;var n=s(1);e.Metadata=class Metadata{#ht;#dt;constructor({parsedData:t,rawData:e}){this.#ht=t;this.#dt=e}getRaw(){return this.#dt}get(t){return this.#ht.get(t)??null}getAll(){return(0,n.objectFromMap)(this.#ht)}has(t){return this.#ht.has(t)}}},(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0});e.OptionalContentConfig=void 0;var n=s(1),i=s(8);const a=Symbol("INTERNAL");class OptionalContentGroup{#ut=!0;constructor(t,e){this.name=t;this.intent=e}get visible(){return this.#ut}_setVisible(t,e){t!==a&&(0,n.unreachable)("Internal method `_setVisible` called.");this.#ut=e}}e.OptionalContentConfig=class OptionalContentConfig{#pt=null;#gt=new Map;#mt=null;#ft=null;constructor(t){this.name=null;this.creator=null;if(null!==t){this.name=t.name;this.creator=t.creator;this.#ft=t.order;for(const e of t.groups)this.#gt.set(e.id,new OptionalContentGroup(e.name,e.intent));if("OFF"===t.baseState)for(const t of this.#gt.values())t._setVisible(a,!1);for(const e of t.on)this.#gt.get(e)._setVisible(a,!0);for(const e of t.off)this.#gt.get(e)._setVisible(a,!1);this.#mt=this.getHash()}}#bt(t){const e=t.length;if(e<2)return!0;const s=t[0];for(let i=1;i0?(0,n.objectFromMap)(this.#gt):null}getGroup(t){return this.#gt.get(t)||null}getHash(){if(null!==this.#pt)return this.#pt;const t=new i.MurmurHash3_64;for(const[e,s]of this.#gt)t.update(`${e}:${s.visible}`);return this.#pt=t.hexdigest()}}},(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0});e.PDFDataTransportStream=void 0;var n=s(1),i=s(6);e.PDFDataTransportStream=class PDFDataTransportStream{constructor(t,e){(0,n.assert)(e,'PDFDataTransportStream - missing required "pdfDataRangeTransport" argument.');this._queuedChunks=[];this._progressiveDone=t.progressiveDone||!1;this._contentDispositionFilename=t.contentDispositionFilename||null;const s=t.initialData;if(s?.length>0){const t=new Uint8Array(s).buffer;this._queuedChunks.push(t)}this._pdfDataRangeTransport=e;this._isStreamingSupported=!t.disableStream;this._isRangeSupported=!t.disableRange;this._contentLength=t.length;this._fullRequestReader=null;this._rangeReaders=[];this._pdfDataRangeTransport.addRangeListener(((t,e)=>{this._onReceiveData({begin:t,chunk:e})}));this._pdfDataRangeTransport.addProgressListener(((t,e)=>{this._onProgress({loaded:t,total:e})}));this._pdfDataRangeTransport.addProgressiveReadListener((t=>{this._onReceiveData({chunk:t})}));this._pdfDataRangeTransport.addProgressiveDoneListener((()=>{this._onProgressiveDone()}));this._pdfDataRangeTransport.transportReady()}_onReceiveData(t){const e=new Uint8Array(t.chunk).buffer;if(void 0===t.begin)this._fullRequestReader?this._fullRequestReader._enqueue(e):this._queuedChunks.push(e);else{const s=this._rangeReaders.some((function(s){if(s._begin!==t.begin)return!1;s._enqueue(e);return!0}));(0,n.assert)(s,"_onReceiveData - no `PDFDataTransportStreamRangeReader` instance found.")}}get _progressiveDataLength(){return this._fullRequestReader?._loaded??0}_onProgress(t){void 0===t.total?this._rangeReaders[0]?.onProgress?.({loaded:t.loaded}):this._fullRequestReader?.onProgress?.({loaded:t.loaded,total:t.total})}_onProgressiveDone(){this._fullRequestReader?.progressiveDone();this._progressiveDone=!0}_removeRangeReader(t){const e=this._rangeReaders.indexOf(t);e>=0&&this._rangeReaders.splice(e,1)}getFullReader(){(0,n.assert)(!this._fullRequestReader,"PDFDataTransportStream.getFullReader can only be called once.");const t=this._queuedChunks;this._queuedChunks=null;return new PDFDataTransportStreamReader(this,t,this._progressiveDone,this._contentDispositionFilename)}getRangeReader(t,e){if(e<=this._progressiveDataLength)return null;const s=new PDFDataTransportStreamRangeReader(this,t,e);this._pdfDataRangeTransport.requestDataRange(t,e);this._rangeReaders.push(s);return s}cancelAllRequests(t){this._fullRequestReader?.cancel(t);for(const e of this._rangeReaders.slice(0))e.cancel(t);this._pdfDataRangeTransport.abort()}};class PDFDataTransportStreamReader{constructor(t,e,s=!1,n=null){this._stream=t;this._done=s||!1;this._filename=(0,i.isPdfFile)(n)?n:null;this._queuedChunks=e||[];this._loaded=0;for(const t of this._queuedChunks)this._loaded+=t.byteLength;this._requests=[];this._headersReady=Promise.resolve();t._fullRequestReader=this;this.onProgress=null}_enqueue(t){if(!this._done){if(this._requests.length>0){this._requests.shift().resolve({value:t,done:!1})}else this._queuedChunks.push(t);this._loaded+=t.byteLength}}get headersReady(){return this._headersReady}get filename(){return this._filename}get isRangeSupported(){return this._stream._isRangeSupported}get isStreamingSupported(){return this._stream._isStreamingSupported}get contentLength(){return this._stream._contentLength}async read(){if(this._queuedChunks.length>0){return{value:this._queuedChunks.shift(),done:!1}}if(this._done)return{value:void 0,done:!0};const t=(0,n.createPromiseCapability)();this._requests.push(t);return t.promise}cancel(t){this._done=!0;for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0}progressiveDone(){this._done||(this._done=!0)}}class PDFDataTransportStreamRangeReader{constructor(t,e,s){this._stream=t;this._begin=e;this._end=s;this._queuedChunk=null;this._requests=[];this._done=!1;this.onProgress=null}_enqueue(t){if(!this._done){if(0===this._requests.length)this._queuedChunk=t;else{this._requests.shift().resolve({value:t,done:!1});for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0}this._done=!0;this._stream._removeRangeReader(this)}}get isStreamingSupported(){return!1}async read(){if(this._queuedChunk){const t=this._queuedChunk;this._queuedChunk=null;return{value:t,done:!1}}if(this._done)return{value:void 0,done:!0};const t=(0,n.createPromiseCapability)();this._requests.push(t);return t.promise}cancel(t){this._done=!0;for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0;this._stream._removeRangeReader(this)}}},(t,e)=>{Object.defineProperty(e,"__esModule",{value:!0});e.XfaText=void 0;class XfaText{static textContent(t){const e=[],s={items:e,styles:Object.create(null)};!function walk(t){if(!t)return;let s=null;const n=t.name;if("#text"===n)s=t.value;else{if(!XfaText.shouldBuildText(n))return;t?.attributes?.textContent?s=t.attributes.textContent:t.value&&(s=t.value)}null!==s&&e.push({str:s});if(t.children)for(const e of t.children)walk(e)}(t);return s}static shouldBuildText(t){return!("textarea"===t||"input"===t||"option"===t||"select"===t)}}e.XfaText=XfaText},(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0});e.NodeStandardFontDataFactory=e.NodeCanvasFactory=e.NodeCMapReaderFactory=void 0;var n=s(7);const fetchData=function(t){return new Promise(((e,s)=>{require("fs").readFile(t,((t,n)=>{!t&&n?e(new Uint8Array(n)):s(new Error(t))}))}))};class NodeCanvasFactory extends n.BaseCanvasFactory{_createCanvas(t,e){return require("canvas").createCanvas(t,e)}}e.NodeCanvasFactory=NodeCanvasFactory;class NodeCMapReaderFactory extends n.BaseCMapReaderFactory{_fetchData(t,e){return fetchData(t).then((t=>({cMapData:t,compressionType:e})))}}e.NodeCMapReaderFactory=NodeCMapReaderFactory;class NodeStandardFontDataFactory extends n.BaseStandardFontDataFactory{_fetchData(t){return fetchData(t)}}e.NodeStandardFontDataFactory=NodeStandardFontDataFactory},(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0});e.TextLayerRenderTask=void 0;e.renderTextLayer=function renderTextLayer(t){if(!t.textContentSource&&(t.textContent||t.textContentStream)){(0,i.deprecated)("The TextLayerRender `textContent`/`textContentStream` parameters will be removed in the future, please use `textContentSource` instead.");t.textContentSource=t.textContent||t.textContentStream}const e=new TextLayerRenderTask(t);e._render();return e};e.updateTextLayer=function updateTextLayer({container:t,viewport:e,textDivs:s,textDivProperties:n,isOffscreenCanvasSupported:a,mustRotate:r=!0,mustRescale:o=!0}){r&&(0,i.setLayerDimensions)(t,{rotation:e.rotation});if(o){const t=getCtx(0,a),i={prevFontSize:null,prevFontFamily:null,div:null,scale:e.scale*(globalThis.devicePixelRatio||1),properties:null,ctx:t};for(const t of s){i.properties=n.get(t);i.div=t;layout(i)}}};var n=s(1),i=s(6);const a=30,r=new Map;function getCtx(t,e){let s;if(e&&n.FeatureTest.isOffscreenCanvasSupported)s=new OffscreenCanvas(t,t).getContext("2d",{alpha:!1});else{const e=document.createElement("canvas");e.width=e.height=t;s=e.getContext("2d",{alpha:!1})}return s}function appendText(t,e,s){const i=document.createElement("span"),o={angle:0,canvasWidth:0,hasText:""!==e.str,hasEOL:e.hasEOL,fontSize:0};t._textDivs.push(i);const l=n.Util.transform(t._transform,e.transform);let c=Math.atan2(l[1],l[0]);const h=s[e.fontName];h.vertical&&(c+=Math.PI/2);const d=Math.hypot(l[2],l[3]),u=d*function getAscent(t,e){const s=r.get(t);if(s)return s;const n=getCtx(a,e);n.font=`30px ${t}`;const i=n.measureText("");let o=i.fontBoundingBoxAscent,l=Math.abs(i.fontBoundingBoxDescent);if(o){const e=o/(o+l);r.set(t,e);n.canvas.width=n.canvas.height=0;return e}n.strokeStyle="red";n.clearRect(0,0,a,a);n.strokeText("g",0,0);let c=n.getImageData(0,0,a,a).data;l=0;for(let t=c.length-1-3;t>=0;t-=4)if(c[t]>0){l=Math.ceil(t/4/a);break}n.clearRect(0,0,a,a);n.strokeText("A",0,a);c=n.getImageData(0,0,a,a).data;o=0;for(let t=0,e=c.length;t0){o=a-Math.floor(t/4/a);break}n.canvas.width=n.canvas.height=0;if(o){const e=o/(o+l);r.set(t,e);return e}r.set(t,.8);return.8}(h.fontFamily,t._isOffscreenCanvasSupported);let p,g;if(0===c){p=l[4];g=l[5]-u}else{p=l[4]+u*Math.sin(c);g=l[5]-u*Math.cos(c)}const m="calc(var(--scale-factor)*",f=i.style;if(t._container===t._rootContainer){f.left=`${(100*p/t._pageWidth).toFixed(2)}%`;f.top=`${(100*g/t._pageHeight).toFixed(2)}%`}else{f.left=`${m}${p.toFixed(2)}px)`;f.top=`${m}${g.toFixed(2)}px)`}f.fontSize=`${m}${d.toFixed(2)}px)`;f.fontFamily=h.fontFamily;o.fontSize=d;i.setAttribute("role","presentation");i.textContent=e.str;i.dir=e.dir;t._fontInspectorEnabled&&(i.dataset.fontName=e.fontName);0!==c&&(o.angle=c*(180/Math.PI));let b=!1;if(e.str.length>1)b=!0;else if(" "!==e.str&&e.transform[0]!==e.transform[3]){const t=Math.abs(e.transform[0]),s=Math.abs(e.transform[3]);t!==s&&Math.max(t,s)/Math.min(t,s)>1.5&&(b=!0)}b&&(o.canvasWidth=h.vertical?e.height:e.width);t._textDivProperties.set(i,o);t._isReadableStream&&t._layoutText(i)}function layout(t){const{div:e,scale:s,properties:n,ctx:i,prevFontSize:a,prevFontFamily:r}=t,{style:o}=e;let l="";if(0!==n.canvasWidth&&n.hasText){const{fontFamily:c}=o,{canvasWidth:h,fontSize:d}=n;if(a!==d||r!==c){i.font=`${d*s}px ${c}`;t.prevFontSize=d;t.prevFontFamily=c}const{width:u}=i.measureText(e.textContent);u>0&&(l=`scaleX(${h*s/u})`)}0!==n.angle&&(l=`rotate(${n.angle}deg) ${l}`);l.length>0&&(o.transform=l)}class TextLayerRenderTask{constructor({textContentSource:t,container:e,viewport:s,textDivs:a,textDivProperties:r,textContentItemsStr:o,isOffscreenCanvasSupported:l}){this._textContentSource=t;this._isReadableStream=t instanceof ReadableStream;this._container=this._rootContainer=e;this._textDivs=a||[];this._textContentItemsStr=o||[];this._fontInspectorEnabled=!!globalThis.FontInspector?.enabled;this._reader=null;this._textDivProperties=r||new WeakMap;this._canceled=!1;this._capability=(0,n.createPromiseCapability)();this._layoutTextParams={prevFontSize:null,prevFontFamily:null,div:null,scale:s.scale*(globalThis.devicePixelRatio||1),properties:null,ctx:getCtx(0,l)};const{pageWidth:c,pageHeight:h,pageX:d,pageY:u}=s.rawDims;this._transform=[1,0,0,-1,-d,u+h];this._pageWidth=c;this._pageHeight=h;(0,i.setLayerDimensions)(e,s);this._capability.promise.finally((()=>{this._layoutTextParams=null})).catch((()=>{}))}get promise(){return this._capability.promise}cancel(){this._canceled=!0;if(this._reader){this._reader.cancel(new n.AbortException("TextLayer task cancelled.")).catch((()=>{}));this._reader=null}this._capability.reject(new n.AbortException("TextLayer task cancelled."))}_processItems(t,e){for(const s of t)if(void 0!==s.str){this._textContentItemsStr.push(s.str);appendText(this,s,e)}else if("beginMarkedContentProps"===s.type||"beginMarkedContent"===s.type){const t=this._container;this._container=document.createElement("span");this._container.classList.add("markedContent");null!==s.id&&this._container.setAttribute("id",`${s.id}`);t.append(this._container)}else"endMarkedContent"===s.type&&(this._container=this._container.parentNode)}_layoutText(t){const e=this._layoutTextParams.properties=this._textDivProperties.get(t);this._layoutTextParams.div=t;layout(this._layoutTextParams);e.hasText&&this._container.append(t);if(e.hasEOL){const t=document.createElement("br");t.setAttribute("role","presentation");this._container.append(t)}}_render(){const t=(0,n.createPromiseCapability)();let e=Object.create(null);if(this._isReadableStream){const pump=()=>{this._reader.read().then((({value:s,done:n})=>{if(n)t.resolve();else{Object.assign(e,s.styles);this._processItems(s.items,e);pump()}}),t.reject)};this._reader=this._textContentSource.getReader();pump()}else{if(!this._textContentSource)throw new Error('No "textContentSource" parameter specified.');{const{items:e,styles:s}=this._textContentSource;this._processItems(e,s);t.resolve()}}t.promise.then((()=>{e=null;!function render(t){if(t._canceled)return;const e=t._textDivs,s=t._capability;if(e.length>1e5)s.resolve();else{if(!t._isReadableStream)for(const s of e)t._layoutText(s);s.resolve()}}(this)}),this._capability.reject)}}e.TextLayerRenderTask=TextLayerRenderTask},(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0});e.AnnotationEditorLayer=void 0;var n=s(1),i=s(5),a=s(23),r=s(24),o=s(6);class AnnotationEditorLayer{#At;#_t=!1;#yt=this.pointerup.bind(this);#vt=this.pointerdown.bind(this);#St=new Map;#xt=!1;#Et=!1;#Ct;static _initialized=!1;constructor(t){if(!AnnotationEditorLayer._initialized){AnnotationEditorLayer._initialized=!0;a.FreeTextEditor.initialize(t.l10n);r.InkEditor.initialize(t.l10n)}t.uiManager.registerEditorTypes([a.FreeTextEditor,r.InkEditor]);this.#Ct=t.uiManager;this.pageIndex=t.pageIndex;this.div=t.div;this.#At=t.accessibilityManager;this.#Ct.addLayer(this)}updateToolbar(t){this.#Ct.updateToolbar(t)}updateMode(t=this.#Ct.getMode()){this.#Pt();if(t===n.AnnotationEditorType.INK){this.addInkEditorIfNeeded(!1);this.disableClick()}else this.enableClick();this.#Ct.unselectAll();this.div.classList.toggle("freeTextEditing",t===n.AnnotationEditorType.FREETEXT);this.div.classList.toggle("inkEditing",t===n.AnnotationEditorType.INK)}addInkEditorIfNeeded(t){if(!t&&this.#Ct.getMode()!==n.AnnotationEditorType.INK)return;if(!t)for(const t of this.#St.values())if(t.isEmpty()){t.setInBackground();return}this.#Tt({offsetX:0,offsetY:0}).setInBackground()}setEditingState(t){this.#Ct.setEditingState(t)}addCommands(t){this.#Ct.addCommands(t)}enable(){this.div.style.pointerEvents="auto";for(const t of this.#St.values())t.enableEditing()}disable(){this.div.style.pointerEvents="none";for(const t of this.#St.values())t.disableEditing()}setActiveEditor(t){this.#Ct.getActive()!==t&&this.#Ct.setActiveEditor(t)}enableClick(){this.div.addEventListener("pointerdown",this.#vt);this.div.addEventListener("pointerup",this.#yt)}disableClick(){this.div.removeEventListener("pointerdown",this.#vt);this.div.removeEventListener("pointerup",this.#yt)}attach(t){this.#St.set(t.id,t)}detach(t){this.#St.delete(t.id);this.#At?.removePointerInTextLayer(t.contentDiv)}remove(t){this.#Ct.removeEditor(t);this.detach(t);t.div.style.display="none";setTimeout((()=>{t.div.style.display="";t.div.remove();t.isAttachedToDOM=!1;document.activeElement===document.body&&this.#Ct.focusMainContainer()}),0);this.#Et||this.addInkEditorIfNeeded(!1)}#wt(t){if(t.parent!==this){this.attach(t);t.parent?.detach(t);t.setParent(this);if(t.div&&t.isAttachedToDOM){t.div.remove();this.div.append(t.div)}}}add(t){this.#wt(t);this.#Ct.addEditor(t);this.attach(t);if(!t.isAttachedToDOM){const e=t.render();this.div.append(e);t.isAttachedToDOM=!0}this.moveEditorInDOM(t);t.onceAdded();this.#Ct.addToAnnotationStorage(t)}moveEditorInDOM(t){this.#At?.moveElementInDOM(this.div,t.div,t.contentDiv,!0)}addOrRebuild(t){t.needsToBeRebuilt()?t.rebuild():this.add(t)}addANewEditor(t){this.addCommands({cmd:()=>{this.addOrRebuild(t)},undo:()=>{t.remove()},mustExec:!0})}addUndoableEditor(t){this.addCommands({cmd:()=>{this.addOrRebuild(t)},undo:()=>{t.remove()},mustExec:!1})}getNextId(){return this.#Ct.getId()}#kt(t){switch(this.#Ct.getMode()){case n.AnnotationEditorType.FREETEXT:return new a.FreeTextEditor(t);case n.AnnotationEditorType.INK:return new r.InkEditor(t)}return null}deserialize(t){switch(t.annotationType){case n.AnnotationEditorType.FREETEXT:return a.FreeTextEditor.deserialize(t,this,this.#Ct);case n.AnnotationEditorType.INK:return r.InkEditor.deserialize(t,this,this.#Ct)}return null}#Tt(t){const e=this.getNextId(),s=this.#kt({parent:this,id:e,x:t.offsetX,y:t.offsetY,uiManager:this.#Ct});s&&this.add(s);return s}setSelected(t){this.#Ct.setSelected(t)}toggleSelected(t){this.#Ct.toggleSelected(t)}isSelected(t){return this.#Ct.isSelected(t)}unselect(t){this.#Ct.unselect(t)}pointerup(t){const{isMac:e}=n.FeatureTest.platform;if(!(0!==t.button||t.ctrlKey&&e)&&t.target===this.div&&this.#xt){this.#xt=!1;this.#_t?this.#Tt(t):this.#_t=!0}}pointerdown(t){const{isMac:e}=n.FeatureTest.platform;if(0!==t.button||t.ctrlKey&&e)return;if(t.target!==this.div)return;this.#xt=!0;const s=this.#Ct.getActive();this.#_t=!s||s.isEmpty()}drop(t){const e=t.dataTransfer.getData("text/plain"),s=this.#Ct.getEditor(e);if(!s)return;t.preventDefault();t.dataTransfer.dropEffect="move";this.#wt(s);const n=this.div.getBoundingClientRect(),i=t.clientX-n.x,a=t.clientY-n.y;s.translate(i-s.startX,a-s.startY);this.moveEditorInDOM(s);s.div.focus()}dragover(t){t.preventDefault()}destroy(){this.#Ct.getActive()?.parent===this&&this.#Ct.setActiveEditor(null);for(const t of this.#St.values()){this.#At?.removePointerInTextLayer(t.contentDiv);t.setParent(null);t.isAttachedToDOM=!1;t.div.remove()}this.div=null;this.#St.clear();this.#Ct.removeLayer(this)}#Pt(){this.#Et=!0;for(const t of this.#St.values())t.isEmpty()&&t.remove();this.#Et=!1}render({viewport:t}){this.viewport=t;(0,o.setLayerDimensions)(this.div,t);(0,i.bindEvents)(this,this.div,["dragover","drop"]);for(const t of this.#Ct.getEditors(this.pageIndex))this.add(t);this.updateMode()}update({viewport:t}){this.#Ct.commitOrRemove();this.viewport=t;(0,o.setLayerDimensions)(this.div,{rotation:t.rotation});this.updateMode()}get pageDimensions(){const{pageWidth:t,pageHeight:e}=this.viewport.rawDims;return[t,e]}}e.AnnotationEditorLayer=AnnotationEditorLayer},(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0});e.FreeTextEditor=void 0;var n=s(1),i=s(5),a=s(4);class FreeTextEditor extends a.AnnotationEditor{#Ft=this.editorDivBlur.bind(this);#Rt=this.editorDivFocus.bind(this);#Mt=this.editorDivInput.bind(this);#Dt=this.editorDivKeydown.bind(this);#It;#Ot="";#Lt=`${this.id}-editor`;#Nt=!1;#jt;static _freeTextDefaultContent="";static _l10nPromise;static _internalPadding=0;static _defaultColor=null;static _defaultFontSize=10;static _keyboardManager=new i.KeyboardManager([[["ctrl+Enter","mac+meta+Enter","Escape","mac+Escape"],FreeTextEditor.prototype.commitOrRemove]]);static _type="freetext";constructor(t){super({...t,name:"freeTextEditor"});this.#It=t.color||FreeTextEditor._defaultColor||a.AnnotationEditor._defaultLineColor;this.#jt=t.fontSize||FreeTextEditor._defaultFontSize}static initialize(t){this._l10nPromise=new Map(["free_text2_default_content","editor_free_text2_aria_label"].map((e=>[e,t.get(e)])));const e=getComputedStyle(document.documentElement);this._internalPadding=parseFloat(e.getPropertyValue("--freetext-padding"))}static updateDefaultParams(t,e){switch(t){case n.AnnotationEditorParamsType.FREETEXT_SIZE:FreeTextEditor._defaultFontSize=e;break;case n.AnnotationEditorParamsType.FREETEXT_COLOR:FreeTextEditor._defaultColor=e}}updateParams(t,e){switch(t){case n.AnnotationEditorParamsType.FREETEXT_SIZE:this.#Ut(e);break;case n.AnnotationEditorParamsType.FREETEXT_COLOR:this.#Bt(e)}}static get defaultPropertiesToUpdate(){return[[n.AnnotationEditorParamsType.FREETEXT_SIZE,FreeTextEditor._defaultFontSize],[n.AnnotationEditorParamsType.FREETEXT_COLOR,FreeTextEditor._defaultColor||a.AnnotationEditor._defaultLineColor]]}get propertiesToUpdate(){return[[n.AnnotationEditorParamsType.FREETEXT_SIZE,this.#jt],[n.AnnotationEditorParamsType.FREETEXT_COLOR,this.#It]]}#Ut(t){const setFontsize=t=>{this.editorDiv.style.fontSize=`calc(${t}px * var(--scale-factor))`;this.translate(0,-(t-this.#jt)*this.parentScale);this.#jt=t;this.#qt()},e=this.#jt;this.addCommands({cmd:()=>{setFontsize(t)},undo:()=>{setFontsize(e)},mustExec:!0,type:n.AnnotationEditorParamsType.FREETEXT_SIZE,overwriteIfSameType:!0,keepUndo:!0})}#Bt(t){const e=this.#It;this.addCommands({cmd:()=>{this.#It=this.editorDiv.style.color=t},undo:()=>{this.#It=this.editorDiv.style.color=e},mustExec:!0,type:n.AnnotationEditorParamsType.FREETEXT_COLOR,overwriteIfSameType:!0,keepUndo:!0})}getInitialTranslation(){const t=this.parentScale;return[-FreeTextEditor._internalPadding*t,-(FreeTextEditor._internalPadding+this.#jt)*t]}rebuild(){super.rebuild();null!==this.div&&(this.isAttachedToDOM||this.parent.add(this))}enableEditMode(){if(!this.isInEditMode()){this.parent.setEditingState(!1);this.parent.updateToolbar(n.AnnotationEditorType.FREETEXT);super.enableEditMode();this.overlayDiv.classList.remove("enabled");this.editorDiv.contentEditable=!0;this.div.draggable=!1;this.div.removeAttribute("aria-activedescendant");this.editorDiv.addEventListener("keydown",this.#Dt);this.editorDiv.addEventListener("focus",this.#Rt);this.editorDiv.addEventListener("blur",this.#Ft);this.editorDiv.addEventListener("input",this.#Mt)}}disableEditMode(){if(this.isInEditMode()){this.parent.setEditingState(!0);super.disableEditMode();this.overlayDiv.classList.add("enabled");this.editorDiv.contentEditable=!1;this.div.setAttribute("aria-activedescendant",this.#Lt);this.div.draggable=!0;this.editorDiv.removeEventListener("keydown",this.#Dt);this.editorDiv.removeEventListener("focus",this.#Rt);this.editorDiv.removeEventListener("blur",this.#Ft);this.editorDiv.removeEventListener("input",this.#Mt);this.div.focus({preventScroll:!0});this.isEditing=!1;this.parent.div.classList.add("freeTextEditing")}}focusin(t){super.focusin(t);t.target!==this.editorDiv&&this.editorDiv.focus()}onceAdded(){if(!this.width){this.enableEditMode();this.editorDiv.focus()}}isEmpty(){return!this.editorDiv||""===this.editorDiv.innerText.trim()}remove(){this.isEditing=!1;this.parent.setEditingState(!0);this.parent.div.classList.add("freeTextEditing");super.remove()}#Wt(){const t=this.editorDiv.getElementsByTagName("div");if(0===t.length)return this.editorDiv.innerText;const e=[];for(const s of t)e.push(s.innerText.replace(/\r\n?|\n/,""));return e.join("\n")}#qt(){const[t,e]=this.parentDimensions;let s;if(this.isAttachedToDOM)s=this.div.getBoundingClientRect();else{const{currentLayer:t,div:e}=this,n=e.style.display;e.style.display="hidden";t.div.append(this.div);s=e.getBoundingClientRect();e.remove();e.style.display=n}this.width=s.width/t;this.height=s.height/e}commit(){if(this.isInEditMode()){super.commit();if(!this.#Nt){this.#Nt=!0;this.parent.addUndoableEditor(this)}this.disableEditMode();this.#Ot=this.#Wt().trimEnd();this.#qt()}}shouldGetKeyboardEvents(){return this.isInEditMode()}dblclick(t){this.enableEditMode();this.editorDiv.focus()}keydown(t){if(t.target===this.div&&"Enter"===t.key){this.enableEditMode();this.editorDiv.focus()}}editorDivKeydown(t){FreeTextEditor._keyboardManager.exec(this,t)}editorDivFocus(t){this.isEditing=!0}editorDivBlur(t){this.isEditing=!1}editorDivInput(t){this.parent.div.classList.toggle("freeTextEditing",this.isEmpty())}disableEditing(){this.editorDiv.setAttribute("role","comment");this.editorDiv.removeAttribute("aria-multiline")}enableEditing(){this.editorDiv.setAttribute("role","textbox");this.editorDiv.setAttribute("aria-multiline",!0)}render(){if(this.div)return this.div;let t,e;if(this.width){t=this.x;e=this.y}super.render();this.editorDiv=document.createElement("div");this.editorDiv.className="internal";this.editorDiv.setAttribute("id",this.#Lt);this.enableEditing();FreeTextEditor._l10nPromise.get("editor_free_text2_aria_label").then((t=>this.editorDiv?.setAttribute("aria-label",t)));FreeTextEditor._l10nPromise.get("free_text2_default_content").then((t=>this.editorDiv?.setAttribute("default-content",t)));this.editorDiv.contentEditable=!0;const{style:s}=this.editorDiv;s.fontSize=`calc(${this.#jt}px * var(--scale-factor))`;s.color=this.#It;this.div.append(this.editorDiv);this.overlayDiv=document.createElement("div");this.overlayDiv.classList.add("overlay","enabled");this.div.append(this.overlayDiv);(0,i.bindEvents)(this,this.div,["dblclick","keydown"]);if(this.width){const[s,n]=this.parentDimensions;this.setAt(t*s,e*n,this.width*s,this.height*n);for(const t of this.#Ot.split("\n")){const e=document.createElement("div");e.append(t?document.createTextNode(t):document.createElement("br"));this.editorDiv.append(e)}this.div.draggable=!0;this.editorDiv.contentEditable=!1}else{this.div.draggable=!1;this.editorDiv.contentEditable=!0}return this.div}get contentDiv(){return this.editorDiv}static deserialize(t,e,s){const i=super.deserialize(t,e,s);i.#jt=t.fontSize;i.#It=n.Util.makeHexColor(...t.color);i.#Ot=t.value;return i}serialize(){if(this.isEmpty())return null;const t=FreeTextEditor._internalPadding*this.parentScale,e=this.getRect(t,t),s=a.AnnotationEditor._colorManager.convert(this.isAttachedToDOM?getComputedStyle(this.editorDiv).color:this.#It);return{annotationType:n.AnnotationEditorType.FREETEXT,color:s,fontSize:this.#jt,value:this.#Ot,pageIndex:this.pageIndex,rect:e,rotation:this.rotation}}}e.FreeTextEditor=FreeTextEditor},(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0});e.InkEditor=void 0;Object.defineProperty(e,"fitCurve",{enumerable:!0,get:function(){return a.fitCurve}});var n=s(1),i=s(4),a=s(25),r=s(5);const o=16;class InkEditor extends i.AnnotationEditor{#Ht=0;#Gt=0;#zt=0;#Vt=this.canvasPointermove.bind(this);#Xt=this.canvasPointerleave.bind(this);#$t=this.canvasPointerup.bind(this);#Yt=this.canvasPointerdown.bind(this);#Kt=!1;#Jt=!1;#Qt=null;#Zt=null;#te=0;#ee=0;#se=null;static _defaultColor=null;static _defaultOpacity=1;static _defaultThickness=1;static _l10nPromise;static _type="ink";constructor(t){super({...t,name:"inkEditor"});this.color=t.color||null;this.thickness=t.thickness||null;this.opacity=t.opacity||null;this.paths=[];this.bezierPath2D=[];this.currentPath=[];this.scaleFactor=1;this.translationX=this.translationY=0;this.x=0;this.y=0}static initialize(t){this._l10nPromise=new Map(["editor_ink_canvas_aria_label","editor_ink2_aria_label"].map((e=>[e,t.get(e)])))}static updateDefaultParams(t,e){switch(t){case n.AnnotationEditorParamsType.INK_THICKNESS:InkEditor._defaultThickness=e;break;case n.AnnotationEditorParamsType.INK_COLOR:InkEditor._defaultColor=e;break;case n.AnnotationEditorParamsType.INK_OPACITY:InkEditor._defaultOpacity=e/100}}updateParams(t,e){switch(t){case n.AnnotationEditorParamsType.INK_THICKNESS:this.#ne(e);break;case n.AnnotationEditorParamsType.INK_COLOR:this.#Bt(e);break;case n.AnnotationEditorParamsType.INK_OPACITY:this.#ie(e)}}static get defaultPropertiesToUpdate(){return[[n.AnnotationEditorParamsType.INK_THICKNESS,InkEditor._defaultThickness],[n.AnnotationEditorParamsType.INK_COLOR,InkEditor._defaultColor||i.AnnotationEditor._defaultLineColor],[n.AnnotationEditorParamsType.INK_OPACITY,Math.round(100*InkEditor._defaultOpacity)]]}get propertiesToUpdate(){return[[n.AnnotationEditorParamsType.INK_THICKNESS,this.thickness||InkEditor._defaultThickness],[n.AnnotationEditorParamsType.INK_COLOR,this.color||InkEditor._defaultColor||i.AnnotationEditor._defaultLineColor],[n.AnnotationEditorParamsType.INK_OPACITY,Math.round(100*(this.opacity??InkEditor._defaultOpacity))]]}#ne(t){const e=this.thickness;this.addCommands({cmd:()=>{this.thickness=t;this.#ae()},undo:()=>{this.thickness=e;this.#ae()},mustExec:!0,type:n.AnnotationEditorParamsType.INK_THICKNESS,overwriteIfSameType:!0,keepUndo:!0})}#Bt(t){const e=this.color;this.addCommands({cmd:()=>{this.color=t;this.#re()},undo:()=>{this.color=e;this.#re()},mustExec:!0,type:n.AnnotationEditorParamsType.INK_COLOR,overwriteIfSameType:!0,keepUndo:!0})}#ie(t){t/=100;const e=this.opacity;this.addCommands({cmd:()=>{this.opacity=t;this.#re()},undo:()=>{this.opacity=e;this.#re()},mustExec:!0,type:n.AnnotationEditorParamsType.INK_OPACITY,overwriteIfSameType:!0,keepUndo:!0})}rebuild(){super.rebuild();if(null!==this.div){if(!this.canvas){this.#oe();this.#le()}if(!this.isAttachedToDOM){this.parent.add(this);this.#ce()}this.#ae()}}remove(){if(null!==this.canvas){this.isEmpty()||this.commit();this.canvas.width=this.canvas.height=0;this.canvas.remove();this.canvas=null;this.#Zt.disconnect();this.#Zt=null;super.remove()}}setParent(t){!this.parent&&t?this._uiManager.removeShouldRescale(this):this.parent&&null===t&&this._uiManager.addShouldRescale(this);super.setParent(t)}onScaleChanging(){const[t,e]=this.parentDimensions,s=this.width*t,n=this.height*e;this.setDimensions(s,n)}enableEditMode(){if(!this.#Kt&&null!==this.canvas){super.enableEditMode();this.div.draggable=!1;this.canvas.addEventListener("pointerdown",this.#Yt);this.canvas.addEventListener("pointerup",this.#$t)}}disableEditMode(){if(this.isInEditMode()&&null!==this.canvas){super.disableEditMode();this.div.draggable=!this.isEmpty();this.div.classList.remove("editing");this.canvas.removeEventListener("pointerdown",this.#Yt);this.canvas.removeEventListener("pointerup",this.#$t)}}onceAdded(){this.div.draggable=!this.isEmpty()}isEmpty(){return 0===this.paths.length||1===this.paths.length&&0===this.paths[0].length}#he(){const{parentRotation:t,parentDimensions:[e,s]}=this;switch(t){case 90:return[0,s,s,e];case 180:return[e,s,e,s];case 270:return[e,0,s,e];default:return[0,0,e,s]}}#de(){const{ctx:t,color:e,opacity:s,thickness:n,parentScale:i,scaleFactor:a}=this;t.lineWidth=n*i/a;t.lineCap="round";t.lineJoin="round";t.miterLimit=10;t.strokeStyle=`${e}${(0,r.opacityToHex)(s)}`}#ue(t,e){this.isEditing=!0;if(!this.#Jt){this.#Jt=!0;this.#ce();this.thickness||=InkEditor._defaultThickness;this.color||=InkEditor._defaultColor||i.AnnotationEditor._defaultLineColor;this.opacity??=InkEditor._defaultOpacity}this.currentPath.push([t,e]);this.#Qt=null;this.#de();this.ctx.beginPath();this.ctx.moveTo(t,e);this.#se=()=>{if(this.#se){if(this.#Qt){if(this.isEmpty()){this.ctx.setTransform(1,0,0,1,0,0);this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height)}else this.#re();this.ctx.lineTo(...this.#Qt);this.#Qt=null;this.ctx.stroke()}window.requestAnimationFrame(this.#se)}};window.requestAnimationFrame(this.#se)}#pe(t,e){const[s,n]=this.currentPath.at(-1);if(t!==s||e!==n){this.currentPath.push([t,e]);this.#Qt=[t,e]}}#ge(t,e){this.ctx.closePath();this.#se=null;t=Math.min(Math.max(t,0),this.canvas.width);e=Math.min(Math.max(e,0),this.canvas.height);const[s,n]=this.currentPath.at(-1);t===s&&e===n||this.currentPath.push([t,e]);let i;if(1!==this.currentPath.length)i=(0,a.fitCurve)(this.currentPath,30,null);else{const s=[t,e];i=[[s,s.slice(),s.slice(),s]]}const r=InkEditor.#me(i);this.currentPath.length=0;this.addCommands({cmd:()=>{this.paths.push(i);this.bezierPath2D.push(r);this.rebuild()},undo:()=>{this.paths.pop();this.bezierPath2D.pop();if(0===this.paths.length)this.remove();else{if(!this.canvas){this.#oe();this.#le()}this.#ae()}},mustExec:!0})}#re(){if(this.isEmpty()){this.#fe();return}this.#de();const{canvas:t,ctx:e}=this;e.setTransform(1,0,0,1,0,0);e.clearRect(0,0,t.width,t.height);this.#fe();for(const t of this.bezierPath2D)e.stroke(t)}commit(){if(!this.#Kt){super.commit();this.isEditing=!1;this.disableEditMode();this.setInForeground();this.#Kt=!0;this.div.classList.add("disabled");this.#ae(!0);this.parent.addInkEditorIfNeeded(!0);this.parent.moveEditorInDOM(this);this.div.focus({preventScroll:!0})}}focusin(t){super.focusin(t);this.enableEditMode()}canvasPointerdown(t){if(0===t.button&&this.isInEditMode()&&!this.#Kt){this.setInForeground();"mouse"!==t.type&&this.div.focus();t.stopPropagation();this.canvas.addEventListener("pointerleave",this.#Xt);this.canvas.addEventListener("pointermove",this.#Vt);this.#ue(t.offsetX,t.offsetY)}}canvasPointermove(t){t.stopPropagation();this.#pe(t.offsetX,t.offsetY)}canvasPointerup(t){if(0===t.button&&this.isInEditMode()&&0!==this.currentPath.length){t.stopPropagation();this.#be(t);this.setInBackground()}}canvasPointerleave(t){this.#be(t);this.setInBackground()}#be(t){this.#ge(t.offsetX,t.offsetY);this.canvas.removeEventListener("pointerleave",this.#Xt);this.canvas.removeEventListener("pointermove",this.#Vt);this.addToAnnotationStorage()}#oe(){this.canvas=document.createElement("canvas");this.canvas.width=this.canvas.height=0;this.canvas.className="inkEditorCanvas";InkEditor._l10nPromise.get("editor_ink_canvas_aria_label").then((t=>this.canvas?.setAttribute("aria-label",t)));this.div.append(this.canvas);this.ctx=this.canvas.getContext("2d")}#le(){let t=null;this.#Zt=new ResizeObserver((e=>{const s=e[0].contentRect;if(s.width&&s.height){null!==t&&clearTimeout(t);t=setTimeout((()=>{this.fixDims();t=null}),100);this.setDimensions(s.width,s.height)}}));this.#Zt.observe(this.div)}render(){if(this.div)return this.div;let t,e;if(this.width){t=this.x;e=this.y}super.render();InkEditor._l10nPromise.get("editor_ink2_aria_label").then((t=>this.div?.setAttribute("aria-label",t)));const[s,n,i,a]=this.#he();this.setAt(s,n,0,0);this.setDims(i,a);this.#oe();if(this.width){const[s,n]=this.parentDimensions;this.setAt(t*s,e*n,this.width*s,this.height*n);this.#Jt=!0;this.#ce();this.setDims(this.width*s,this.height*n);this.#re();this.#Ae();this.div.classList.add("disabled")}else{this.div.classList.add("editing");this.enableEditMode()}this.#le();return this.div}#ce(){if(!this.#Jt)return;const[t,e]=this.parentDimensions;this.canvas.width=Math.ceil(this.width*t);this.canvas.height=Math.ceil(this.height*e);this.#fe()}setDimensions(t,e){const s=Math.round(t),n=Math.round(e);if(this.#te===s&&this.#ee===n)return;this.#te=s;this.#ee=n;this.canvas.style.visibility="hidden";if(this.#Ht&&Math.abs(this.#Ht-t/e)>.01){e=Math.ceil(t/this.#Ht);this.setDims(t,e)}const[i,a]=this.parentDimensions;this.width=t/i;this.height=e/a;this.#Kt&&this.#_e(t,e);this.#ce();this.#re();this.canvas.style.visibility="visible"}#_e(t,e){const s=this.#ye(),n=(t-s)/this.#zt,i=(e-s)/this.#Gt;this.scaleFactor=Math.min(n,i)}#fe(){const t=this.#ye()/2;this.ctx.setTransform(this.scaleFactor,0,0,this.scaleFactor,this.translationX*this.scaleFactor+t,this.translationY*this.scaleFactor+t)}static#me(t){const e=new Path2D;for(let s=0,n=t.length;s=1){t.minHeight="16px";t.minWidth=`${Math.round(this.#Ht*o)}px`}else{t.minWidth="16px";t.minHeight=`${Math.round(o/this.#Ht)}px`}}static deserialize(t,e,s){const i=super.deserialize(t,e,s);i.thickness=t.thickness;i.color=n.Util.makeHexColor(...t.color);i.opacity=t.opacity;const[a,r]=i.pageDimensions,l=i.width*a,c=i.height*r,h=i.parentScale,d=t.thickness/2;i.#Ht=l/c;i.#Kt=!0;i.#te=Math.round(l);i.#ee=Math.round(c);for(const{bezier:e}of t.paths){const t=[];i.paths.push(t);let s=h*(e[0]-d),n=h*(c-e[1]-d);for(let i=2,a=e.length;i{Object.defineProperty(e,"__esModule",{value:!0});e.fitCurve=void 0;const n=s(26);e.fitCurve=n},t=>{function fitCubic(t,e,s,n,i){var a,r,o,l,c,h,d,u,p,g,m,f,b;if(2===t.length){f=maths.vectorLen(maths.subtract(t[0],t[1]))/3;return[a=[t[0],maths.addArrays(t[0],maths.mulItems(e,f)),maths.addArrays(t[1],maths.mulItems(s,f)),t[1]]]}r=function chordLengthParameterize(t){var e,s,n,i=[];t.forEach(((t,a)=>{e=a?s+maths.vectorLen(maths.subtract(t,n)):0;i.push(e);s=e;n=t}));i=i.map((t=>t/s));return i}(t);[a,l,h]=generateAndReport(t,r,r,e,s,i);if(0===l||l.9999&&t<1.0001)break}c=l;d=h}}m=[];if((u=maths.subtract(t[h-1],t[h+1])).every((t=>0===t))){u=maths.subtract(t[h-1],t[h]);[u[0],u[1]]=[-u[1],u[0]]}p=maths.normalize(u);g=maths.mulItems(p,-1);return m=(m=m.concat(fitCubic(t.slice(0,h+1),e,p,n,i))).concat(fitCubic(t.slice(h),g,s,n,i))}function generateAndReport(t,e,s,n,i,a){var r,o,l;r=function generateBezier(t,e,s,n){var i,a,r,o,l,c,h,d,u,p,g,m,f,b,A,_,y,v=t[0],S=t[t.length-1];i=[v,null,null,S];a=maths.zeros_Xx2x2(e.length);for(f=0,b=e.length;fi){i=n;a=o}}return[i,a]}(t,r,e);a&&a({bez:r,points:t,params:e,maxErr:o,maxPoint:l});return[r,o,l]}function reparameterize(t,e,s){return s.map(((s,n)=>newtonRaphsonRootFind(t,e[n],s)))}function newtonRaphsonRootFind(t,e,s){var n=maths.subtract(bezier.q(t,s),e),i=bezier.qprime(t,s),a=maths.mulMatrix(n,i),r=maths.sum(maths.squareItems(i))+2*maths.mulMatrix(n,bezier.qprimeprime(t,s));return 0===r?s:s-a/r}var mapTtoRelativeDistances=function(t,e){for(var s,n=[0],i=t[0],a=0,r=1;r<=e;r++){s=bezier.q(t,r/e);a+=maths.vectorLen(maths.subtract(s,i));n.push(a);i=s}return n=n.map((t=>t/a))};function find_t(t,e,s,n){if(e<0)return 0;if(e>1)return 1;for(var i,a,r,o,l=1;l<=n;l++)if(e<=s[l]){r=(l-1)/n;a=l/n;o=(e-(i=s[l-1]))/(s[l]-i)*(a-r)+r;break}return o}function createTangent(t,e){return maths.normalize(maths.subtract(t,e))}class maths{static zeros_Xx2x2(t){for(var e=[];t--;)e.push([0,0]);return e}static mulItems(t,e){return t.map((t=>t*e))}static mulMatrix(t,e){return t.reduce(((t,s,n)=>t+s*e[n]),0)}static subtract(t,e){return t.map(((t,s)=>t-e[s]))}static addArrays(t,e){return t.map(((t,s)=>t+e[s]))}static addItems(t,e){return t.map((t=>t+e))}static sum(t){return t.reduce(((t,e)=>t+e))}static dot(t,e){return maths.mulMatrix(t,e)}static vectorLen(t){return Math.hypot(...t)}static divItems(t,e){return t.map((t=>t/e))}static squareItems(t){return t.map((t=>t*t))}static normalize(t){return this.divItems(t,this.vectorLen(t))}}class bezier{static q(t,e){var s=1-e,n=maths.mulItems(t[0],s*s*s),i=maths.mulItems(t[1],3*s*s*e),a=maths.mulItems(t[2],3*s*e*e),r=maths.mulItems(t[3],e*e*e);return maths.addArrays(maths.addArrays(n,i),maths.addArrays(a,r))}static qprime(t,e){var s=1-e,n=maths.mulItems(maths.subtract(t[1],t[0]),3*s*s),i=maths.mulItems(maths.subtract(t[2],t[1]),6*s*e),a=maths.mulItems(maths.subtract(t[3],t[2]),3*e*e);return maths.addArrays(maths.addArrays(n,i),a)}static qprimeprime(t,e){return maths.addArrays(maths.mulItems(maths.addArrays(maths.subtract(t[2],maths.mulItems(t[1],2)),t[0]),6*(1-e)),maths.mulItems(maths.addArrays(maths.subtract(t[3],maths.mulItems(t[2],2)),t[1]),6*e))}}t.exports=function fitCurve(t,e,s){if(!Array.isArray(t))throw new TypeError("First argument should be an array");t.forEach((e=>{if(!Array.isArray(e)||e.some((t=>"number"!=typeof t))||e.length!==t[0].length)throw Error("Each point should be an array of numbers. Each point should have the same amount of numbers.")}));if((t=t.filter(((e,s)=>0===s||!e.every(((e,n)=>e===t[s-1][n]))))).length<2)return[];const n=t.length,i=createTangent(t[1],t[0]),a=createTangent(t[n-2],t[n-1]);return fitCubic(t,i,a,e,s)};t.exports.fitCubic=fitCubic;t.exports.createTangent=createTangent},(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0});e.AnnotationLayer=void 0;var n=s(1),i=s(6),a=s(3),r=s(28),o=s(29);const l=1e3,c=new WeakSet;function getRectDims(t){return{width:t[2]-t[0],height:t[3]-t[1]}}class AnnotationElementFactory{static create(t){switch(t.data.annotationType){case n.AnnotationType.LINK:return new LinkAnnotationElement(t);case n.AnnotationType.TEXT:return new TextAnnotationElement(t);case n.AnnotationType.WIDGET:switch(t.data.fieldType){case"Tx":return new TextWidgetAnnotationElement(t);case"Btn":return t.data.radioButton?new RadioButtonWidgetAnnotationElement(t):t.data.checkBox?new CheckboxWidgetAnnotationElement(t):new PushButtonWidgetAnnotationElement(t);case"Ch":return new ChoiceWidgetAnnotationElement(t)}return new WidgetAnnotationElement(t);case n.AnnotationType.POPUP:return new PopupAnnotationElement(t);case n.AnnotationType.FREETEXT:return new FreeTextAnnotationElement(t);case n.AnnotationType.LINE:return new LineAnnotationElement(t);case n.AnnotationType.SQUARE:return new SquareAnnotationElement(t);case n.AnnotationType.CIRCLE:return new CircleAnnotationElement(t);case n.AnnotationType.POLYLINE:return new PolylineAnnotationElement(t);case n.AnnotationType.CARET:return new CaretAnnotationElement(t);case n.AnnotationType.INK:return new InkAnnotationElement(t);case n.AnnotationType.POLYGON:return new PolygonAnnotationElement(t);case n.AnnotationType.HIGHLIGHT:return new HighlightAnnotationElement(t);case n.AnnotationType.UNDERLINE:return new UnderlineAnnotationElement(t);case n.AnnotationType.SQUIGGLY:return new SquigglyAnnotationElement(t);case n.AnnotationType.STRIKEOUT:return new StrikeOutAnnotationElement(t);case n.AnnotationType.STAMP:return new StampAnnotationElement(t);case n.AnnotationType.FILEATTACHMENT:return new FileAttachmentAnnotationElement(t);default:return new AnnotationElement(t)}}}class AnnotationElement{constructor(t,{isRenderable:e=!1,ignoreBorder:s=!1,createQuadrilaterals:n=!1}={}){this.isRenderable=e;this.data=t.data;this.layer=t.layer;this.page=t.page;this.viewport=t.viewport;this.linkService=t.linkService;this.downloadManager=t.downloadManager;this.imageResourcesPath=t.imageResourcesPath;this.renderForms=t.renderForms;this.svgFactory=t.svgFactory;this.annotationStorage=t.annotationStorage;this.enableScripting=t.enableScripting;this.hasJSActions=t.hasJSActions;this._fieldObjects=t.fieldObjects;e&&(this.container=this._createContainer(s));n&&(this.quadrilaterals=this._createQuadrilaterals(s))}_createContainer(t=!1){const{data:e,page:s,viewport:i}=this,a=document.createElement("section");a.setAttribute("data-annotation-id",e.id);const{pageWidth:r,pageHeight:o,pageX:l,pageY:c}=i.rawDims,{width:h,height:d}=getRectDims(e.rect),u=n.Util.normalizeRect([e.rect[0],s.view[3]-e.rect[1]+s.view[1],e.rect[2],s.view[3]-e.rect[3]+s.view[1]]);if(!t&&e.borderStyle.width>0){a.style.borderWidth=`${e.borderStyle.width}px`;const t=e.borderStyle.horizontalCornerRadius,s=e.borderStyle.verticalCornerRadius;if(t>0||s>0){const e=`calc(${t}px * var(--scale-factor)) / calc(${s}px * var(--scale-factor))`;a.style.borderRadius=e}else if(this instanceof RadioButtonWidgetAnnotationElement){const t=`calc(${h}px * var(--scale-factor)) / calc(${d}px * var(--scale-factor))`;a.style.borderRadius=t}switch(e.borderStyle.style){case n.AnnotationBorderStyleType.SOLID:a.style.borderStyle="solid";break;case n.AnnotationBorderStyleType.DASHED:a.style.borderStyle="dashed";break;case n.AnnotationBorderStyleType.BEVELED:(0,n.warn)("Unimplemented border style: beveled");break;case n.AnnotationBorderStyleType.INSET:(0,n.warn)("Unimplemented border style: inset");break;case n.AnnotationBorderStyleType.UNDERLINE:a.style.borderBottomStyle="solid"}const i=e.borderColor||null;i?a.style.borderColor=n.Util.makeHexColor(0|i[0],0|i[1],0|i[2]):a.style.borderWidth=0}a.style.left=100*(u[0]-l)/r+"%";a.style.top=100*(u[1]-c)/o+"%";const{rotation:p}=e;if(e.hasOwnCanvas||0===p){a.style.width=100*h/r+"%";a.style.height=100*d/o+"%"}else this.setRotation(p,a);return a}setRotation(t,e=this.container){const{pageWidth:s,pageHeight:n}=this.viewport.rawDims,{width:i,height:a}=getRectDims(this.data.rect);let r,o;if(t%180==0){r=100*i/s;o=100*a/n}else{r=100*a/s;o=100*i/n}e.style.width=`${r}%`;e.style.height=`${o}%`;e.setAttribute("data-main-rotation",(360-t)%360)}get _commonActions(){const setColor=(t,e,s)=>{const n=s.detail[t];s.target.style[e]=r.ColorConverters[`${n[0]}_HTML`](n.slice(1))};return(0,n.shadow)(this,"_commonActions",{display:t=>{const e=t.detail.display%2==1;this.container.style.visibility=e?"hidden":"visible";this.annotationStorage.setValue(this.data.id,{hidden:e,print:0===t.detail.display||3===t.detail.display})},print:t=>{this.annotationStorage.setValue(this.data.id,{print:t.detail.print})},hidden:t=>{this.container.style.visibility=t.detail.hidden?"hidden":"visible";this.annotationStorage.setValue(this.data.id,{hidden:t.detail.hidden})},focus:t=>{setTimeout((()=>t.target.focus({preventScroll:!1})),0)},userName:t=>{t.target.title=t.detail.userName},readonly:t=>{t.detail.readonly?t.target.setAttribute("readonly",""):t.target.removeAttribute("readonly")},required:t=>{this._setRequired(t.target,t.detail.required)},bgColor:t=>{setColor("bgColor","backgroundColor",t)},fillColor:t=>{setColor("fillColor","backgroundColor",t)},fgColor:t=>{setColor("fgColor","color",t)},textColor:t=>{setColor("textColor","color",t)},borderColor:t=>{setColor("borderColor","borderColor",t)},strokeColor:t=>{setColor("strokeColor","borderColor",t)},rotation:t=>{const e=t.detail.rotation;this.setRotation(e);this.annotationStorage.setValue(this.data.id,{rotation:e})}})}_dispatchEventFromSandbox(t,e){const s=this._commonActions;for(const n of Object.keys(e.detail)){(t[n]||s[n])?.(e)}}_setDefaultPropertiesFromJS(t){if(!this.enableScripting)return;const e=this.annotationStorage.getRawValue(this.data.id);if(!e)return;const s=this._commonActions;for(const[n,i]of Object.entries(e)){const a=s[n];if(a){a({detail:{[n]:i},target:t});delete e[n]}}}_createQuadrilaterals(t=!1){if(!this.data.quadPoints)return null;const e=[],s=this.data.rect;for(const s of this.data.quadPoints){this.data.rect=[s[2].x,s[2].y,s[1].x,s[1].y];e.push(this._createContainer(t))}this.data.rect=s;return e}_createPopup(t,e){let s=this.container;if(this.quadrilaterals){t=t||this.quadrilaterals;s=this.quadrilaterals[0]}if(!t){(t=document.createElement("div")).className="popupTriggerArea";s.append(t)}const n=new PopupElement({container:s,trigger:t,color:e.color,titleObj:e.titleObj,modificationDate:e.modificationDate,contentsObj:e.contentsObj,richText:e.richText,hideWrapper:!0}).render();n.style.left="100%";s.append(n)}_renderQuadrilaterals(t){for(const e of this.quadrilaterals)e.className=t;return this.quadrilaterals}render(){(0,n.unreachable)("Abstract method `AnnotationElement.render` called")}_getElementsByName(t,e=null){const s=[];if(this._fieldObjects){const i=this._fieldObjects[t];if(i)for(const{page:t,id:a,exportValues:r}of i){if(-1===t)continue;if(a===e)continue;const i="string"==typeof r?r:null,o=document.querySelector(`[data-element-id="${a}"]`);!o||c.has(o)?s.push({id:a,exportValue:i,domElement:o}):(0,n.warn)(`_getElementsByName - element not allowed: ${a}`)}return s}for(const n of document.getElementsByName(t)){const{exportValue:t}=n,i=n.getAttribute("data-element-id");i!==e&&(c.has(n)&&s.push({id:i,exportValue:t,domElement:n}))}return s}}class LinkAnnotationElement extends AnnotationElement{constructor(t,e=null){super(t,{isRenderable:!0,ignoreBorder:!!e?.ignoreBorder,createQuadrilaterals:!0});this.isTooltipOnly=t.data.isTooltipOnly}render(){const{data:t,linkService:e}=this,s=document.createElement("a");s.setAttribute("data-element-id",t.id);let n=!1;if(t.url){e.addLinkAttributes(s,t.url,t.newWindow);n=!0}else if(t.action){this._bindNamedAction(s,t.action);n=!0}else if(t.attachment){this._bindAttachment(s,t.attachment);n=!0}else if(t.setOCGState){this.#Ce(s,t.setOCGState);n=!0}else if(t.dest){this._bindLink(s,t.dest);n=!0}else{if(t.actions&&(t.actions.Action||t.actions["Mouse Up"]||t.actions["Mouse Down"])&&this.enableScripting&&this.hasJSActions){this._bindJSAction(s,t);n=!0}if(t.resetForm){this._bindResetFormAction(s,t.resetForm);n=!0}else if(this.isTooltipOnly&&!n){this._bindLink(s,"");n=!0}}if(this.quadrilaterals)return this._renderQuadrilaterals("linkAnnotation").map(((t,e)=>{const n=0===e?s:s.cloneNode();t.append(n);return t}));this.container.className="linkAnnotation";n&&this.container.append(s);return this.container}#Pe(){this.container.setAttribute("data-internal-link","")}_bindLink(t,e){t.href=this.linkService.getDestinationHash(e);t.onclick=()=>{e&&this.linkService.goToDestination(e);return!1};(e||""===e)&&this.#Pe()}_bindNamedAction(t,e){t.href=this.linkService.getAnchorUrl("");t.onclick=()=>{this.linkService.executeNamedAction(e);return!1};this.#Pe()}_bindAttachment(t,e){t.href=this.linkService.getAnchorUrl("");t.onclick=()=>{this.downloadManager?.openOrDownloadData(this.container,e.content,e.filename);return!1};this.#Pe()}#Ce(t,e){t.href=this.linkService.getAnchorUrl("");t.onclick=()=>{this.linkService.executeSetOCGState(e);return!1};this.#Pe()}_bindJSAction(t,e){t.href=this.linkService.getAnchorUrl("");const s=new Map([["Action","onclick"],["Mouse Up","onmouseup"],["Mouse Down","onmousedown"]]);for(const n of Object.keys(e.actions)){const i=s.get(n);i&&(t[i]=()=>{this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e.id,name:n}});return!1})}t.onclick||(t.onclick=()=>!1);this.#Pe()}_bindResetFormAction(t,e){const s=t.onclick;s||(t.href=this.linkService.getAnchorUrl(""));this.#Pe();if(this._fieldObjects)t.onclick=()=>{s?.();const{fields:t,refs:i,include:a}=e,r=[];if(0!==t.length||0!==i.length){const e=new Set(i);for(const s of t){const t=this._fieldObjects[s]||[];for(const{id:s}of t)e.add(s)}for(const t of Object.values(this._fieldObjects))for(const s of t)e.has(s.id)===a&&r.push(s)}else for(const t of Object.values(this._fieldObjects))r.push(...t);const o=this.annotationStorage,l=[];for(const t of r){const{id:e}=t;l.push(e);switch(t.type){case"text":{const s=t.defaultValue||"";o.setValue(e,{value:s});break}case"checkbox":case"radiobutton":{const s=t.defaultValue===t.exportValues;o.setValue(e,{value:s});break}case"combobox":case"listbox":{const s=t.defaultValue||"";o.setValue(e,{value:s});break}default:continue}const s=document.querySelector(`[data-element-id="${e}"]`);s&&(c.has(s)?s.dispatchEvent(new Event("resetform")):(0,n.warn)(`_bindResetFormAction - element not allowed: ${e}`))}this.enableScripting&&this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:"app",ids:l,name:"ResetForm"}});return!1};else{(0,n.warn)('_bindResetFormAction - "resetForm" action not supported, ensure that the `fieldObjects` parameter is provided.');s||(t.onclick=()=>!1)}}}class TextAnnotationElement extends AnnotationElement{constructor(t){super(t,{isRenderable:!!(t.data.hasPopup||t.data.titleObj?.str||t.data.contentsObj?.str||t.data.richText?.str)})}render(){this.container.className="textAnnotation";const t=document.createElement("img");t.src=this.imageResourcesPath+"annotation-"+this.data.name.toLowerCase()+".svg";t.alt="[{{type}} Annotation]";t.dataset.l10nId="text_annotation_type";t.dataset.l10nArgs=JSON.stringify({type:this.data.name});this.data.hasPopup||this._createPopup(t,this.data);this.container.append(t);return this.container}}class WidgetAnnotationElement extends AnnotationElement{render(){this.data.alternativeText&&(this.container.title=this.data.alternativeText);return this.container}_getKeyModifier(t){const{isWin:e,isMac:s}=n.FeatureTest.platform;return e&&t.ctrlKey||s&&t.metaKey}_setEventListener(t,e,s,n){e.includes("mouse")?t.addEventListener(e,(t=>{this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:this.data.id,name:s,value:n(t),shift:t.shiftKey,modifier:this._getKeyModifier(t)}})})):t.addEventListener(e,(t=>{this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:this.data.id,name:s,value:n(t)}})}))}_setEventListeners(t,e,s){for(const[n,i]of e)("Action"===i||this.data.actions?.[i])&&this._setEventListener(t,n,i,s)}_setBackgroundColor(t){const e=this.data.backgroundColor||null;t.style.backgroundColor=null===e?"transparent":n.Util.makeHexColor(e[0],e[1],e[2])}_setTextStyle(t){const e=["left","center","right"],{fontColor:s}=this.data.defaultAppearanceData,i=this.data.defaultAppearanceData.fontSize||9,a=t.style;let r;const roundToOneDecimal=t=>Math.round(10*t)/10;if(this.data.multiLine){const t=Math.abs(this.data.rect[3]-this.data.rect[1]-2),e=t/(Math.round(t/(n.LINE_FACTOR*i))||1);r=Math.min(i,roundToOneDecimal(e/n.LINE_FACTOR))}else{const t=Math.abs(this.data.rect[3]-this.data.rect[1]-2);r=Math.min(i,roundToOneDecimal(t/n.LINE_FACTOR))}a.fontSize=`calc(${r}px * var(--scale-factor))`;a.color=n.Util.makeHexColor(s[0],s[1],s[2]);null!==this.data.textAlignment&&(a.textAlign=e[this.data.textAlignment])}_setRequired(t,e){e?t.setAttribute("required",!0):t.removeAttribute("required");t.setAttribute("aria-required",e)}}class TextWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms||!t.data.hasAppearance&&!!t.data.fieldValue})}setPropertyOnSiblings(t,e,s,n){const i=this.annotationStorage;for(const a of this._getElementsByName(t.name,t.id)){a.domElement&&(a.domElement[e]=s);i.setValue(a.id,{[n]:s})}}render(){const t=this.annotationStorage,e=this.data.id;this.container.className="textWidgetAnnotation";let s=null;if(this.renderForms){const n=t.getValue(e,{value:this.data.fieldValue});let i=n.formattedValue||n.value||"";const a=t.getValue(e,{charLimit:this.data.maxLen}).charLimit;a&&i.length>a&&(i=i.slice(0,a));const r={userValue:i,formattedValue:null,lastCommittedValue:null,commitKey:1};if(this.data.multiLine){s=document.createElement("textarea");s.textContent=i;this.data.doNotScroll&&(s.style.overflowY="hidden")}else{s=document.createElement("input");s.type="text";s.setAttribute("value",i);this.data.doNotScroll&&(s.style.overflowX="hidden")}c.add(s);s.setAttribute("data-element-id",e);s.disabled=this.data.readOnly;s.name=this.data.fieldName;s.tabIndex=l;this._setRequired(s,this.data.required);a&&(s.maxLength=a);s.addEventListener("input",(n=>{t.setValue(e,{value:n.target.value});this.setPropertyOnSiblings(s,"value",n.target.value,"value")}));s.addEventListener("resetform",(t=>{const e=this.data.defaultFieldValue??"";s.value=r.userValue=e;r.formattedValue=null}));let blurListener=t=>{const{formattedValue:e}=r;null!=e&&(t.target.value=e);t.target.scrollLeft=0};if(this.enableScripting&&this.hasJSActions){s.addEventListener("focus",(t=>{const{target:e}=t;r.userValue&&(e.value=r.userValue);r.lastCommittedValue=e.value;r.commitKey=1}));s.addEventListener("updatefromsandbox",(s=>{const n={value(s){r.userValue=s.detail.value??"";t.setValue(e,{value:r.userValue.toString()});s.target.value=r.userValue},formattedValue(s){const{formattedValue:n}=s.detail;r.formattedValue=n;null!=n&&s.target!==document.activeElement&&(s.target.value=n);t.setValue(e,{formattedValue:n})},selRange(t){t.target.setSelectionRange(...t.detail.selRange)},charLimit:s=>{const{charLimit:n}=s.detail,{target:i}=s;if(0===n){i.removeAttribute("maxLength");return}i.setAttribute("maxLength",n);let a=r.userValue;if(a&&!(a.length<=n)){a=a.slice(0,n);i.value=r.userValue=a;t.setValue(e,{value:a});this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:a,willCommit:!0,commitKey:1,selStart:i.selectionStart,selEnd:i.selectionEnd}})}}};this._dispatchEventFromSandbox(n,s)}));s.addEventListener("keydown",(t=>{r.commitKey=1;let s=-1;"Escape"===t.key?s=0:"Enter"!==t.key||this.data.multiLine?"Tab"===t.key&&(r.commitKey=3):s=2;if(-1===s)return;const{value:n}=t.target;if(r.lastCommittedValue!==n){r.lastCommittedValue=n;r.userValue=n;this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:n,willCommit:!0,commitKey:s,selStart:t.target.selectionStart,selEnd:t.target.selectionEnd}})}}));const n=blurListener;blurListener=null;s.addEventListener("blur",(t=>{if(!t.relatedTarget)return;const{value:s}=t.target;r.userValue=s;r.lastCommittedValue!==s&&this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:s,willCommit:!0,commitKey:r.commitKey,selStart:t.target.selectionStart,selEnd:t.target.selectionEnd}});n(t)}));this.data.actions?.Keystroke&&s.addEventListener("beforeinput",(t=>{r.lastCommittedValue=null;const{data:s,target:n}=t,{value:i,selectionStart:a,selectionEnd:o}=n;let l=a,c=o;switch(t.inputType){case"deleteWordBackward":{const t=i.substring(0,a).match(/\w*[^\w]*$/);t&&(l-=t[0].length);break}case"deleteWordForward":{const t=i.substring(a).match(/^[^\w]*\w*/);t&&(c+=t[0].length);break}case"deleteContentBackward":a===o&&(l-=1);break;case"deleteContentForward":a===o&&(c+=1)}t.preventDefault();this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:i,change:s||"",willCommit:!1,selStart:l,selEnd:c}})}));this._setEventListeners(s,[["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"]],(t=>t.target.value))}blurListener&&s.addEventListener("blur",blurListener);if(this.data.comb){const t=(this.data.rect[2]-this.data.rect[0])/a;s.classList.add("comb");s.style.letterSpacing=`calc(${t}px * var(--scale-factor) - 1ch)`}}else{s=document.createElement("div");s.textContent=this.data.fieldValue;s.style.verticalAlign="middle";s.style.display="table-cell"}this._setTextStyle(s);this._setBackgroundColor(s);this._setDefaultPropertiesFromJS(s);this.container.append(s);return this.container}}class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms})}render(){const t=this.annotationStorage,e=this.data,s=e.id;let n=t.getValue(s,{value:e.exportValue===e.fieldValue}).value;if("string"==typeof n){n="Off"!==n;t.setValue(s,{value:n})}this.container.className="buttonWidgetAnnotation checkBox";const i=document.createElement("input");c.add(i);i.setAttribute("data-element-id",s);i.disabled=e.readOnly;this._setRequired(i,this.data.required);i.type="checkbox";i.name=e.fieldName;n&&i.setAttribute("checked",!0);i.setAttribute("exportValue",e.exportValue);i.tabIndex=l;i.addEventListener("change",(n=>{const{name:i,checked:a}=n.target;for(const n of this._getElementsByName(i,s)){const s=a&&n.exportValue===e.exportValue;n.domElement&&(n.domElement.checked=s);t.setValue(n.id,{value:s})}t.setValue(s,{value:a})}));i.addEventListener("resetform",(t=>{const s=e.defaultFieldValue||"Off";t.target.checked=s===e.exportValue}));if(this.enableScripting&&this.hasJSActions){i.addEventListener("updatefromsandbox",(e=>{const n={value(e){e.target.checked="Off"!==e.detail.value;t.setValue(s,{value:e.target.checked})}};this._dispatchEventFromSandbox(n,e)}));this._setEventListeners(i,[["change","Validate"],["change","Action"],["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"]],(t=>t.target.checked))}this._setBackgroundColor(i);this._setDefaultPropertiesFromJS(i);this.container.append(i);return this.container}}class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms})}render(){this.container.className="buttonWidgetAnnotation radioButton";const t=this.annotationStorage,e=this.data,s=e.id;let n=t.getValue(s,{value:e.fieldValue===e.buttonValue}).value;if("string"==typeof n){n=n!==e.buttonValue;t.setValue(s,{value:n})}const i=document.createElement("input");c.add(i);i.setAttribute("data-element-id",s);i.disabled=e.readOnly;this._setRequired(i,this.data.required);i.type="radio";i.name=e.fieldName;n&&i.setAttribute("checked",!0);i.tabIndex=l;i.addEventListener("change",(e=>{const{name:n,checked:i}=e.target;for(const e of this._getElementsByName(n,s))t.setValue(e.id,{value:!1});t.setValue(s,{value:i})}));i.addEventListener("resetform",(t=>{const s=e.defaultFieldValue;t.target.checked=null!=s&&s===e.buttonValue}));if(this.enableScripting&&this.hasJSActions){const n=e.buttonValue;i.addEventListener("updatefromsandbox",(e=>{const i={value:e=>{const i=n===e.detail.value;for(const n of this._getElementsByName(e.target.name)){const e=i&&n.id===s;n.domElement&&(n.domElement.checked=e);t.setValue(n.id,{value:e})}}};this._dispatchEventFromSandbox(i,e)}));this._setEventListeners(i,[["change","Validate"],["change","Action"],["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"]],(t=>t.target.checked))}this._setBackgroundColor(i);this._setDefaultPropertiesFromJS(i);this.container.append(i);return this.container}}class PushButtonWidgetAnnotationElement extends LinkAnnotationElement{constructor(t){super(t,{ignoreBorder:t.data.hasAppearance})}render(){const t=super.render();t.className="buttonWidgetAnnotation pushButton";this.data.alternativeText&&(t.title=this.data.alternativeText);const e=t.lastChild;if(this.enableScripting&&this.hasJSActions&&e){this._setDefaultPropertiesFromJS(e);e.addEventListener("updatefromsandbox",(t=>{this._dispatchEventFromSandbox({},t)}))}return t}}class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms})}render(){this.container.className="choiceWidgetAnnotation";const t=this.annotationStorage,e=this.data.id,s=t.getValue(e,{value:this.data.fieldValue}),n=document.createElement("select");c.add(n);n.setAttribute("data-element-id",e);n.disabled=this.data.readOnly;this._setRequired(n,this.data.required);n.name=this.data.fieldName;n.tabIndex=l;let i=this.data.combo&&this.data.options.length>0;if(!this.data.combo){n.size=this.data.options.length;this.data.multiSelect&&(n.multiple=!0)}n.addEventListener("resetform",(t=>{const e=this.data.defaultFieldValue;for(const t of n.options)t.selected=t.value===e}));for(const t of this.data.options){const e=document.createElement("option");e.textContent=t.displayValue;e.value=t.exportValue;if(s.value.includes(t.exportValue)){e.setAttribute("selected",!0);i=!1}n.append(e)}let a=null;if(i){const t=document.createElement("option");t.value=" ";t.setAttribute("hidden",!0);t.setAttribute("selected",!0);n.prepend(t);a=()=>{t.remove();n.removeEventListener("input",a);a=null};n.addEventListener("input",a)}const getValue=t=>{const e=t?"value":"textContent",{options:s,multiple:i}=n;return i?Array.prototype.filter.call(s,(t=>t.selected)).map((t=>t[e])):-1===s.selectedIndex?null:s[s.selectedIndex][e]};let r=getValue(!1);const getItems=t=>{const e=t.target.options;return Array.prototype.map.call(e,(t=>({displayValue:t.textContent,exportValue:t.value})))};if(this.enableScripting&&this.hasJSActions){n.addEventListener("updatefromsandbox",(s=>{const i={value(s){a?.();const i=s.detail.value,o=new Set(Array.isArray(i)?i:[i]);for(const t of n.options)t.selected=o.has(t.value);t.setValue(e,{value:getValue(!0)});r=getValue(!1)},multipleSelection(t){n.multiple=!0},remove(s){const i=n.options,a=s.detail.remove;i[a].selected=!1;n.remove(a);if(i.length>0){-1===Array.prototype.findIndex.call(i,(t=>t.selected))&&(i[0].selected=!0)}t.setValue(e,{value:getValue(!0),items:getItems(s)});r=getValue(!1)},clear(s){for(;0!==n.length;)n.remove(0);t.setValue(e,{value:null,items:[]});r=getValue(!1)},insert(s){const{index:i,displayValue:a,exportValue:o}=s.detail.insert,l=n.children[i],c=document.createElement("option");c.textContent=a;c.value=o;l?l.before(c):n.append(c);t.setValue(e,{value:getValue(!0),items:getItems(s)});r=getValue(!1)},items(s){const{items:i}=s.detail;for(;0!==n.length;)n.remove(0);for(const t of i){const{displayValue:e,exportValue:s}=t,i=document.createElement("option");i.textContent=e;i.value=s;n.append(i)}n.options.length>0&&(n.options[0].selected=!0);t.setValue(e,{value:getValue(!0),items:getItems(s)});r=getValue(!1)},indices(s){const n=new Set(s.detail.indices);for(const t of s.target.options)t.selected=n.has(t.index);t.setValue(e,{value:getValue(!0)});r=getValue(!1)},editable(t){t.target.disabled=!t.detail.editable}};this._dispatchEventFromSandbox(i,s)}));n.addEventListener("input",(s=>{const n=getValue(!0);t.setValue(e,{value:n});s.preventDefault();this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:r,changeEx:n,willCommit:!1,commitKey:1,keyDown:!1}})}));this._setEventListeners(n,[["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"],["input","Action"]],(t=>t.target.checked))}else n.addEventListener("input",(function(s){t.setValue(e,{value:getValue(!0)})}));this.data.combo&&this._setTextStyle(n);this._setBackgroundColor(n);this._setDefaultPropertiesFromJS(n);this.container.append(n);return this.container}}class PopupAnnotationElement extends AnnotationElement{static IGNORE_TYPES=new Set(["Line","Square","Circle","PolyLine","Polygon","Ink"]);constructor(t){const{data:e}=t;super(t,{isRenderable:!PopupAnnotationElement.IGNORE_TYPES.has(e.parentType)&&!!(e.titleObj?.str||e.contentsObj?.str||e.richText?.str)})}render(){this.container.className="popupAnnotation";const t=this.layer.querySelectorAll(`[data-annotation-id="${this.data.parentId}"]`);if(0===t.length)return this.container;const e=new PopupElement({container:this.container,trigger:Array.from(t),color:this.data.color,titleObj:this.data.titleObj,modificationDate:this.data.modificationDate,contentsObj:this.data.contentsObj,richText:this.data.richText}),s=this.page,i=n.Util.normalizeRect([this.data.parentRect[0],s.view[3]-this.data.parentRect[1]+s.view[1],this.data.parentRect[2],s.view[3]-this.data.parentRect[3]+s.view[1]]),a=i[0]+this.data.parentRect[2]-this.data.parentRect[0],r=i[1],{pageWidth:o,pageHeight:l,pageX:c,pageY:h}=this.viewport.rawDims;this.container.style.left=100*(a-c)/o+"%";this.container.style.top=100*(r-h)/l+"%";this.container.append(e.render());return this.container}}class PopupElement{constructor(t){this.container=t.container;this.trigger=t.trigger;this.color=t.color;this.titleObj=t.titleObj;this.modificationDate=t.modificationDate;this.contentsObj=t.contentsObj;this.richText=t.richText;this.hideWrapper=t.hideWrapper||!1;this.pinned=!1}render(){const t=document.createElement("div");t.className="popupWrapper";this.hideElement=this.hideWrapper?t:this.container;this.hideElement.hidden=!0;const e=document.createElement("div");e.className="popup";const s=this.color;if(s){const t=.7*(255-s[0])+s[0],i=.7*(255-s[1])+s[1],a=.7*(255-s[2])+s[2];e.style.backgroundColor=n.Util.makeHexColor(0|t,0|i,0|a)}const a=document.createElement("h1");a.dir=this.titleObj.dir;a.textContent=this.titleObj.str;e.append(a);const r=i.PDFDateString.toDateObject(this.modificationDate);if(r){const t=document.createElement("span");t.className="popupDate";t.textContent="{{date}}, {{time}}";t.dataset.l10nId="annotation_date_string";t.dataset.l10nArgs=JSON.stringify({date:r.toLocaleDateString(),time:r.toLocaleTimeString()});e.append(t)}if(!this.richText?.str||this.contentsObj?.str&&this.contentsObj.str!==this.richText.str){const t=this._formatContents(this.contentsObj);e.append(t)}else{o.XfaLayer.render({xfaHtml:this.richText.html,intent:"richText",div:e});e.lastChild.className="richText popupContent"}Array.isArray(this.trigger)||(this.trigger=[this.trigger]);for(const t of this.trigger){t.addEventListener("click",this._toggle.bind(this));t.addEventListener("mouseover",this._show.bind(this,!1));t.addEventListener("mouseout",this._hide.bind(this,!1))}e.addEventListener("click",this._hide.bind(this,!0));t.append(e);return t}_formatContents({str:t,dir:e}){const s=document.createElement("p");s.className="popupContent";s.dir=e;const n=t.split(/(?:\r\n?|\n)/);for(let t=0,e=n.length;t{Object.defineProperty(e,"__esModule",{value:!0});e.ColorConverters=void 0;function makeColorComp(t){return Math.floor(255*Math.max(0,Math.min(1,t))).toString(16).padStart(2,"0")}e.ColorConverters=class ColorConverters{static CMYK_G([t,e,s,n]){return["G",1-Math.min(1,.3*t+.59*s+.11*e+n)]}static G_CMYK([t]){return["CMYK",0,0,0,1-t]}static G_RGB([t]){return["RGB",t,t,t]}static G_HTML([t]){const e=makeColorComp(t);return`#${e}${e}${e}`}static RGB_G([t,e,s]){return["G",.3*t+.59*e+.11*s]}static RGB_HTML([t,e,s]){return`#${makeColorComp(t)}${makeColorComp(e)}${makeColorComp(s)}`}static T_HTML(){return"#00000000"}static CMYK_RGB([t,e,s,n]){return["RGB",1-Math.min(1,t+n),1-Math.min(1,s+n),1-Math.min(1,e+n)]}static CMYK_HTML(t){const e=this.CMYK_RGB(t).slice(1);return this.RGB_HTML(e)}static RGB_CMYK([t,e,s]){const n=1-t,i=1-e,a=1-s;return["CMYK",n,i,a,Math.min(n,i,a)]}}},(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0});e.XfaLayer=void 0;var n=s(19);e.XfaLayer=class XfaLayer{static setupStorage(t,e,s,n,i){const a=n.getValue(e,{value:null});switch(s.name){case"textarea":null!==a.value&&(t.textContent=a.value);if("print"===i)break;t.addEventListener("input",(t=>{n.setValue(e,{value:t.target.value})}));break;case"input":if("radio"===s.attributes.type||"checkbox"===s.attributes.type){a.value===s.attributes.xfaOn?t.setAttribute("checked",!0):a.value===s.attributes.xfaOff&&t.removeAttribute("checked");if("print"===i)break;t.addEventListener("change",(t=>{n.setValue(e,{value:t.target.checked?t.target.getAttribute("xfaOn"):t.target.getAttribute("xfaOff")})}))}else{null!==a.value&&t.setAttribute("value",a.value);if("print"===i)break;t.addEventListener("input",(t=>{n.setValue(e,{value:t.target.value})}))}break;case"select":if(null!==a.value)for(const t of s.children)t.attributes.value===a.value&&(t.attributes.selected=!0);t.addEventListener("input",(t=>{const s=t.target.options,i=-1===s.selectedIndex?"":s[s.selectedIndex].value;n.setValue(e,{value:i})}))}}static setAttributes({html:t,element:e,storage:s=null,intent:n,linkService:i}){const{attributes:a}=e,r=t instanceof HTMLAnchorElement;"radio"===a.type&&(a.name=`${a.name}-${n}`);for(const[e,s]of Object.entries(a))if(null!=s)switch(e){case"class":s.length&&t.setAttribute(e,s.join(" "));break;case"dataId":break;case"id":t.setAttribute("data-element-id",s);break;case"style":Object.assign(t.style,s);break;case"textContent":t.textContent=s;break;default:(!r||"href"!==e&&"newWindow"!==e)&&t.setAttribute(e,s)}r&&i.addLinkAttributes(t,a.href,a.newWindow);s&&a.dataId&&this.setupStorage(t,a.dataId,e,s)}static render(t){const e=t.annotationStorage,s=t.linkService,i=t.xfaHtml,a=t.intent||"display",r=document.createElement(i.name);i.attributes&&this.setAttributes({html:r,element:i,intent:a,linkService:s});const o=[[i,-1,r]],l=t.div;l.append(r);if(t.viewport){const e=`matrix(${t.viewport.transform.join(",")})`;l.style.transform=e}"richText"!==a&&l.setAttribute("class","xfaLayer xfaFont");const c=[];for(;o.length>0;){const[t,i,r]=o.at(-1);if(i+1===t.children.length){o.pop();continue}const l=t.children[++o.at(-1)[1]];if(null===l)continue;const{name:h}=l;if("#text"===h){const t=document.createTextNode(l.value);c.push(t);r.append(t);continue}let d;d=l?.attributes?.xmlns?document.createElementNS(l.attributes.xmlns,h):document.createElement(h);r.append(d);l.attributes&&this.setAttributes({html:d,element:l,storage:e,intent:a,linkService:s});if(l.children&&l.children.length>0)o.push([l,-1,d]);else if(l.value){const t=document.createTextNode(l.value);n.XfaText.shouldBuildText(h)&&c.push(t);d.append(t)}}for(const t of l.querySelectorAll(".xfaNonInteractive input, .xfaNonInteractive textarea"))t.setAttribute("readOnly",!0);return{textDivs:c}}static update(t){const e=`matrix(${t.viewport.transform.join(",")})`;t.div.style.transform=e;t.div.hidden=!1}}},(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0});e.SVGGraphics=void 0;var n=s(6),i=s(1),a=s(10);let r=class{constructor(){(0,i.unreachable)("Not implemented: SVGGraphics")}};e.SVGGraphics=r;{const o={fontStyle:"normal",fontWeight:"normal",fillColor:"#000000"},l="http://www.w3.org/XML/1998/namespace",c="http://www.w3.org/1999/xlink",h=["butt","round","square"],d=["miter","round","bevel"],createObjectURL=function(t,e="",s=!1){if(URL.createObjectURL&&"undefined"!=typeof Blob&&!s)return URL.createObjectURL(new Blob([t],{type:e}));const n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";let i=`data:${e};base64,`;for(let e=0,s=t.length;e>2]+n[(3&a)<<4|r>>4]+n[e+1>6:64]+n[e+2>1&2147483647:s>>1&2147483647;e[t]=s}function writePngChunk(t,s,n,i){let a=i;const r=s.length;n[a]=r>>24&255;n[a+1]=r>>16&255;n[a+2]=r>>8&255;n[a+3]=255&r;a+=4;n[a]=255&t.charCodeAt(0);n[a+1]=255&t.charCodeAt(1);n[a+2]=255&t.charCodeAt(2);n[a+3]=255&t.charCodeAt(3);a+=4;n.set(s,a);a+=s.length;const o=function crc32(t,s,n){let i=-1;for(let a=s;a>>8^e[s]}return-1^i}(n,i+4,a);n[a]=o>>24&255;n[a+1]=o>>16&255;n[a+2]=o>>8&255;n[a+3]=255&o}function deflateSyncUncompressed(t){let e=t.length;const s=65535,n=Math.ceil(e/s),i=new Uint8Array(2+e+5*n+4);let a=0;i[a++]=120;i[a++]=156;let r=0;for(;e>s;){i[a++]=0;i[a++]=255;i[a++]=255;i[a++]=0;i[a++]=0;i.set(t.subarray(r,r+s),a);a+=s;r+=s;e-=s}i[a++]=1;i[a++]=255&e;i[a++]=e>>8&255;i[a++]=255&~e;i[a++]=(65535&~e)>>8&255;i.set(t.subarray(r),a);a+=t.length-r;const o=function adler32(t,e,s){let n=1,i=0;for(let a=e;a>24&255;i[a++]=o>>16&255;i[a++]=o>>8&255;i[a++]=255&o;return i}function encode(e,s,n,r){const o=e.width,l=e.height;let c,h,d;const u=e.data;switch(s){case i.ImageKind.GRAYSCALE_1BPP:h=0;c=1;d=o+7>>3;break;case i.ImageKind.RGB_24BPP:h=2;c=8;d=3*o;break;case i.ImageKind.RGBA_32BPP:h=6;c=8;d=4*o;break;default:throw new Error("invalid format")}const p=new Uint8Array((1+d)*l);let g=0,m=0;for(let t=0;t>24&255,o>>16&255,o>>8&255,255&o,l>>24&255,l>>16&255,l>>8&255,255&l,c,h,0,0,0]),b=function deflateSync(t){if(!a.isNodeJS)return deflateSyncUncompressed(t);try{let e;e=parseInt(process.versions.node)>=8?t:Buffer.from(t);const s=require("zlib").deflateSync(e,{level:9});return s instanceof Uint8Array?s:new Uint8Array(s)}catch(t){(0,i.warn)("Not compressing PNG because zlib.deflateSync is unavailable: "+t)}return deflateSyncUncompressed(t)}(p),A=t.length+36+f.length+b.length,_=new Uint8Array(A);let y=0;_.set(t,y);y+=t.length;writePngChunk("IHDR",f,_,y);y+=12+f.length;writePngChunk("IDATA",b,_,y);y+=12+b.length;writePngChunk("IEND",new Uint8Array(0),_,y);return createObjectURL(_,"image/png",n)}return function convertImgDataToPng(t,e,s){return encode(t,void 0===t.kind?i.ImageKind.GRAYSCALE_1BPP:t.kind,e,s)}}();class SVGExtraState{constructor(){this.fontSizeScale=1;this.fontWeight=o.fontWeight;this.fontSize=0;this.textMatrix=i.IDENTITY_MATRIX;this.fontMatrix=i.FONT_IDENTITY_MATRIX;this.leading=0;this.textRenderingMode=i.TextRenderingMode.FILL;this.textMatrixScale=1;this.x=0;this.y=0;this.lineX=0;this.lineY=0;this.charSpacing=0;this.wordSpacing=0;this.textHScale=1;this.textRise=0;this.fillColor=o.fillColor;this.strokeColor="#000000";this.fillAlpha=1;this.strokeAlpha=1;this.lineWidth=1;this.lineJoin="";this.lineCap="";this.miterLimit=0;this.dashArray=[];this.dashPhase=0;this.dependencies=[];this.activeClipUrl=null;this.clipGroup=null;this.maskId=""}clone(){return Object.create(this)}setCurrentPoint(t,e){this.x=t;this.y=e}}function opListToTree(t){let e=[];const s=[];for(const n of t)if("save"!==n.fn)"restore"===n.fn?e=s.pop():e.push(n);else{e.push({fnId:92,fn:"group",items:[]});s.push(e);e=e.at(-1).items}return e}function pf(t){if(Number.isInteger(t))return t.toString();const e=t.toFixed(10);let s=e.length-1;if("0"!==e[s])return e;do{s--}while("0"===e[s]);return e.substring(0,"."===e[s]?s:s+1)}function pm(t){if(0===t[4]&&0===t[5]){if(0===t[1]&&0===t[2])return 1===t[0]&&1===t[3]?"":`scale(${pf(t[0])} ${pf(t[3])})`;if(t[0]===t[3]&&t[1]===-t[2]){return`rotate(${pf(180*Math.acos(t[0])/Math.PI)})`}}else if(1===t[0]&&0===t[1]&&0===t[2]&&1===t[3])return`translate(${pf(t[4])} ${pf(t[5])})`;return`matrix(${pf(t[0])} ${pf(t[1])} ${pf(t[2])} ${pf(t[3])} ${pf(t[4])} ${pf(t[5])})`}let p=0,g=0,m=0;e.SVGGraphics=r=class{constructor(t,e,s=!1){(0,n.deprecated)("The SVG back-end is no longer maintained and *may* be removed in the future.");this.svgFactory=new n.DOMSVGFactory;this.current=new SVGExtraState;this.transformMatrix=i.IDENTITY_MATRIX;this.transformStack=[];this.extraStack=[];this.commonObjs=t;this.objs=e;this.pendingClip=null;this.pendingEOFill=!1;this.embedFonts=!1;this.embeddedFonts=Object.create(null);this.cssStyle=null;this.forceDataSchema=!!s;this._operatorIdMapping=[];for(const t in i.OPS)this._operatorIdMapping[i.OPS[t]]=t}getObject(t,e=null){return"string"==typeof t?t.startsWith("g_")?this.commonObjs.get(t):this.objs.get(t):e}save(){this.transformStack.push(this.transformMatrix);const t=this.current;this.extraStack.push(t);this.current=t.clone()}restore(){this.transformMatrix=this.transformStack.pop();this.current=this.extraStack.pop();this.pendingClip=null;this.tgrp=null}group(t){this.save();this.executeOpTree(t);this.restore()}loadDependencies(t){const e=t.fnArray,s=t.argsArray;for(let t=0,n=e.length;t{t.get(e,s)}));this.current.dependencies.push(s)}return Promise.all(this.current.dependencies)}transform(t,e,s,n,a,r){const o=[t,e,s,n,a,r];this.transformMatrix=i.Util.transform(this.transformMatrix,o);this.tgrp=null}getSVG(t,e){this.viewport=e;const s=this._initialize(e);return this.loadDependencies(t).then((()=>{this.transformMatrix=i.IDENTITY_MATRIX;this.executeOpTree(this.convertOpList(t));return s}))}convertOpList(t){const e=this._operatorIdMapping,s=t.argsArray,n=t.fnArray,i=[];for(let t=0,a=n.length;t0&&(this.current.lineWidth=t)}setLineCap(t){this.current.lineCap=h[t]}setLineJoin(t){this.current.lineJoin=d[t]}setMiterLimit(t){this.current.miterLimit=t}setStrokeAlpha(t){this.current.strokeAlpha=t}setStrokeRGBColor(t,e,s){this.current.strokeColor=i.Util.makeHexColor(t,e,s)}setFillAlpha(t){this.current.fillAlpha=t}setFillRGBColor(t,e,s){this.current.fillColor=i.Util.makeHexColor(t,e,s);this.current.tspan=this.svgFactory.createElement("svg:tspan");this.current.xcoords=[];this.current.ycoords=[]}setStrokeColorN(t){this.current.strokeColor=this._makeColorN_Pattern(t)}setFillColorN(t){this.current.fillColor=this._makeColorN_Pattern(t)}shadingFill(t){const e=this.viewport.width,s=this.viewport.height,n=i.Util.inverseTransform(this.transformMatrix),a=i.Util.applyTransform([0,0],n),r=i.Util.applyTransform([0,s],n),o=i.Util.applyTransform([e,0],n),l=i.Util.applyTransform([e,s],n),c=Math.min(a[0],r[0],o[0],l[0]),h=Math.min(a[1],r[1],o[1],l[1]),d=Math.max(a[0],r[0],o[0],l[0]),u=Math.max(a[1],r[1],o[1],l[1]),p=this.svgFactory.createElement("svg:rect");p.setAttributeNS(null,"x",c);p.setAttributeNS(null,"y",h);p.setAttributeNS(null,"width",d-c);p.setAttributeNS(null,"height",u-h);p.setAttributeNS(null,"fill",this._makeShadingPattern(t));this.current.fillAlpha<1&&p.setAttributeNS(null,"fill-opacity",this.current.fillAlpha);this._ensureTransformGroup().append(p)}_makeColorN_Pattern(t){return"TilingPattern"===t[0]?this._makeTilingPattern(t):this._makeShadingPattern(t)}_makeTilingPattern(t){const e=t[1],s=t[2],n=t[3]||i.IDENTITY_MATRIX,[a,r,o,l]=t[4],c=t[5],h=t[6],d=t[7],u="shading"+m++,[p,g,f,b]=i.Util.normalizeRect([...i.Util.applyTransform([a,r],n),...i.Util.applyTransform([o,l],n)]),[A,_]=i.Util.singularValueDecompose2dScale(n),y=c*A,v=h*_,S=this.svgFactory.createElement("svg:pattern");S.setAttributeNS(null,"id",u);S.setAttributeNS(null,"patternUnits","userSpaceOnUse");S.setAttributeNS(null,"width",y);S.setAttributeNS(null,"height",v);S.setAttributeNS(null,"x",`${p}`);S.setAttributeNS(null,"y",`${g}`);const x=this.svg,E=this.transformMatrix,C=this.current.fillColor,P=this.current.strokeColor,T=this.svgFactory.create(f-p,b-g);this.svg=T;this.transformMatrix=n;if(2===d){const t=i.Util.makeHexColor(...e);this.current.fillColor=t;this.current.strokeColor=t}this.executeOpTree(this.convertOpList(s));this.svg=x;this.transformMatrix=E;this.current.fillColor=C;this.current.strokeColor=P;S.append(T.childNodes[0]);this.defs.append(S);return`url(#${u})`}_makeShadingPattern(t){"string"==typeof t&&(t=this.objs.get(t));switch(t[0]){case"RadialAxial":const e="shading"+m++,s=t[3];let n;switch(t[1]){case"axial":const s=t[4],i=t[5];n=this.svgFactory.createElement("svg:linearGradient");n.setAttributeNS(null,"id",e);n.setAttributeNS(null,"gradientUnits","userSpaceOnUse");n.setAttributeNS(null,"x1",s[0]);n.setAttributeNS(null,"y1",s[1]);n.setAttributeNS(null,"x2",i[0]);n.setAttributeNS(null,"y2",i[1]);break;case"radial":const a=t[4],r=t[5],o=t[6],l=t[7];n=this.svgFactory.createElement("svg:radialGradient");n.setAttributeNS(null,"id",e);n.setAttributeNS(null,"gradientUnits","userSpaceOnUse");n.setAttributeNS(null,"cx",r[0]);n.setAttributeNS(null,"cy",r[1]);n.setAttributeNS(null,"r",l);n.setAttributeNS(null,"fx",a[0]);n.setAttributeNS(null,"fy",a[1]);n.setAttributeNS(null,"fr",o);break;default:throw new Error(`Unknown RadialAxial type: ${t[1]}`)}for(const t of s){const e=this.svgFactory.createElement("svg:stop");e.setAttributeNS(null,"offset",t[0]);e.setAttributeNS(null,"stop-color",t[1]);n.append(e)}this.defs.append(n);return`url(#${e})`;case"Mesh":(0,i.warn)("Unimplemented pattern Mesh");return null;case"Dummy":return"hotpink";default:throw new Error(`Unknown IR type: ${t[0]}`)}}setDash(t,e){this.current.dashArray=t;this.current.dashPhase=e}constructPath(t,e){const s=this.current;let n=s.x,a=s.y,r=[],o=0;for(const s of t)switch(0|s){case i.OPS.rectangle:n=e[o++];a=e[o++];const t=n+e[o++],s=a+e[o++];r.push("M",pf(n),pf(a),"L",pf(t),pf(a),"L",pf(t),pf(s),"L",pf(n),pf(s),"Z");break;case i.OPS.moveTo:n=e[o++];a=e[o++];r.push("M",pf(n),pf(a));break;case i.OPS.lineTo:n=e[o++];a=e[o++];r.push("L",pf(n),pf(a));break;case i.OPS.curveTo:n=e[o+4];a=e[o+5];r.push("C",pf(e[o]),pf(e[o+1]),pf(e[o+2]),pf(e[o+3]),pf(n),pf(a));o+=6;break;case i.OPS.curveTo2:r.push("C",pf(n),pf(a),pf(e[o]),pf(e[o+1]),pf(e[o+2]),pf(e[o+3]));n=e[o+2];a=e[o+3];o+=4;break;case i.OPS.curveTo3:n=e[o+2];a=e[o+3];r.push("C",pf(e[o]),pf(e[o+1]),pf(n),pf(a),pf(n),pf(a));o+=4;break;case i.OPS.closePath:r.push("Z")}r=r.join(" ");if(s.path&&t.length>0&&t[0]!==i.OPS.rectangle&&t[0]!==i.OPS.moveTo)r=s.path.getAttributeNS(null,"d")+r;else{s.path=this.svgFactory.createElement("svg:path");this._ensureTransformGroup().append(s.path)}s.path.setAttributeNS(null,"d",r);s.path.setAttributeNS(null,"fill","none");s.element=s.path;s.setCurrentPoint(n,a)}endPath(){const t=this.current;t.path=null;if(!this.pendingClip)return;if(!t.element){this.pendingClip=null;return}const e="clippath"+p++,s=this.svgFactory.createElement("svg:clipPath");s.setAttributeNS(null,"id",e);s.setAttributeNS(null,"transform",pm(this.transformMatrix));const n=t.element.cloneNode(!0);"evenodd"===this.pendingClip?n.setAttributeNS(null,"clip-rule","evenodd"):n.setAttributeNS(null,"clip-rule","nonzero");this.pendingClip=null;s.append(n);this.defs.append(s);if(t.activeClipUrl){t.clipGroup=null;for(const t of this.extraStack)t.clipGroup=null;s.setAttributeNS(null,"clip-path",t.activeClipUrl)}t.activeClipUrl=`url(#${e})`;this.tgrp=null}clip(t){this.pendingClip=t}closePath(){const t=this.current;if(t.path){const e=`${t.path.getAttributeNS(null,"d")}Z`;t.path.setAttributeNS(null,"d",e)}}setLeading(t){this.current.leading=-t}setTextRise(t){this.current.textRise=t}setTextRenderingMode(t){this.current.textRenderingMode=t}setHScale(t){this.current.textHScale=t/100}setRenderingIntent(t){}setFlatness(t){}setGState(t){for(const[e,s]of t)switch(e){case"LW":this.setLineWidth(s);break;case"LC":this.setLineCap(s);break;case"LJ":this.setLineJoin(s);break;case"ML":this.setMiterLimit(s);break;case"D":this.setDash(s[0],s[1]);break;case"RI":this.setRenderingIntent(s);break;case"FL":this.setFlatness(s);break;case"Font":this.setFont(s);break;case"CA":this.setStrokeAlpha(s);break;case"ca":this.setFillAlpha(s);break;default:(0,i.warn)(`Unimplemented graphic state operator ${e}`)}}fill(){const t=this.current;if(t.element){t.element.setAttributeNS(null,"fill",t.fillColor);t.element.setAttributeNS(null,"fill-opacity",t.fillAlpha);this.endPath()}}stroke(){const t=this.current;if(t.element){this._setStrokeAttributes(t.element);t.element.setAttributeNS(null,"fill","none");this.endPath()}}_setStrokeAttributes(t,e=1){const s=this.current;let n=s.dashArray;1!==e&&n.length>0&&(n=n.map((function(t){return e*t})));t.setAttributeNS(null,"stroke",s.strokeColor);t.setAttributeNS(null,"stroke-opacity",s.strokeAlpha);t.setAttributeNS(null,"stroke-miterlimit",pf(s.miterLimit));t.setAttributeNS(null,"stroke-linecap",s.lineCap);t.setAttributeNS(null,"stroke-linejoin",s.lineJoin);t.setAttributeNS(null,"stroke-width",pf(e*s.lineWidth)+"px");t.setAttributeNS(null,"stroke-dasharray",n.map(pf).join(" "));t.setAttributeNS(null,"stroke-dashoffset",pf(e*s.dashPhase)+"px")}eoFill(){this.current.element?.setAttributeNS(null,"fill-rule","evenodd");this.fill()}fillStroke(){this.stroke();this.fill()}eoFillStroke(){this.current.element?.setAttributeNS(null,"fill-rule","evenodd");this.fillStroke()}closeStroke(){this.closePath();this.stroke()}closeFillStroke(){this.closePath();this.fillStroke()}closeEOFillStroke(){this.closePath();this.eoFillStroke()}paintSolidColorImageMask(){const t=this.svgFactory.createElement("svg:rect");t.setAttributeNS(null,"x","0");t.setAttributeNS(null,"y","0");t.setAttributeNS(null,"width","1px");t.setAttributeNS(null,"height","1px");t.setAttributeNS(null,"fill",this.current.fillColor);this._ensureTransformGroup().append(t)}paintImageXObject(t){const e=this.getObject(t);e?this.paintInlineImageXObject(e):(0,i.warn)(`Dependent image with object ID ${t} is not ready yet`)}paintInlineImageXObject(t,e){const s=t.width,n=t.height,i=u(t,this.forceDataSchema,!!e),a=this.svgFactory.createElement("svg:rect");a.setAttributeNS(null,"x","0");a.setAttributeNS(null,"y","0");a.setAttributeNS(null,"width",pf(s));a.setAttributeNS(null,"height",pf(n));this.current.element=a;this.clip("nonzero");const r=this.svgFactory.createElement("svg:image");r.setAttributeNS(c,"xlink:href",i);r.setAttributeNS(null,"x","0");r.setAttributeNS(null,"y",pf(-n));r.setAttributeNS(null,"width",pf(s)+"px");r.setAttributeNS(null,"height",pf(n)+"px");r.setAttributeNS(null,"transform",`scale(${pf(1/s)} ${pf(-1/n)})`);e?e.append(r):this._ensureTransformGroup().append(r)}paintImageMaskXObject(t){const e=this.getObject(t.data,t);if(e.bitmap){(0,i.warn)("paintImageMaskXObject: ImageBitmap support is not implemented, ensure that the `isOffscreenCanvasSupported` API parameter is disabled.");return}const s=this.current,n=e.width,a=e.height,r=s.fillColor;s.maskId="mask"+g++;const o=this.svgFactory.createElement("svg:mask");o.setAttributeNS(null,"id",s.maskId);const l=this.svgFactory.createElement("svg:rect");l.setAttributeNS(null,"x","0");l.setAttributeNS(null,"y","0");l.setAttributeNS(null,"width",pf(n));l.setAttributeNS(null,"height",pf(a));l.setAttributeNS(null,"fill",r);l.setAttributeNS(null,"mask",`url(#${s.maskId})`);this.defs.append(o);this._ensureTransformGroup().append(l);this.paintInlineImageXObject(e,o)}paintFormXObjectBegin(t,e){Array.isArray(t)&&6===t.length&&this.transform(t[0],t[1],t[2],t[3],t[4],t[5]);if(e){const t=e[2]-e[0],s=e[3]-e[1],n=this.svgFactory.createElement("svg:rect");n.setAttributeNS(null,"x",e[0]);n.setAttributeNS(null,"y",e[1]);n.setAttributeNS(null,"width",pf(t));n.setAttributeNS(null,"height",pf(s));this.current.element=n;this.clip("nonzero");this.endPath()}}paintFormXObjectEnd(){}_initialize(t){const e=this.svgFactory.create(t.width,t.height),s=this.svgFactory.createElement("svg:defs");e.append(s);this.defs=s;const n=this.svgFactory.createElement("svg:g");n.setAttributeNS(null,"transform",pm(t.transform));e.append(n);this.svg=n;return e}_ensureClipGroup(){if(!this.current.clipGroup){const t=this.svgFactory.createElement("svg:g");t.setAttributeNS(null,"clip-path",this.current.activeClipUrl);this.svg.append(t);this.current.clipGroup=t}return this.current.clipGroup}_ensureTransformGroup(){if(!this.tgrp){this.tgrp=this.svgFactory.createElement("svg:g");this.tgrp.setAttributeNS(null,"transform",pm(this.transformMatrix));this.current.activeClipUrl?this._ensureClipGroup().append(this.tgrp):this.svg.append(this.tgrp)}return this.tgrp}}}},(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0});e.PDFNodeStream=void 0;var n=s(1),i=s(32);const a=require("fs"),r=require("http"),o=require("https"),l=require("url"),c=/^file:\/\/\/[a-zA-Z]:\//;e.PDFNodeStream=class PDFNodeStream{constructor(t){this.source=t;this.url=function parseUrl(t){const e=l.parse(t);if("file:"===e.protocol||e.host)return e;if(/^[a-z]:[/\\]/i.test(t))return l.parse(`file:///${t}`);e.host||(e.protocol="file:");return e}(t.url);this.isHttp="http:"===this.url.protocol||"https:"===this.url.protocol;this.isFsUrl="file:"===this.url.protocol;this.httpHeaders=this.isHttp&&t.httpHeaders||{};this._fullRequestReader=null;this._rangeRequestReaders=[]}get _progressiveDataLength(){return this._fullRequestReader?._loaded??0}getFullReader(){(0,n.assert)(!this._fullRequestReader,"PDFNodeStream.getFullReader can only be called once.");this._fullRequestReader=this.isFsUrl?new PDFNodeStreamFsFullReader(this):new PDFNodeStreamFullReader(this);return this._fullRequestReader}getRangeReader(t,e){if(e<=this._progressiveDataLength)return null;const s=this.isFsUrl?new PDFNodeStreamFsRangeReader(this,t,e):new PDFNodeStreamRangeReader(this,t,e);this._rangeRequestReaders.push(s);return s}cancelAllRequests(t){this._fullRequestReader?.cancel(t);for(const e of this._rangeRequestReaders.slice(0))e.cancel(t)}};class BaseFullReader{constructor(t){this._url=t.url;this._done=!1;this._storedError=null;this.onProgress=null;const e=t.source;this._contentLength=e.length;this._loaded=0;this._filename=null;this._disableRange=e.disableRange||!1;this._rangeChunkSize=e.rangeChunkSize;this._rangeChunkSize||this._disableRange||(this._disableRange=!0);this._isStreamingSupported=!e.disableStream;this._isRangeSupported=!e.disableRange;this._readableStream=null;this._readCapability=(0,n.createPromiseCapability)();this._headersCapability=(0,n.createPromiseCapability)()}get headersReady(){return this._headersCapability.promise}get filename(){return this._filename}get contentLength(){return this._contentLength}get isRangeSupported(){return this._isRangeSupported}get isStreamingSupported(){return this._isStreamingSupported}async read(){await this._readCapability.promise;if(this._done)return{value:void 0,done:!0};if(this._storedError)throw this._storedError;const t=this._readableStream.read();if(null===t){this._readCapability=(0,n.createPromiseCapability)();return this.read()}this._loaded+=t.length;this.onProgress?.({loaded:this._loaded,total:this._contentLength});return{value:new Uint8Array(t).buffer,done:!1}}cancel(t){this._readableStream?this._readableStream.destroy(t):this._error(t)}_error(t){this._storedError=t;this._readCapability.resolve()}_setReadableStream(t){this._readableStream=t;t.on("readable",(()=>{this._readCapability.resolve()}));t.on("end",(()=>{t.destroy();this._done=!0;this._readCapability.resolve()}));t.on("error",(t=>{this._error(t)}));!this._isStreamingSupported&&this._isRangeSupported&&this._error(new n.AbortException("streaming is disabled"));this._storedError&&this._readableStream.destroy(this._storedError)}}class BaseRangeReader{constructor(t){this._url=t.url;this._done=!1;this._storedError=null;this.onProgress=null;this._loaded=0;this._readableStream=null;this._readCapability=(0,n.createPromiseCapability)();const e=t.source;this._isStreamingSupported=!e.disableStream}get isStreamingSupported(){return this._isStreamingSupported}async read(){await this._readCapability.promise;if(this._done)return{value:void 0,done:!0};if(this._storedError)throw this._storedError;const t=this._readableStream.read();if(null===t){this._readCapability=(0,n.createPromiseCapability)();return this.read()}this._loaded+=t.length;this.onProgress?.({loaded:this._loaded});return{value:new Uint8Array(t).buffer,done:!1}}cancel(t){this._readableStream?this._readableStream.destroy(t):this._error(t)}_error(t){this._storedError=t;this._readCapability.resolve()}_setReadableStream(t){this._readableStream=t;t.on("readable",(()=>{this._readCapability.resolve()}));t.on("end",(()=>{t.destroy();this._done=!0;this._readCapability.resolve()}));t.on("error",(t=>{this._error(t)}));this._storedError&&this._readableStream.destroy(this._storedError)}}function createRequestOptions(t,e){return{protocol:t.protocol,auth:t.auth,host:t.hostname,port:t.port,path:t.path,method:"GET",headers:e}}class PDFNodeStreamFullReader extends BaseFullReader{constructor(t){super(t);const handleResponse=e=>{if(404===e.statusCode){const t=new n.MissingPDFException(`Missing PDF "${this._url}".`);this._storedError=t;this._headersCapability.reject(t);return}this._headersCapability.resolve();this._setReadableStream(e);const getResponseHeader=t=>this._readableStream.headers[t.toLowerCase()],{allowRangeRequests:s,suggestedLength:a}=(0,i.validateRangeRequestCapabilities)({getResponseHeader:getResponseHeader,isHttp:t.isHttp,rangeChunkSize:this._rangeChunkSize,disableRange:this._disableRange});this._isRangeSupported=s;this._contentLength=a||this._contentLength;this._filename=(0,i.extractFilenameFromHeader)(getResponseHeader)};this._request=null;"http:"===this._url.protocol?this._request=r.request(createRequestOptions(this._url,t.httpHeaders),handleResponse):this._request=o.request(createRequestOptions(this._url,t.httpHeaders),handleResponse);this._request.on("error",(t=>{this._storedError=t;this._headersCapability.reject(t)}));this._request.end()}}class PDFNodeStreamRangeReader extends BaseRangeReader{constructor(t,e,s){super(t);this._httpHeaders={};for(const e in t.httpHeaders){const s=t.httpHeaders[e];void 0!==s&&(this._httpHeaders[e]=s)}this._httpHeaders.Range=`bytes=${e}-${s-1}`;const handleResponse=t=>{if(404!==t.statusCode)this._setReadableStream(t);else{const t=new n.MissingPDFException(`Missing PDF "${this._url}".`);this._storedError=t}};this._request=null;"http:"===this._url.protocol?this._request=r.request(createRequestOptions(this._url,this._httpHeaders),handleResponse):this._request=o.request(createRequestOptions(this._url,this._httpHeaders),handleResponse);this._request.on("error",(t=>{this._storedError=t}));this._request.end()}}class PDFNodeStreamFsFullReader extends BaseFullReader{constructor(t){super(t);let e=decodeURIComponent(this._url.path);c.test(this._url.href)&&(e=e.replace(/^\//,""));a.lstat(e,((t,s)=>{if(t){"ENOENT"===t.code&&(t=new n.MissingPDFException(`Missing PDF "${e}".`));this._storedError=t;this._headersCapability.reject(t)}else{this._contentLength=s.size;this._setReadableStream(a.createReadStream(e));this._headersCapability.resolve()}}))}}class PDFNodeStreamFsRangeReader extends BaseRangeReader{constructor(t,e,s){super(t);let n=decodeURIComponent(this._url.path);c.test(this._url.href)&&(n=n.replace(/^\//,""));this._setReadableStream(a.createReadStream(n,{start:e,end:s-1}))}}},(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0});e.createResponseStatusError=function createResponseStatusError(t,e){if(404===t||0===t&&e.startsWith("file:"))return new n.MissingPDFException('Missing PDF "'+e+'".');return new n.UnexpectedResponseException(`Unexpected server response (${t}) while retrieving PDF "${e}".`,t)};e.extractFilenameFromHeader=function extractFilenameFromHeader(t){const e=t("Content-Disposition");if(e){let t=(0,i.getFilenameFromContentDispositionHeader)(e);if(t.includes("%"))try{t=decodeURIComponent(t)}catch(t){}if((0,a.isPdfFile)(t))return t}return null};e.validateRangeRequestCapabilities=function validateRangeRequestCapabilities({getResponseHeader:t,isHttp:e,rangeChunkSize:s,disableRange:n}){const i={allowRangeRequests:!1,suggestedLength:void 0},a=parseInt(t("Content-Length"),10);if(!Number.isInteger(a))return i;i.suggestedLength=a;if(a<=2*s)return i;if(n||!e)return i;if("bytes"!==t("Accept-Ranges"))return i;if("identity"!==(t("Content-Encoding")||"identity"))return i;i.allowRangeRequests=!0;return i};e.validateResponseStatus=function validateResponseStatus(t){return 200===t||206===t};var n=s(1),i=s(33),a=s(6)},(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0});e.getFilenameFromContentDispositionHeader=function getFilenameFromContentDispositionHeader(t){let e=!0,s=toParamRegExp("filename\\*","i").exec(t);if(s){s=s[1];let t=rfc2616unquote(s);t=unescape(t);t=rfc5987decode(t);t=rfc2047decode(t);return fixupEncoding(t)}s=function rfc2231getparam(t){const e=[];let s;const n=toParamRegExp("filename\\*((?!0\\d)\\d+)(\\*?)","ig");for(;null!==(s=n.exec(t));){let[,t,n,i]=s;t=parseInt(t,10);if(t in e){if(0===t)break}else e[t]=[n,i]}const i=[];for(let t=0;t{Object.defineProperty(e,"__esModule",{value:!0});e.PDFNetworkStream=void 0;var n=s(1),i=s(32);class NetworkManager{constructor(t,e={}){this.url=t;this.isHttp=/^https?:/i.test(t);this.httpHeaders=this.isHttp&&e.httpHeaders||Object.create(null);this.withCredentials=e.withCredentials||!1;this.getXhr=e.getXhr||function NetworkManager_getXhr(){return new XMLHttpRequest};this.currXhrId=0;this.pendingRequests=Object.create(null)}requestRange(t,e,s){const n={begin:t,end:e};for(const t in s)n[t]=s[t];return this.request(n)}requestFull(t){return this.request(t)}request(t){const e=this.getXhr(),s=this.currXhrId++,n=this.pendingRequests[s]={xhr:e};e.open("GET",this.url);e.withCredentials=this.withCredentials;for(const t in this.httpHeaders){const s=this.httpHeaders[t];void 0!==s&&e.setRequestHeader(t,s)}if(this.isHttp&&"begin"in t&&"end"in t){e.setRequestHeader("Range",`bytes=${t.begin}-${t.end-1}`);n.expectedStatus=206}else n.expectedStatus=200;e.responseType="arraybuffer";t.onError&&(e.onerror=function(s){t.onError(e.status)});e.onreadystatechange=this.onStateChange.bind(this,s);e.onprogress=this.onProgress.bind(this,s);n.onHeadersReceived=t.onHeadersReceived;n.onDone=t.onDone;n.onError=t.onError;n.onProgress=t.onProgress;e.send(null);return s}onProgress(t,e){const s=this.pendingRequests[t];s&&s.onProgress?.(e)}onStateChange(t,e){const s=this.pendingRequests[t];if(!s)return;const i=s.xhr;if(i.readyState>=2&&s.onHeadersReceived){s.onHeadersReceived();delete s.onHeadersReceived}if(4!==i.readyState)return;if(!(t in this.pendingRequests))return;delete this.pendingRequests[t];if(0===i.status&&this.isHttp){s.onError?.(i.status);return}const a=i.status||200;if(!(200===a&&206===s.expectedStatus)&&a!==s.expectedStatus){s.onError?.(i.status);return}const r=function getArrayBuffer(t){const e=t.response;return"string"!=typeof e?e:(0,n.stringToBytes)(e).buffer}(i);if(206===a){const t=i.getResponseHeader("Content-Range"),e=/bytes (\d+)-(\d+)\/(\d+)/.exec(t);s.onDone({begin:parseInt(e[1],10),chunk:r})}else r?s.onDone({begin:0,chunk:r}):s.onError?.(i.status)}getRequestXhr(t){return this.pendingRequests[t].xhr}isPendingRequest(t){return t in this.pendingRequests}abortRequest(t){const e=this.pendingRequests[t].xhr;delete this.pendingRequests[t];e.abort()}}e.PDFNetworkStream=class PDFNetworkStream{constructor(t){this._source=t;this._manager=new NetworkManager(t.url,{httpHeaders:t.httpHeaders,withCredentials:t.withCredentials});this._rangeChunkSize=t.rangeChunkSize;this._fullRequestReader=null;this._rangeRequestReaders=[]}_onRangeRequestReaderClosed(t){const e=this._rangeRequestReaders.indexOf(t);e>=0&&this._rangeRequestReaders.splice(e,1)}getFullReader(){(0,n.assert)(!this._fullRequestReader,"PDFNetworkStream.getFullReader can only be called once.");this._fullRequestReader=new PDFNetworkStreamFullRequestReader(this._manager,this._source);return this._fullRequestReader}getRangeReader(t,e){const s=new PDFNetworkStreamRangeRequestReader(this._manager,t,e);s.onClosed=this._onRangeRequestReaderClosed.bind(this);this._rangeRequestReaders.push(s);return s}cancelAllRequests(t){this._fullRequestReader?.cancel(t);for(const e of this._rangeRequestReaders.slice(0))e.cancel(t)}};class PDFNetworkStreamFullRequestReader{constructor(t,e){this._manager=t;const s={onHeadersReceived:this._onHeadersReceived.bind(this),onDone:this._onDone.bind(this),onError:this._onError.bind(this),onProgress:this._onProgress.bind(this)};this._url=e.url;this._fullRequestId=t.requestFull(s);this._headersReceivedCapability=(0,n.createPromiseCapability)();this._disableRange=e.disableRange||!1;this._contentLength=e.length;this._rangeChunkSize=e.rangeChunkSize;this._rangeChunkSize||this._disableRange||(this._disableRange=!0);this._isStreamingSupported=!1;this._isRangeSupported=!1;this._cachedChunks=[];this._requests=[];this._done=!1;this._storedError=void 0;this._filename=null;this.onProgress=null}_onHeadersReceived(){const t=this._fullRequestId,e=this._manager.getRequestXhr(t),getResponseHeader=t=>e.getResponseHeader(t),{allowRangeRequests:s,suggestedLength:n}=(0,i.validateRangeRequestCapabilities)({getResponseHeader:getResponseHeader,isHttp:this._manager.isHttp,rangeChunkSize:this._rangeChunkSize,disableRange:this._disableRange});s&&(this._isRangeSupported=!0);this._contentLength=n||this._contentLength;this._filename=(0,i.extractFilenameFromHeader)(getResponseHeader);this._isRangeSupported&&this._manager.abortRequest(t);this._headersReceivedCapability.resolve()}_onDone(t){if(t)if(this._requests.length>0){this._requests.shift().resolve({value:t.chunk,done:!1})}else this._cachedChunks.push(t.chunk);this._done=!0;if(!(this._cachedChunks.length>0)){for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0}}_onError(t){this._storedError=(0,i.createResponseStatusError)(t,this._url);this._headersReceivedCapability.reject(this._storedError);for(const t of this._requests)t.reject(this._storedError);this._requests.length=0;this._cachedChunks.length=0}_onProgress(t){this.onProgress?.({loaded:t.loaded,total:t.lengthComputable?t.total:this._contentLength})}get filename(){return this._filename}get isRangeSupported(){return this._isRangeSupported}get isStreamingSupported(){return this._isStreamingSupported}get contentLength(){return this._contentLength}get headersReady(){return this._headersReceivedCapability.promise}async read(){if(this._storedError)throw this._storedError;if(this._cachedChunks.length>0){return{value:this._cachedChunks.shift(),done:!1}}if(this._done)return{value:void 0,done:!0};const t=(0,n.createPromiseCapability)();this._requests.push(t);return t.promise}cancel(t){this._done=!0;this._headersReceivedCapability.reject(t);for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0;this._manager.isPendingRequest(this._fullRequestId)&&this._manager.abortRequest(this._fullRequestId);this._fullRequestReader=null}}class PDFNetworkStreamRangeRequestReader{constructor(t,e,s){this._manager=t;const n={onDone:this._onDone.bind(this),onError:this._onError.bind(this),onProgress:this._onProgress.bind(this)};this._url=t.url;this._requestId=t.requestRange(e,s,n);this._requests=[];this._queuedChunk=null;this._done=!1;this._storedError=void 0;this.onProgress=null;this.onClosed=null}_close(){this.onClosed?.(this)}_onDone(t){const e=t.chunk;if(this._requests.length>0){this._requests.shift().resolve({value:e,done:!1})}else this._queuedChunk=e;this._done=!0;for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0;this._close()}_onError(t){this._storedError=(0,i.createResponseStatusError)(t,this._url);for(const t of this._requests)t.reject(this._storedError);this._requests.length=0;this._queuedChunk=null}_onProgress(t){this.isStreamingSupported||this.onProgress?.({loaded:t.loaded})}get isStreamingSupported(){return!1}async read(){if(this._storedError)throw this._storedError;if(null!==this._queuedChunk){const t=this._queuedChunk;this._queuedChunk=null;return{value:t,done:!1}}if(this._done)return{value:void 0,done:!0};const t=(0,n.createPromiseCapability)();this._requests.push(t);return t.promise}cancel(t){this._done=!0;for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0;this._manager.isPendingRequest(this._requestId)&&this._manager.abortRequest(this._requestId);this._close()}}},(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0});e.PDFFetchStream=void 0;var n=s(1),i=s(32);function createFetchOptions(t,e,s){return{method:"GET",headers:t,signal:s.signal,mode:"cors",credentials:e?"include":"same-origin",redirect:"follow"}}function createHeaders(t){const e=new Headers;for(const s in t){const n=t[s];void 0!==n&&e.append(s,n)}return e}e.PDFFetchStream=class PDFFetchStream{constructor(t){this.source=t;this.isHttp=/^https?:/i.test(t.url);this.httpHeaders=this.isHttp&&t.httpHeaders||{};this._fullRequestReader=null;this._rangeRequestReaders=[]}get _progressiveDataLength(){return this._fullRequestReader?._loaded??0}getFullReader(){(0,n.assert)(!this._fullRequestReader,"PDFFetchStream.getFullReader can only be called once.");this._fullRequestReader=new PDFFetchStreamReader(this);return this._fullRequestReader}getRangeReader(t,e){if(e<=this._progressiveDataLength)return null;const s=new PDFFetchStreamRangeReader(this,t,e);this._rangeRequestReaders.push(s);return s}cancelAllRequests(t){this._fullRequestReader?.cancel(t);for(const e of this._rangeRequestReaders.slice(0))e.cancel(t)}};class PDFFetchStreamReader{constructor(t){this._stream=t;this._reader=null;this._loaded=0;this._filename=null;const e=t.source;this._withCredentials=e.withCredentials||!1;this._contentLength=e.length;this._headersCapability=(0,n.createPromiseCapability)();this._disableRange=e.disableRange||!1;this._rangeChunkSize=e.rangeChunkSize;this._rangeChunkSize||this._disableRange||(this._disableRange=!0);this._abortController=new AbortController;this._isStreamingSupported=!e.disableStream;this._isRangeSupported=!e.disableRange;this._headers=createHeaders(this._stream.httpHeaders);const s=e.url;fetch(s,createFetchOptions(this._headers,this._withCredentials,this._abortController)).then((t=>{if(!(0,i.validateResponseStatus)(t.status))throw(0,i.createResponseStatusError)(t.status,s);this._reader=t.body.getReader();this._headersCapability.resolve();const getResponseHeader=e=>t.headers.get(e),{allowRangeRequests:e,suggestedLength:a}=(0,i.validateRangeRequestCapabilities)({getResponseHeader:getResponseHeader,isHttp:this._stream.isHttp,rangeChunkSize:this._rangeChunkSize,disableRange:this._disableRange});this._isRangeSupported=e;this._contentLength=a||this._contentLength;this._filename=(0,i.extractFilenameFromHeader)(getResponseHeader);!this._isStreamingSupported&&this._isRangeSupported&&this.cancel(new n.AbortException("Streaming is disabled."))})).catch(this._headersCapability.reject);this.onProgress=null}get headersReady(){return this._headersCapability.promise}get filename(){return this._filename}get contentLength(){return this._contentLength}get isRangeSupported(){return this._isRangeSupported}get isStreamingSupported(){return this._isStreamingSupported}async read(){await this._headersCapability.promise;const{value:t,done:e}=await this._reader.read();if(e)return{value:t,done:e};this._loaded+=t.byteLength;this.onProgress?.({loaded:this._loaded,total:this._contentLength});return{value:new Uint8Array(t).buffer,done:!1}}cancel(t){this._reader?.cancel(t);this._abortController.abort()}}class PDFFetchStreamRangeReader{constructor(t,e,s){this._stream=t;this._reader=null;this._loaded=0;const a=t.source;this._withCredentials=a.withCredentials||!1;this._readCapability=(0,n.createPromiseCapability)();this._isStreamingSupported=!a.disableStream;this._abortController=new AbortController;this._headers=createHeaders(this._stream.httpHeaders);this._headers.append("Range",`bytes=${e}-${s-1}`);const r=a.url;fetch(r,createFetchOptions(this._headers,this._withCredentials,this._abortController)).then((t=>{if(!(0,i.validateResponseStatus)(t.status))throw(0,i.createResponseStatusError)(t.status,r);this._readCapability.resolve();this._reader=t.body.getReader()})).catch(this._readCapability.reject);this.onProgress=null}get isStreamingSupported(){return this._isStreamingSupported}async read(){await this._readCapability.promise;const{value:t,done:e}=await this._reader.read();if(e)return{value:t,done:e};this._loaded+=t.byteLength;this.onProgress?.({loaded:this._loaded});return{value:new Uint8Array(t).buffer,done:!1}}cancel(t){this._reader?.cancel(t);this._abortController.abort()}}}],__webpack_module_cache__={};function __w_pdfjs_require__(t){var e=__webpack_module_cache__[t];if(void 0!==e)return e.exports;var s=__webpack_module_cache__[t]={exports:{}};__webpack_modules__[t](s,s.exports,__w_pdfjs_require__);return s.exports}var __webpack_exports__={};(()=>{var t=__webpack_exports__;Object.defineProperty(t,"__esModule",{value:!0});Object.defineProperty(t,"AbortException",{enumerable:!0,get:function(){return e.AbortException}});Object.defineProperty(t,"AnnotationEditorLayer",{enumerable:!0,get:function(){return a.AnnotationEditorLayer}});Object.defineProperty(t,"AnnotationEditorParamsType",{enumerable:!0,get:function(){return e.AnnotationEditorParamsType}});Object.defineProperty(t,"AnnotationEditorType",{enumerable:!0,get:function(){return e.AnnotationEditorType}});Object.defineProperty(t,"AnnotationEditorUIManager",{enumerable:!0,get:function(){return r.AnnotationEditorUIManager}});Object.defineProperty(t,"AnnotationLayer",{enumerable:!0,get:function(){return o.AnnotationLayer}});Object.defineProperty(t,"AnnotationMode",{enumerable:!0,get:function(){return e.AnnotationMode}});Object.defineProperty(t,"CMapCompressionType",{enumerable:!0,get:function(){return e.CMapCompressionType}});Object.defineProperty(t,"GlobalWorkerOptions",{enumerable:!0,get:function(){return l.GlobalWorkerOptions}});Object.defineProperty(t,"InvalidPDFException",{enumerable:!0,get:function(){return e.InvalidPDFException}});Object.defineProperty(t,"MissingPDFException",{enumerable:!0,get:function(){return e.MissingPDFException}});Object.defineProperty(t,"OPS",{enumerable:!0,get:function(){return e.OPS}});Object.defineProperty(t,"PDFDataRangeTransport",{enumerable:!0,get:function(){return s.PDFDataRangeTransport}});Object.defineProperty(t,"PDFDateString",{enumerable:!0,get:function(){return n.PDFDateString}});Object.defineProperty(t,"PDFWorker",{enumerable:!0,get:function(){return s.PDFWorker}});Object.defineProperty(t,"PasswordResponses",{enumerable:!0,get:function(){return e.PasswordResponses}});Object.defineProperty(t,"PermissionFlag",{enumerable:!0,get:function(){return e.PermissionFlag}});Object.defineProperty(t,"PixelsPerInch",{enumerable:!0,get:function(){return n.PixelsPerInch}});Object.defineProperty(t,"RenderingCancelledException",{enumerable:!0,get:function(){return n.RenderingCancelledException}});Object.defineProperty(t,"SVGGraphics",{enumerable:!0,get:function(){return h.SVGGraphics}});Object.defineProperty(t,"UNSUPPORTED_FEATURES",{enumerable:!0,get:function(){return e.UNSUPPORTED_FEATURES}});Object.defineProperty(t,"UnexpectedResponseException",{enumerable:!0,get:function(){return e.UnexpectedResponseException}});Object.defineProperty(t,"Util",{enumerable:!0,get:function(){return e.Util}});Object.defineProperty(t,"VerbosityLevel",{enumerable:!0,get:function(){return e.VerbosityLevel}});Object.defineProperty(t,"XfaLayer",{enumerable:!0,get:function(){return d.XfaLayer}});Object.defineProperty(t,"build",{enumerable:!0,get:function(){return s.build}});Object.defineProperty(t,"createPromiseCapability",{enumerable:!0,get:function(){return e.createPromiseCapability}});Object.defineProperty(t,"createValidAbsoluteUrl",{enumerable:!0,get:function(){return e.createValidAbsoluteUrl}});Object.defineProperty(t,"getDocument",{enumerable:!0,get:function(){return s.getDocument}});Object.defineProperty(t,"getFilenameFromUrl",{enumerable:!0,get:function(){return n.getFilenameFromUrl}});Object.defineProperty(t,"getPdfFilenameFromUrl",{enumerable:!0,get:function(){return n.getPdfFilenameFromUrl}});Object.defineProperty(t,"getXfaPageViewport",{enumerable:!0,get:function(){return n.getXfaPageViewport}});Object.defineProperty(t,"isDataScheme",{enumerable:!0,get:function(){return n.isDataScheme}});Object.defineProperty(t,"isPdfFile",{enumerable:!0,get:function(){return n.isPdfFile}});Object.defineProperty(t,"loadScript",{enumerable:!0,get:function(){return n.loadScript}});Object.defineProperty(t,"renderTextLayer",{enumerable:!0,get:function(){return i.renderTextLayer}});Object.defineProperty(t,"setLayerDimensions",{enumerable:!0,get:function(){return n.setLayerDimensions}});Object.defineProperty(t,"shadow",{enumerable:!0,get:function(){return e.shadow}});Object.defineProperty(t,"updateTextLayer",{enumerable:!0,get:function(){return i.updateTextLayer}});Object.defineProperty(t,"version",{enumerable:!0,get:function(){return s.version}});var e=__w_pdfjs_require__(1),s=__w_pdfjs_require__(2),n=__w_pdfjs_require__(6),i=__w_pdfjs_require__(21),a=__w_pdfjs_require__(22),r=__w_pdfjs_require__(5),o=__w_pdfjs_require__(27),l=__w_pdfjs_require__(14),c=__w_pdfjs_require__(10),h=__w_pdfjs_require__(30),d=__w_pdfjs_require__(29);if(c.isNodeJS){const{PDFNodeStream:t}=__w_pdfjs_require__(31);(0,s.setPDFNetworkStreamFactory)((e=>new t(e)))}else{const{PDFNetworkStream:t}=__w_pdfjs_require__(34),{PDFFetchStream:e}=__w_pdfjs_require__(35);(0,s.setPDFNetworkStreamFactory)((s=>(0,n.isValidFetchUrl)(s.url)?new e(s):new t(s)))}})();return __webpack_exports__})())); \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 166b1aa..f9cadda 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,6 +1,8 @@ PODS: - camera_avfoundation (0.0.1): - Flutter + - file_saver (0.0.1): + - Flutter - Flutter (1.0.0) - flutter_native_splash (0.0.1): - Flutter @@ -63,6 +65,8 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - printing (1.0.0): + - Flutter - PromisesObjC (2.4.0) - share_plus (0.0.1): - Flutter @@ -83,12 +87,14 @@ PODS: DEPENDENCIES: - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) + - file_saver (from `.symlinks/plugins/file_saver/ios`) - Flutter (from `Flutter`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - pasteboard (from `.symlinks/plugins/pasteboard/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - printing (from `.symlinks/plugins/printing/ios`) - 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`) @@ -115,6 +121,8 @@ SPEC REPOS: EXTERNAL SOURCES: camera_avfoundation: :path: ".symlinks/plugins/camera_avfoundation/ios" + file_saver: + :path: ".symlinks/plugins/file_saver/ios" Flutter: :path: Flutter flutter_native_splash: @@ -127,6 +135,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/pasteboard/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" + printing: + :path: ".symlinks/plugins/printing/ios" share_plus: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: @@ -144,6 +154,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: camera_avfoundation: 759172d1a77ae7be0de08fc104cfb79738b8a59e + file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a @@ -161,6 +172,7 @@ SPEC CHECKSUMS: package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + printing: 233e1b73bd1f4a05615548e9b5a324c98588640b PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 diff --git a/lib/account_manager/repository/account_repository.dart b/lib/account_manager/repository/account_repository.dart index 52351af..9510997 100644 --- a/lib/account_manager/repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository.dart @@ -132,7 +132,8 @@ class AccountRepository { /// Creates a new super identity, an identity instance, an account associated /// with the identity instance, stores the account in the identity key and /// then logs into that account with no password set at this time - Future createWithNewSuperIdentity(proto.Profile newProfile) async { + Future createWithNewSuperIdentity( + proto.Profile newProfile) async { log.debug('Creating super identity'); final wsi = await WritableSuperIdentity.create(); try { @@ -146,7 +147,7 @@ class AccountRepository { localAccount.superIdentity.recordKey, EncryptionKeyType.none, ''); assert(ok, 'login with none should never fail'); - return wsi.superSecret; + return wsi; } on Exception catch (_) { await wsi.delete(); rethrow; diff --git a/lib/account_manager/views/edit_account_page.dart b/lib/account_manager/views/edit_account_page.dart index 0cf0264..47e2ab2 100644 --- a/lib/account_manager/views/edit_account_page.dart +++ b/lib/account_manager/views/edit_account_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -55,6 +57,13 @@ class _EditAccountPageState extends State { }); } + @override + void dispose() { + unawaited( + changeWindowSetup(TitleBarStyle.normal, OrientationCapability.normal)); + super.dispose(); + } + Widget _editAccountForm(BuildContext context, {required Future Function(GlobalKey) onSubmit}) => @@ -282,30 +291,27 @@ class _EditAccountPageState extends State { await GoRouterHelper(context).push('/settings'); }) ]), - body: Column(children: [ + body: SingleChildScrollView( + child: Column(children: [ _editAccountForm( context, onSubmit: _onSubmit, - ).expanded(), - Text(translate('edit_account_page.remove_account_description')), - ElevatedButton( - onPressed: _onRemoveAccount, - child: Row(mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.person_remove_alt_1, size: 16) - .paddingLTRB(0, 0, 4, 0), - Text(translate('edit_account_page.remove_account')) - .paddingLTRB(0, 0, 4, 0) - ])).paddingLTRB(0, 8, 0, 24), - Text(translate('edit_account_page.destroy_account_description')), - ElevatedButton( - onPressed: _onDestroyAccount, - child: Row(mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.person_off, size: 16) - .paddingLTRB(0, 0, 4, 0), - Text(translate('edit_account_page.destroy_account')) - .paddingLTRB(0, 0, 4, 0) - ])).paddingLTRB(0, 8, 0, 24) - ]).paddingSymmetric(horizontal: 24, vertical: 8)) + ).paddingLTRB(0, 0, 0, 32), + OptionBox( + instructions: + translate('edit_account_page.remove_account_description'), + buttonIcon: Icons.person_remove_alt_1, + buttonText: translate('edit_account_page.remove_account'), + onClick: _onRemoveAccount, + ), + OptionBox( + instructions: + translate('edit_account_page.destroy_account_description'), + buttonIcon: Icons.person_off, + buttonText: translate('edit_account_page.destroy_account'), + onClick: _onDestroyAccount, + ) + ]).paddingSymmetric(horizontal: 24, vertical: 8))) .withModalHUD(context, displayModalHUD); } } diff --git a/lib/account_manager/views/new_account_page.dart b/lib/account_manager/views/new_account_page.dart index 6b98467..cc2a333 100644 --- a/lib/account_manager/views/new_account_page.dart +++ b/lib/account_manager/views/new_account_page.dart @@ -70,10 +70,10 @@ class _NewAccountPageState extends State { _isInAsyncCall = true; }); try { - final superSecret = await AccountRepository.instance + final writableSuperIdentity = await AccountRepository.instance .createWithNewSuperIdentity(newProfile); - GoRouterHelper(context) - .pushReplacement('/new_account/recovery_key', extra: superSecret); + GoRouterHelper(context).pushReplacement('/new_account/recovery_key', + extra: [writableSuperIdentity, newProfile.name]); } finally { if (mounted) { setState(() { @@ -94,7 +94,6 @@ class _NewAccountPageState extends State { final displayModalHUD = _isInAsyncCall; return StyledScaffold( - // resizeToAvoidBottomInset: false, appBar: DefaultAppBar( title: Text(translate('new_account_page.titlebar')), leading: Navigator.canPop(context) @@ -114,10 +113,11 @@ class _NewAccountPageState extends State { await GoRouterHelper(context).push('/settings'); }) ]), - body: _newAccountForm( + body: SingleChildScrollView( + child: _newAccountForm( context, onSubmit: _onSubmit, - ).paddingSymmetric(horizontal: 24, vertical: 8), + )).paddingSymmetric(horizontal: 24, vertical: 8), ).withModalHUD(context, displayModalHUD); } } diff --git a/lib/account_manager/views/profile_edit_form.dart b/lib/account_manager/views/profile_edit_form.dart index d7b3c7d..cc6e987 100644 --- a/lib/account_manager/views/profile_edit_form.dart +++ b/lib/account_manager/views/profile_edit_form.dart @@ -58,7 +58,7 @@ class _EditProfileFormState extends State { ) => FormBuilder( key: _formKey, - child: ListView( + child: Column( children: [ Text(widget.header) .textStyle(context.headlineSmall) diff --git a/lib/account_manager/views/profile_widget.dart b/lib/account_manager/views/profile_widget.dart index 1f06476..9d06648 100644 --- a/lib/account_manager/views/profile_widget.dart +++ b/lib/account_manager/views/profile_widget.dart @@ -7,12 +7,15 @@ import '../../theme/theme.dart'; class ProfileWidget extends StatelessWidget { const ProfileWidget({ required proto.Profile profile, + required bool showPronouns, super.key, - }) : _profile = profile; + }) : _profile = profile, + _showPronouns = showPronouns; // final proto.Profile _profile; + final bool _showPronouns; // @@ -42,22 +45,25 @@ class ProfileWidget extends StatelessWidget { borderRadius: BorderRadius.all( Radius.circular(16 * scaleConfig.borderRadiusScale))), ), - child: Column(children: [ + child: Row(children: [ + const Spacer(), Text( _profile.name, - style: textTheme.headlineSmall!.copyWith( + style: textTheme.titleMedium!.copyWith( color: scaleConfig.preferBorders ? scale.primaryScale.border : scale.primaryScale.borderText), textAlign: TextAlign.left, - ).paddingAll(4), - if (_profile.pronouns.isNotEmpty) - Text(_profile.pronouns, - style: textTheme.bodyMedium!.copyWith( + ).paddingAll(12), + if (_profile.pronouns.isNotEmpty && _showPronouns) + Text('(${_profile.pronouns})', + textAlign: TextAlign.right, + style: textTheme.bodySmall!.copyWith( color: scaleConfig.preferBorders ? scale.primaryScale.border - : scale.primaryScale.borderText)) - .paddingLTRB(4, 0, 4, 4), + : scale.primaryScale.primary)) + .paddingAll(12), + const Spacer() ]), ); } diff --git a/lib/account_manager/views/show_recovery_key_page.dart b/lib/account_manager/views/show_recovery_key_page.dart index 6230a85..33de475 100644 --- a/lib/account_manager/views/show_recovery_key_page.dart +++ b/lib/account_manager/views/show_recovery_key_page.dart @@ -1,10 +1,18 @@ +import 'dart:io'; import 'dart:math'; +import 'dart:typed_data'; import 'package:async_tools/async_tools.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:file_saver/file_saver.dart'; import 'package:flutter/material.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:go_router/go_router.dart'; +import 'package:pdf/widgets.dart' as pw; +import 'package:printing/printing.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:screenshot/screenshot.dart'; +import 'package:share_plus/share_plus.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../layout/default_app_bar.dart'; @@ -13,13 +21,18 @@ import '../../tools/tools.dart'; import '../../veilid_processor/veilid_processor.dart'; class ShowRecoveryKeyPage extends StatefulWidget { - const ShowRecoveryKeyPage({required SecretKey secretKey, super.key}) - : _secretKey = secretKey; + const ShowRecoveryKeyPage( + {required WritableSuperIdentity writableSuperIdentity, + required String name, + super.key}) + : _writableSuperIdentity = writableSuperIdentity, + _name = name; @override ShowRecoveryKeyPageState createState() => ShowRecoveryKeyPageState(); - final SecretKey _secretKey; + final WritableSuperIdentity _writableSuperIdentity; + final String _name; } class ShowRecoveryKeyPageState extends State { @@ -33,18 +46,99 @@ class ShowRecoveryKeyPageState extends State { }); } - Widget _recoveryKeyWidget(SecretKey _secretKey) { + Future _shareRecoveryKey( + BuildContext context, Uint8List recoveryKey, String name) async { + setState(() { + _isInAsyncCall = true; + }); + + final screenshotController = ScreenshotController(); + final bytes = await screenshotController.captureFromWidget( + Container( + color: Colors.white, + width: 400, + height: 400, + child: _recoveryKeyWidget(context, recoveryKey, name)), + ); + + setState(() { + _isInAsyncCall = false; + }); + + if (Platform.isLinux) { + // Share plus doesn't do Linux yet + await FileSaver.instance.saveFile(name: 'recovery_key.png', bytes: bytes); + } else { + final xfile = XFile.fromData( + bytes, + mimeType: 'image/png', + name: 'recovery_key.png', + ); + await Share.shareXFiles([xfile]); + } + } + + static Future _printRecoveryKey( + BuildContext context, Uint8List recoveryKey, String name) async { + final wrapped = await WidgetWrapper.fromWidget( + context: context, + widget: SizedBox( + width: 400, + height: 400, + child: _recoveryKeyWidget(context, recoveryKey, name)), + constraints: const BoxConstraints(maxWidth: 400, maxHeight: 400), + pixelRatio: 3); + + final doc = pw.Document() + ..addPage(pw.Page( + build: (context) => + pw.Center(child: pw.Image(wrapped, width: 400)) // Center + )); // Page + + await Printing.layoutPdf(onLayout: (format) async => doc.save()); + } + + static Widget _recoveryKeyWidget( + BuildContext context, Uint8List recoveryKey, String name) { final theme = Theme.of(context); final textTheme = theme.textTheme; + //final scaleConfig = theme.extension()!; + + return Column(mainAxisSize: MainAxisSize.min, children: [ + Text( + style: textTheme.headlineSmall!.copyWith( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + translate('show_recovery_key_page.recovery_key')) + .paddingLTRB(16, 16, 16, 0), + FittedBox( + child: QrImageView.withQr( + size: 300, + qr: QrCode.fromUint8List( + data: recoveryKey, + errorCorrectLevel: QrErrorCorrectLevel.L))) + .paddingLTRB(16, 16, 16, 8) + .expanded(), + Text( + style: textTheme.labelMedium!.copyWith( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + name) + .paddingLTRB(16, 8, 16, 24), + ]); + } + + static Widget _recoveryKeyDialog( + BuildContext context, Uint8List recoveryKey, String name) { + final theme = Theme.of(context); + //final textTheme = theme.textTheme; final scaleConfig = theme.extension()!; final cardsize = min(MediaQuery.of(context).size.shortestSide - 48.0, 400); - final phonoString = prettyPhonoString( - encodePhono(_secretKey.decode()), - wordsPerLine: 2, - ); return Dialog( shape: RoundedRectangleBorder( side: const BorderSide(width: 2), @@ -55,65 +149,19 @@ class ShowRecoveryKeyPageState extends State { constraints: BoxConstraints( minWidth: cardsize, maxWidth: cardsize, - minHeight: cardsize, - maxHeight: cardsize), - child: Column(mainAxisSize: MainAxisSize.min, children: [ - Text( - style: textTheme.headlineSmall!.copyWith( - color: Colors.black, - fontWeight: FontWeight.bold, - ), - translate('show_recovery_key_page.recovery_key')) - .paddingAll(32), - Text( - style: textTheme.headlineSmall!.copyWith( - color: Colors.black, fontFamily: 'Source Code Pro'), - phonoString) - ]))); - } - - Widget _optionBox( - {required String instructions, - required Icon buttonIcon, - required String buttonText, - required void Function() onClick}) { - final theme = Theme.of(context); - final scale = theme.extension()!; - final scaleConfig = theme.extension()!; - - return Container( - constraints: const BoxConstraints(maxWidth: 400), - decoration: BoxDecoration( - color: scale.primaryScale.subtleBackground, - borderRadius: - BorderRadius.circular(8 * scaleConfig.borderRadiusScale), - border: Border.all(color: scale.primaryScale.border)), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - style: theme.textTheme.labelMedium! - .copyWith(color: scale.primaryScale.appText), - softWrap: true, - textAlign: TextAlign.center, - instructions), - ElevatedButton( - onPressed: onClick, - child: Row(mainAxisSize: MainAxisSize.min, children: [ - buttonIcon.paddingLTRB(0, 8, 12, 8), - Text(textAlign: TextAlign.center, buttonText) - ])).paddingLTRB(0, 12, 0, 0).toCenter() - ]).paddingAll(12)) - .paddingLTRB(24, 0, 24, 12); + minHeight: cardsize + 16, + maxHeight: cardsize + 16), + child: _recoveryKeyWidget(context, recoveryKey, name))); } @override // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { - final secretKey = widget._secretKey; final theme = Theme.of(context); - final scale = theme.extension()!; - final scaleConfig = theme.extension()!; + // final scale = theme.extension()!; + // final scaleConfig = theme.extension()!; + + final displayModalHUD = _isInAsyncCall; return StyledScaffold( // resizeToAvoidBottomInset: false, @@ -146,42 +194,55 @@ class ShowRecoveryKeyPageState extends State { Text( textAlign: TextAlign.center, translate('show_recovery_key_page.instructions_options')) - .paddingLTRB(12, 0, 12, 12), - _optionBox( + .paddingLTRB(12, 0, 12, 24), + OptionBox( instructions: translate('show_recovery_key_page.instructions_print'), - buttonIcon: const Icon(Icons.print), + buttonIcon: Icons.print, buttonText: translate('show_recovery_key_page.print'), - onClick: () { - // - setState(() { - _codeHandled = true; - }); - }), - _optionBox( - instructions: - translate('show_recovery_key_page.instructions_view'), - buttonIcon: const Icon(Icons.edit_document), - buttonText: translate('show_recovery_key_page.view'), onClick: () { // singleFuture(this, () async { - await showDialog( - context: context, - builder: (context) => _recoveryKeyWidget(secretKey)); + await _printRecoveryKey(context, + widget._writableSuperIdentity.recoveryKey, widget._name); }); setState(() { _codeHandled = true; }); }), - _optionBox( + OptionBox( + instructions: + translate('show_recovery_key_page.instructions_view'), + buttonIcon: Icons.edit_document, + buttonText: translate('show_recovery_key_page.view'), + onClick: () { + // + singleFuture(this, () async { + await showDialog( + context: context, + builder: (context) => _recoveryKeyDialog( + context, + widget._writableSuperIdentity.recoveryKey, + widget._name)); + }); + + setState(() { + _codeHandled = true; + }); + }), + OptionBox( instructions: translate('show_recovery_key_page.instructions_share'), - buttonIcon: const Icon(Icons.ios_share), + buttonIcon: Icons.ios_share, buttonText: translate('show_recovery_key_page.share'), onClick: () { // + singleFuture(this, () async { + await _shareRecoveryKey(context, + widget._writableSuperIdentity.recoveryKey, widget._name); + }); + setState(() { _codeHandled = true; }); @@ -198,8 +259,9 @@ class ShowRecoveryKeyPageState extends State { }, child: Text(translate('button.finish')).paddingAll(8)) .paddingAll(12)) - ]))); + ]))).withModalHUD(context, displayModalHUD); } bool _codeHandled = false; + bool _isInAsyncCall = false; } diff --git a/lib/app.dart b/lib/app.dart index 3dd2d57..3480b18 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -149,8 +149,8 @@ class VeilidChatApp extends StatelessWidget { scale.grayScale.subtleBackground, ] : [ - scale.tertiaryScale.hoverElementBackground, - scale.tertiaryScale.subtleBackground, + scale.primaryScale.hoverElementBackground, + scale.primaryScale.subtleBackground, ]); return DecoratedBox( diff --git a/lib/chat/views/no_conversation_widget.dart b/lib/chat/views/no_conversation_widget.dart index c19b535..e05cbf0 100644 --- a/lib/chat/views/no_conversation_widget.dart +++ b/lib/chat/views/no_conversation_widget.dart @@ -14,14 +14,13 @@ class NoConversationWidget extends StatelessWidget { final theme = Theme.of(context); final scale = theme.extension()!; - return Container( - width: double.infinity, - height: double.infinity, + return DecoratedBox( decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, + color: scale.primaryScale.appBackground, ), child: Column( mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Icon( Icons.diversity_3, @@ -29,6 +28,7 @@ class NoConversationWidget extends StatelessWidget { size: 48, ), Text( + textAlign: TextAlign.center, translate('chat.start_a_conversation'), style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: scale.primaryScale.subtleBorder, diff --git a/lib/contact_invitation/views/contact_invitation_display.dart b/lib/contact_invitation/views/contact_invitation_display.dart index 25ca4e5..9c0f688 100644 --- a/lib/contact_invitation/views/contact_invitation_display.dart +++ b/lib/contact_invitation/views/contact_invitation_display.dart @@ -11,7 +11,6 @@ import 'package:qr_flutter/qr_flutter.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../theme/theme.dart'; -import '../../tools/tools.dart'; import '../contact_invitation.dart'; class ContactInvitationDisplayDialog extends StatelessWidget { diff --git a/lib/contact_invitation/views/contact_invitation_list_widget.dart b/lib/contact_invitation/views/contact_invitation_list_widget.dart index 4ea44a3..ad72027 100644 --- a/lib/contact_invitation/views/contact_invitation_list_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_list_widget.dart @@ -49,7 +49,7 @@ class ContactInvitationListWidgetState shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16 * scaleConfig.borderRadiusScale), )), - constraints: const BoxConstraints(maxHeight: 200), + constraints: const BoxConstraints(maxHeight: 100), child: Container( width: double.infinity, decoration: ShapeDecoration( @@ -59,6 +59,7 @@ class ContactInvitationListWidgetState BorderRadius.circular(16 * scaleConfig.borderRadiusScale), )), child: ListView.builder( + shrinkWrap: true, controller: _scrollController, itemCount: widget.contactInvitationRecordList.length, itemBuilder: (context, index) { diff --git a/lib/contact_invitation/views/create_invitation_dialog.dart b/lib/contact_invitation/views/create_invitation_dialog.dart index 93b5796..06503a8 100644 --- a/lib/contact_invitation/views/create_invitation_dialog.dart +++ b/lib/contact_invitation/views/create_invitation_dialog.dart @@ -11,7 +11,6 @@ import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../theme/theme.dart'; -import '../../tools/tools.dart'; import '../contact_invitation.dart'; class CreateInvitationDialog extends StatefulWidget { diff --git a/lib/contact_invitation/views/invitation_dialog.dart b/lib/contact_invitation/views/invitation_dialog.dart index cd962ab..a8afd70 100644 --- a/lib/contact_invitation/views/invitation_dialog.dart +++ b/lib/contact_invitation/views/invitation_dialog.dart @@ -270,11 +270,12 @@ class InvitationDialogState extends State { if (_validInvitation != null && !_isValidating) Column(children: [ Container( - constraints: const BoxConstraints(maxHeight: 64), - width: double.infinity, - child: - ProfileWidget(profile: _validInvitation!.remoteProfile)) - .paddingLTRB(0, 0, 0, 16), + constraints: const BoxConstraints(maxHeight: 64), + width: double.infinity, + child: ProfileWidget( + profile: _validInvitation!.remoteProfile, + showPronouns: true, + )).paddingLTRB(0, 0, 0, 16), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ diff --git a/lib/contact_invitation/views/paste_invitation_dialog.dart b/lib/contact_invitation/views/paste_invitation_dialog.dart index 377d13f..9cd9efc 100644 --- a/lib/contact_invitation/views/paste_invitation_dialog.dart +++ b/lib/contact_invitation/views/paste_invitation_dialog.dart @@ -8,7 +8,6 @@ import 'package:provider/provider.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../theme/theme.dart'; -import '../../tools/tools.dart'; import 'invitation_dialog.dart'; class PasteInvitationDialog extends StatefulWidget { diff --git a/lib/contact_invitation/views/scan_invitation_dialog.dart b/lib/contact_invitation/views/scan_invitation_dialog.dart index d3881c2..9f7a878 100644 --- a/lib/contact_invitation/views/scan_invitation_dialog.dart +++ b/lib/contact_invitation/views/scan_invitation_dialog.dart @@ -13,7 +13,6 @@ import 'package:provider/provider.dart'; import 'package:zxing2/qrcode.dart'; import '../../theme/theme.dart'; -import '../../tools/tools.dart'; import 'invitation_dialog.dart'; // class BarcodeOverlay extends CustomPainter { diff --git a/lib/contacts/views/contact_list_widget.dart b/lib/contacts/views/contact_list_widget.dart index eda6776..eb9958d 100644 --- a/lib/contacts/views/contact_list_widget.dart +++ b/lib/contacts/views/contact_list_widget.dart @@ -10,12 +10,15 @@ import '../../theme/theme.dart'; import 'contact_item_widget.dart'; import 'empty_contact_list_widget.dart'; -class ContactListWidget extends StatelessWidget { +class ContactListWidget extends StatefulWidget { const ContactListWidget( {required this.contactList, required this.disabled, super.key}); final IList contactList; final bool disabled; + @override + State createState() => _ContactListWidgetState(); + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); @@ -23,45 +26,51 @@ class ContactListWidget extends StatelessWidget { ..add(IterableProperty('contactList', contactList)) ..add(DiagnosticsProperty('disabled', disabled)); } +} + +class _ContactListWidgetState extends State { + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } @override Widget build(BuildContext context) { final theme = Theme.of(context); final scale = theme.extension()!; - return SizedBox.expand( - child: styledTitleContainer( - context: context, - title: translate('contact_list.title'), - child: SizedBox.expand( - child: (contactList.isEmpty) - ? const EmptyContactListWidget() - : SearchableList( - initialList: contactList.toList(), - itemBuilder: (c) => - ContactItemWidget(contact: c, disabled: disabled) - .paddingLTRB(0, 4, 0, 0), - filter: (value) { - final lowerValue = value.toLowerCase(); - return contactList - .where((element) => - element.nickname - .toLowerCase() - .contains(lowerValue) || - element.profile.name - .toLowerCase() - .contains(lowerValue) || - element.profile.pronouns - .toLowerCase() - .contains(lowerValue)) - .toList(); - }, - spaceBetweenSearchAndList: 4, - defaultSuffixIconColor: scale.primaryScale.border, - inputDecoration: InputDecoration( - labelText: translate('contact_list.search'), - ), - ).paddingAll(8), - ))).paddingLTRB(8, 0, 8, 8); + return styledTitleContainer( + context: context, + title: translate('contact_list.title'), + child: SearchableList( + shrinkWrap: true, + initialList: widget.contactList.toList(), + itemBuilder: (c) => + ContactItemWidget(contact: c, disabled: widget.disabled) + .paddingLTRB(0, 4, 0, 0), + filter: (value) { + final lowerValue = value.toLowerCase(); + return widget.contactList + .where((element) => + element.nickname.toLowerCase().contains(lowerValue) || + element.profile.name.toLowerCase().contains(lowerValue) || + element.profile.pronouns.toLowerCase().contains(lowerValue)) + .toList(); + }, + searchFieldHeight: 40, + spaceBetweenSearchAndList: 4, + emptyWidget: const EmptyContactListWidget(), + defaultSuffixIconColor: scale.primaryScale.border, + closeKeyboardWhenScrolling: true, + inputDecoration: InputDecoration( + labelText: translate('contact_list.search'), + ), + ).paddingAll(8), + ).paddingLTRB(8, 0, 8, 8); } } diff --git a/lib/contacts/views/empty_contact_list_widget.dart b/lib/contacts/views/empty_contact_list_widget.dart index db07b4a..ae8a852 100644 --- a/lib/contacts/views/empty_contact_list_widget.dart +++ b/lib/contacts/views/empty_contact_list_widget.dart @@ -17,6 +17,7 @@ class EmptyContactListWidget extends StatelessWidget { return Column( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( @@ -25,6 +26,7 @@ class EmptyContactListWidget extends StatelessWidget { size: 48, ), Text( + textAlign: TextAlign.center, translate('contact_list.invite_people'), style: textTheme.bodyMedium?.copyWith( color: scale.primaryScale.subtleBorder, diff --git a/lib/layout/home/drawer_menu/drawer_menu.dart b/lib/layout/home/drawer_menu/drawer_menu.dart index 855edcc..8f1ec07 100644 --- a/lib/layout/home/drawer_menu/drawer_menu.dart +++ b/lib/layout/home/drawer_menu/drawer_menu.dart @@ -1,7 +1,6 @@ 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/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -20,7 +19,7 @@ class DrawerMenu extends StatefulWidget { const DrawerMenu({super.key}); @override - State createState() => _DrawerMenuState(); + State createState() => _DrawerMenuState(); } class _DrawerMenuState extends State { @@ -105,10 +104,12 @@ class _DrawerMenuState extends State { width: 34, decoration: BoxDecoration( shape: BoxShape.circle, - border: Border.all( - color: border, - width: 2, - strokeAlign: BorderSide.strokeAlignOutside), + border: scaleConfig.preferBorders + ? Border.all( + color: border, + width: 2, + strokeAlign: BorderSide.strokeAlignOutside) + : null, color: Colors.blue, ), child: AvatarImage( @@ -130,9 +131,18 @@ class _DrawerMenuState extends State { backgroundColor: background, backgroundHoverColor: hoverBackground, backgroundFocusColor: activeBackground, - borderColor: border, - borderHoverColor: hoverBorder, - borderFocusColor: activeBorder, + borderColor: + (scaleConfig.preferBorders || scaleConfig.useVisualIndicators) + ? border + : null, + borderHoverColor: + (scaleConfig.preferBorders || scaleConfig.useVisualIndicators) + ? hoverBorder + : null, + borderFocusColor: + (scaleConfig.preferBorders || scaleConfig.useVisualIndicators) + ? activeBorder + : null, borderRadius: 16 * scaleConfig.borderRadiusScale, callback: callback, footerButtonIcon: loggedIn ? Icons.edit_outlined : null, @@ -143,7 +153,7 @@ class _DrawerMenuState extends State { )); } - Widget _getAccountList( + List _getAccountList( {required IList localAccounts, required TypedKey? activeLocalAccount, required PerAccountCollectionBlocMapState @@ -164,7 +174,9 @@ class _DrawerMenuState extends State { final avAccountRecordState = perAccountState?.avAccountRecordState; if (perAccountState != null && avAccountRecordState != null) { // Account is logged in - final scale = theme.extension()!.primaryScale; + final scale = scaleConfig.useVisualIndicators + ? theme.extension()!.primaryScale + : theme.extension()!.tertiaryScale; final loggedInAccount = avAccountRecordState.when( data: (value) => _makeAccountWidget( name: value.profile.name, @@ -209,14 +221,7 @@ class _DrawerMenuState extends State { } // Assemble main menu - final mainMenu = [...loggedInAccounts, ...loggedOutAccounts]; - - // Return main menu widgets - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [...mainMenu], - ); + return [...loggedInAccounts, ...loggedOutAccounts]; } Widget _getButton( @@ -262,18 +267,18 @@ class _DrawerMenuState extends State { }), shape: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.hovered)) { return RoundedRectangleBorder( - side: BorderSide(color: hoverBorder), + side: BorderSide(color: hoverBorder, width: 2), borderRadius: BorderRadius.all( Radius.circular(16 * scaleConfig.borderRadiusScale))); } if (states.contains(WidgetState.focused)) { return RoundedRectangleBorder( - side: BorderSide(color: activeBorder), + side: BorderSide(color: activeBorder, width: 2), borderRadius: BorderRadius.all( Radius.circular(16 * scaleConfig.borderRadiusScale))); } return RoundedRectangleBorder( - side: BorderSide(color: border), + side: BorderSide(color: border, width: 2), borderRadius: BorderRadius.all( Radius.circular(16 * scaleConfig.borderRadiusScale))); })), @@ -306,7 +311,7 @@ class _DrawerMenuState extends State { return Row( mainAxisAlignment: MainAxisAlignment.center, - children: [settingsButton, addButton]).paddingLTRB(0, 16, 0, 0); + children: [settingsButton, addButton]).paddingLTRB(0, 16, 0, 16); } @override @@ -323,8 +328,8 @@ class _DrawerMenuState extends State { begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ + scale.tertiaryScale.border, scale.tertiaryScale.subtleBorder, - scale.tertiaryScale.subtleBackground, ]); return DecoratedBox( @@ -391,35 +396,21 @@ class _DrawerMenuState extends State { ? grayColorFilter : null), ]))), - const Spacer(), - DecoratedBox( - decoration: ShapeDecoration( - shape: RoundedRectangleBorder( - side: !scaleConfig.useVisualIndicators - ? BorderSide.none - : scaleConfig.preferBorders - ? BorderSide(color: scale.tertiaryScale.border) - : BorderSide(color: scale.tertiaryScale.primary), - borderRadius: BorderRadius.circular( - 16 * scaleConfig.borderRadiusScale)), - color: scaleConfig.preferBorders - ? Colors.transparent - : scale.tertiaryScale.border.withAlpha(0x5F)), - child: Column(children: [ - Text(translate('menu.accounts'), - style: theme.textTheme.titleMedium!.copyWith( - color: scaleConfig.preferBorders - ? scale.tertiaryScale.border - : scale.tertiaryScale.primary)) - .paddingLTRB(0, 0, 0, 16), - _getAccountList( - localAccounts: localAccounts, - activeLocalAccount: activeLocalAccount, - perAccountCollectionBlocMapState: - perAccountCollectionBlocMapState) - ]).paddingAll(16)), + Text(translate('menu.accounts'), + style: theme.textTheme.titleMedium!.copyWith( + color: scaleConfig.preferBorders + ? scale.tertiaryScale.border + : scale.tertiaryScale.borderText)) + .paddingLTRB(0, 16, 0, 16), + ListView( + shrinkWrap: true, + children: _getAccountList( + localAccounts: localAccounts, + activeLocalAccount: activeLocalAccount, + perAccountCollectionBlocMapState: + perAccountCollectionBlocMapState)) + .expanded(), _getBottomButtons(), - const Spacer(), Row(children: [ Text('${translate('menu.version')} $packageInfoVersion', style: theme.textTheme.labelMedium! diff --git a/lib/layout/home/drawer_menu/menu_item_widget.dart b/lib/layout/home/drawer_menu/menu_item_widget.dart index b94a243..12c5008 100644 --- a/lib/layout/home/drawer_menu/menu_item_widget.dart +++ b/lib/layout/home/drawer_menu/menu_item_widget.dart @@ -72,7 +72,7 @@ class MenuItemWidget extends StatelessWidget { ).paddingAll(8)), ), if (footerButtonIcon != null) - IconButton.outlined( + IconButton( color: footerButtonIconColor, focusColor: footerButtonIconFocusColor, hoverColor: footerButtonIconHoverColor, 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 cd56aa1..625e01f 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 @@ -34,6 +34,7 @@ class HomeAccountReadyChatState extends State { @override Widget build(BuildContext context) => SafeArea( + bottom: false, child: buildChatComponent(context), ); } 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 0c50bbd..4b79771 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 @@ -1,5 +1,4 @@ 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'; @@ -9,7 +8,6 @@ import '../../../account_manager/account_manager.dart'; import '../../../chat/chat.dart'; import '../../../proto/proto.dart' as proto; import '../../../theme/theme.dart'; -import '../../../tools/tools.dart'; import 'main_pager/main_pager.dart'; class HomeAccountReadyMain extends StatefulWidget { @@ -44,7 +42,7 @@ class _HomeAccountReadyMainState extends State { ? scale.primaryScale.border : scale.primaryScale.borderText, constraints: - const BoxConstraints.expand(height: 64, width: 64), + const BoxConstraints.expand(height: 48, width: 48), style: ButtonStyle( backgroundColor: WidgetStateProperty.all( scaleConfig.preferBorders @@ -61,7 +59,7 @@ class _HomeAccountReadyMainState extends State { : scale.primaryScale.borderText, width: 2), borderRadius: BorderRadius.all(Radius.circular( - 16 * scaleConfig.borderRadiusScale))), + 12 * scaleConfig.borderRadiusScale))), )), tooltip: translate('menu.settings_tooltip'), onPressed: () async { @@ -69,7 +67,10 @@ class _HomeAccountReadyMainState extends State { await ctrl.toggle?.call(); //await GoRouterHelper(context).push('/settings'); }).paddingLTRB(0, 0, 8, 0), - ProfileWidget(profile: profile).expanded(), + ProfileWidget( + profile: profile, + showPronouns: false, + ).expanded(), ]).paddingAll(8), MainPager(key: _mainPagerKey).expanded() ])); 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 f80a8fe..1b48e5b 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 @@ -55,6 +55,8 @@ class AccountPageState extends State { tilePadding: const EdgeInsets.fromLTRB(8, 0, 8, 0), backgroundColor: scale.primaryScale.border, collapsedBackgroundColor: scale.primaryScale.border, + dense: true, + minTileHeight: 16, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16 * scaleConfig.borderRadiusScale), @@ -66,10 +68,11 @@ class AccountPageState extends State { title: Text( translate('account_page.contact_invitations'), textAlign: TextAlign.center, - style: textTheme.titleMedium! + style: textTheme.titleSmall! .copyWith(color: scale.primaryScale.borderText), ), iconColor: scale.primaryScale.borderText, + collapsedIconColor: scale.primaryScale.borderText, initiallyExpanded: true, children: [ ContactInvitationListWidget( diff --git a/lib/layout/home/home_account_ready/main_pager/bottom_sheet_action_button.dart b/lib/layout/home/home_account_ready/main_pager/bottom_sheet_action_button.dart index c34e478..494cdb4 100644 --- a/lib/layout/home/home_account_ready/main_pager/bottom_sheet_action_button.dart +++ b/lib/layout/home/home_account_ready/main_pager/bottom_sheet_action_button.dart @@ -46,6 +46,7 @@ class BottomSheetActionButtonState extends State { return _showFab ? FloatingActionButton( elevation: 0, + heroTag: this, hoverElevation: 0, shape: widget.shape, foregroundColor: widget.foregroundColor, 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 d043433..75ca6ed 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 @@ -113,7 +113,7 @@ class MainPagerState extends State with TickerProviderStateMixin { padding: const EdgeInsets.symmetric(horizontal: 4), child: Text( _bottomLabelList[index], - style: theme.textTheme.labelLarge!.copyWith( + style: theme.textTheme.labelMedium!.copyWith( fontWeight: isActive ? FontWeight.bold : FontWeight.normal, color: color), ), diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index 1e10d27..018c3be 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -74,7 +74,10 @@ class HomeScreenState extends State tablet: false, tabletLandscape: false, desktop: false)) { + final activeChatCubit = context.watch(); + return BlocConsumer( + bloc: activeChatCubit, listener: (context, activeChat) { final hasActiveChat = activeChat != null; if (hasActiveChat) { @@ -179,6 +182,7 @@ class HomeScreenState extends State final canClose = activeIndex != -1; return SafeArea( + bottom: false, child: DefaultTextStyle( style: theme.textTheme.bodySmall!, child: ZoomDrawer( diff --git a/lib/router/cubit/router_cubit.dart b/lib/router/cubit/router_cubit.dart index 6323da3..9e9bfe6 100644 --- a/lib/router/cubit/router_cubit.dart +++ b/lib/router/cubit/router_cubit.dart @@ -78,10 +78,14 @@ class RouterCubit extends Cubit { builder: (context, state) => const NewAccountPage(), ), GoRoute( - path: '/new_account/recovery_key', - builder: (context, state) => - ShowRecoveryKeyPage(secretKey: state.extra! as SecretKey), - ), + path: '/new_account/recovery_key', + builder: (context, state) { + final extra = state.extra! as List; + + return ShowRecoveryKeyPage( + writableSuperIdentity: extra[0]! as WritableSuperIdentity, + name: extra[1]! as String); + }), GoRoute( path: '/settings', builder: (context, state) => const SettingsPage(), diff --git a/lib/theme/models/chat_theme.dart b/lib/theme/models/chat_theme.dart index 2d52eae..8623427 100644 --- a/lib/theme/models/chat_theme.dart +++ b/lib/theme/models/chat_theme.dart @@ -31,6 +31,7 @@ ChatTheme makeChatTheme( ), inputBackgroundColor: Colors.blue, inputBorderRadius: BorderRadius.zero, + inputTextStyle: textTheme.bodyLarge!, inputTextDecoration: InputDecoration( filled: !scaleConfig.preferBorders, fillColor: scale.primaryScale.subtleBackground, @@ -77,13 +78,10 @@ ChatTheme makeChatTheme( color: Colors.white, fontSize: 64, ), - receivedMessageBodyTextStyle: TextStyle( + receivedMessageBodyTextStyle: textTheme.bodyLarge!.copyWith( color: scaleConfig.preferBorders ? scale.secondaryScale.calloutBackground : scale.secondaryScale.calloutText, - fontSize: 16, - fontWeight: FontWeight.w500, - height: 1.5, ), receivedEmojiMessageTextStyle: const TextStyle( color: Colors.white, diff --git a/lib/theme/models/slider_tile.dart b/lib/theme/models/slider_tile.dart index 11b4199..e6c4711 100644 --- a/lib/theme/models/slider_tile.dart +++ b/lib/theme/models/slider_tile.dart @@ -106,7 +106,7 @@ class SliderTile extends StatelessWidget { ? tileColor.border : tileColor.borderText) : scale.scale(a.actionScale).primaryText, - icon: a.icon, + icon: subtitle.isNotEmpty ? a.icon : null, label: a.label, padding: const EdgeInsets.all(2)), ) @@ -129,7 +129,7 @@ class SliderTile extends StatelessWidget { ? tileColor.border : tileColor.borderText) : scale.scale(a.actionScale).primaryText, - icon: a.icon, + icon: subtitle.isNotEmpty ? a.icon : null, label: a.label, padding: const EdgeInsets.all(2)), ) @@ -140,9 +140,12 @@ class SliderTile extends StatelessWidget { : const EdgeInsets.fromLTRB(0, 2, 0, 2), child: ListTile( onTap: onTap, + dense: true, + visualDensity: const VisualDensity(vertical: -4), title: Text( title, - softWrap: true, + overflow: TextOverflow.fade, + softWrap: false, ), subtitle: subtitle.isNotEmpty ? Text(subtitle) : null, iconColor: textColor, diff --git a/lib/tools/enter_password.dart b/lib/theme/views/enter_password.dart similarity index 99% rename from lib/tools/enter_password.dart rename to lib/theme/views/enter_password.dart index 2240278..fc876da 100644 --- a/lib/tools/enter_password.dart +++ b/lib/theme/views/enter_password.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import '../theme/theme.dart'; +import '../theme.dart'; class EnterPasswordDialog extends StatefulWidget { const EnterPasswordDialog({ diff --git a/lib/tools/enter_pin.dart b/lib/theme/views/enter_pin.dart similarity index 99% rename from lib/tools/enter_pin.dart rename to lib/theme/views/enter_pin.dart index 5d476fb..f4055c1 100644 --- a/lib/tools/enter_pin.dart +++ b/lib/theme/views/enter_pin.dart @@ -5,7 +5,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:pinput/pinput.dart'; -import '../theme/theme.dart'; +import '../theme.dart'; class EnterPinDialog extends StatefulWidget { const EnterPinDialog({ diff --git a/lib/theme/views/option_box.dart b/lib/theme/views/option_box.dart new file mode 100644 index 0000000..508c7ba --- /dev/null +++ b/lib/theme/views/option_box.dart @@ -0,0 +1,54 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:flutter/material.dart'; + +import '../theme.dart'; + +class OptionBox extends StatelessWidget { + const OptionBox( + {required String instructions, + required IconData buttonIcon, + required String buttonText, + required void Function() onClick, + super.key}) + : _instructions = instructions, + _buttonIcon = buttonIcon, + _buttonText = buttonText, + _onClick = onClick; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + + return Container( + constraints: const BoxConstraints(maxWidth: 400), + decoration: BoxDecoration( + color: scale.primaryScale.subtleBackground, + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale), + border: Border.all(color: scale.primaryScale.border)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + style: theme.textTheme.labelMedium! + .copyWith(color: scale.primaryScale.appText), + softWrap: true, + textAlign: TextAlign.center, + _instructions), + ElevatedButton( + onPressed: _onClick, + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Icon(_buttonIcon, size: 24).paddingLTRB(0, 8, 8, 8), + Text(textAlign: TextAlign.center, _buttonText) + ])).paddingLTRB(0, 12, 0, 0).toCenter() + ]).paddingAll(12)) + .paddingLTRB(24, 0, 24, 12); + } + + final String _instructions; + final IconData _buttonIcon; + final String _buttonText; + final void Function() _onClick; +} diff --git a/lib/tools/pop_control.dart b/lib/theme/views/pop_control.dart similarity index 100% rename from lib/tools/pop_control.dart rename to lib/theme/views/pop_control.dart diff --git a/lib/theme/views/recovery_key_widget.dart b/lib/theme/views/recovery_key_widget.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/tools/responsive.dart b/lib/theme/views/responsive.dart similarity index 96% rename from lib/tools/responsive.dart rename to lib/theme/views/responsive.dart index 4ee206b..a80faf6 100644 --- a/lib/tools/responsive.dart +++ b/lib/theme/views/responsive.dart @@ -9,7 +9,7 @@ bool get isWeb => kIsWeb; bool get isDesktop => !isWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS); -const kMobileWidthCutoff = 479.0; +const kMobileWidthCutoff = 500.0; bool isMobileWidth(BuildContext context) => MediaQuery.of(context).size.width < kMobileWidthCutoff; diff --git a/lib/theme/views/styled_scaffold.dart b/lib/theme/views/styled_scaffold.dart index 6be898c..055684b 100644 --- a/lib/theme/views/styled_scaffold.dart +++ b/lib/theme/views/styled_scaffold.dart @@ -12,13 +12,15 @@ class StyledScaffold extends StatelessWidget { final scale = theme.extension()!; final scaleConfig = theme.extension()!; - return clipBorder( - clipEnabled: true, - borderEnabled: scaleConfig.useVisualIndicators, - borderRadius: 16 * scaleConfig.borderRadiusScale, - borderColor: scale.primaryScale.border, - child: Scaffold(appBar: appBar, body: body, key: key)) - .paddingAll(32); + return isDesktop + ? clipBorder( + clipEnabled: true, + borderEnabled: scaleConfig.useVisualIndicators, + borderRadius: 16 * scaleConfig.borderRadiusScale, + borderColor: scale.primaryScale.border, + child: Scaffold(appBar: appBar, body: body, key: key)) + .paddingAll(32) + : Scaffold(appBar: appBar, body: body, key: key); } //////////////////////////////////////////////////////////////////////////// diff --git a/lib/theme/views/views.dart b/lib/theme/views/views.dart index 0bdf87b..642255e 100644 --- a/lib/theme/views/views.dart +++ b/lib/theme/views/views.dart @@ -1,5 +1,11 @@ export 'brightness_preferences.dart'; export 'color_preferences.dart'; +export 'enter_password.dart'; +export 'enter_pin.dart'; +export 'option_box.dart'; +export 'pop_control.dart'; +export 'recovery_key_widget.dart'; +export 'responsive.dart'; export 'scanner_error_widget.dart'; export 'styled_dialog.dart'; export 'styled_scaffold.dart'; diff --git a/lib/theme/views/widget_helpers.dart b/lib/theme/views/widget_helpers.dart index 3577d66..1bd71bc 100644 --- a/lib/theme/views/widget_helpers.dart +++ b/lib/theme/views/widget_helpers.dart @@ -183,9 +183,9 @@ Widget styledTitleContainer({ child: Column(children: [ Text( title, - style: textTheme.titleMedium! + style: textTheme.titleSmall! .copyWith(color: titleColor ?? scale.primaryScale.borderText), - ).paddingLTRB(8, 8, 8, 4), + ).paddingLTRB(8, 6, 8, 2), DecoratedBox( decoration: ShapeDecoration( color: diff --git a/lib/tools/loggy.dart b/lib/tools/loggy.dart index c422ec8..69faeb7 100644 --- a/lib/tools/loggy.dart +++ b/lib/tools/loggy.dart @@ -9,7 +9,7 @@ import 'package:loggy/loggy.dart'; import 'package:veilid_support/veilid_support.dart'; import '../veilid_processor/views/developer.dart'; -import 'responsive.dart'; +import '../theme/views/responsive.dart'; import 'state_logger.dart'; String wrapWithLogColor(LogLevel? level, String text) { diff --git a/lib/tools/tools.dart b/lib/tools/tools.dart index b61570e..2963df0 100644 --- a/lib/tools/tools.dart +++ b/lib/tools/tools.dart @@ -1,13 +1,8 @@ - export 'animations.dart'; -export 'enter_password.dart'; -export 'enter_pin.dart'; export 'loggy.dart'; export 'misc.dart'; export 'package_info.dart'; export 'phono_byte.dart'; -export 'pop_control.dart'; -export 'responsive.dart'; export 'shared_preferences.dart'; export 'state_logger.dart'; export 'stream_listenable.dart'; diff --git a/lib/tools/window_control.dart b/lib/tools/window_control.dart index c6e33d3..37816f3 100644 --- a/lib/tools/window_control.dart +++ b/lib/tools/window_control.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:window_manager/window_manager.dart'; -import '../../tools/responsive.dart'; +import '../theme/views/responsive.dart'; export 'package:window_manager/window_manager.dart' show TitleBarStyle; @@ -21,7 +21,7 @@ Future initializeWindowControl() async { const windowOptions = WindowOptions( size: Size(768, 1024), - //minimumSize: Size(480, 480), + minimumSize: Size(400, 500), center: true, backgroundColor: Colors.transparent, skipTaskbar: false, diff --git a/lib/veilid_processor/views/developer.dart b/lib/veilid_processor/views/developer.dart index 85914ab..ed495ae 100644 --- a/lib/veilid_processor/views/developer.dart +++ b/lib/veilid_processor/views/developer.dart @@ -273,59 +273,61 @@ class _DeveloperPageState extends State { body: GestureDetector( onTap: () => FocusScope.of(context).unfocus(), child: SafeArea( + bottom: false, child: Column(children: [ - Stack(alignment: AlignmentDirectional.center, children: [ - Image.asset('assets/images/ellet.png'), - TerminalView(globalDebugTerminal, - textStyle: kDefaultTerminalStyle, - controller: _terminalController, - keyboardType: TextInputType.none, - //autofocus: true, - backgroundOpacity: _showEllet ? 0.75 : 1.0, - onSecondaryTapDown: (details, offset) async { - await copySelection(context); - }) - ]).expanded(), - TextField( - controller: _debugCommandController, - onTapOutside: (event) { - FocusManager.instance.primaryFocus?.unfocus(); - }, - decoration: InputDecoration( - filled: true, - contentPadding: const EdgeInsets.fromLTRB(8, 2, 8, 2), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular( - 8 * scaleConfig.borderRadiusScale), - borderSide: BorderSide.none), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular( - 8 * scaleConfig.borderRadiusScale), - ), - fillColor: scale.primaryScale.subtleBackground, - hintText: translate('developer.command'), - suffixIcon: IconButton( - icon: Icon(Icons.send, - color: _debugCommandController.text.isEmpty - ? scale.primaryScale.primary.withAlpha(0x3F) - : scale.primaryScale.primary), - onPressed: _debugCommandController.text.isEmpty - ? null - : () async { - final debugCommand = _debugCommandController.text; - _debugCommandController.clear(); - await _sendDebugCommand(debugCommand); - }, - )), - onChanged: (_) { - setState(() => {}); - }, - onSubmitted: (debugCommand) async { - _debugCommandController.clear(); - await _sendDebugCommand(debugCommand); - }, - ).paddingAll(4) - ])))); + Stack(alignment: AlignmentDirectional.center, children: [ + Image.asset('assets/images/ellet.png'), + TerminalView(globalDebugTerminal, + textStyle: kDefaultTerminalStyle, + controller: _terminalController, + keyboardType: TextInputType.none, + //autofocus: true, + backgroundOpacity: _showEllet ? 0.75 : 1.0, + onSecondaryTapDown: (details, offset) async { + await copySelection(context); + }) + ]).expanded(), + TextField( + controller: _debugCommandController, + onTapOutside: (event) { + FocusManager.instance.primaryFocus?.unfocus(); + }, + decoration: InputDecoration( + filled: true, + contentPadding: const EdgeInsets.fromLTRB(8, 2, 8, 2), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular( + 8 * scaleConfig.borderRadiusScale), + borderSide: BorderSide.none), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular( + 8 * scaleConfig.borderRadiusScale), + ), + fillColor: scale.primaryScale.subtleBackground, + hintText: translate('developer.command'), + suffixIcon: IconButton( + icon: Icon(Icons.send, + color: _debugCommandController.text.isEmpty + ? scale.primaryScale.primary.withAlpha(0x3F) + : scale.primaryScale.primary), + onPressed: _debugCommandController.text.isEmpty + ? null + : () async { + final debugCommand = + _debugCommandController.text; + _debugCommandController.clear(); + await _sendDebugCommand(debugCommand); + }, + )), + onChanged: (_) { + setState(() => {}); + }, + onSubmitted: (debugCommand) async { + _debugCommandController.clear(); + await _sendDebugCommand(debugCommand); + }, + ).paddingAll(4) + ])))); } @override diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index d2ecb95..3acb238 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,7 +6,9 @@ #include "generated_plugin_registrant.h" +#include #include +#include #include #include #include @@ -14,9 +16,15 @@ #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_saver_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin"); + file_saver_plugin_register_with_registrar(file_saver_registrar); g_autoptr(FlPluginRegistrar) pasteboard_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin"); pasteboard_plugin_register_with_registrar(pasteboard_registrar); + g_autoptr(FlPluginRegistrar) printing_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin"); + printing_plugin_register_with_registrar(printing_registrar); g_autoptr(FlPluginRegistrar) screen_retriever_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 10f8b07..d09262f 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,7 +3,9 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_saver pasteboard + printing screen_retriever smart_auth url_launcher_linux diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index b888e6d..c311845 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,10 +5,12 @@ import FlutterMacOS import Foundation +import file_saver import mobile_scanner import package_info_plus import pasteboard import path_provider_foundation +import printing import screen_retriever import share_plus import shared_preferences_foundation @@ -19,10 +21,12 @@ import veilid import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin")) ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 98cf433..3a69524 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - file_saver (0.0.1): + - FlutterMacOS - FlutterMacOS (1.0.0) - mobile_scanner (5.1.1): - FlutterMacOS @@ -9,6 +11,8 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - printing (1.0.0): + - FlutterMacOS - screen_retriever (0.0.1): - FlutterMacOS - share_plus (0.0.1): @@ -29,11 +33,13 @@ PODS: - FlutterMacOS DEPENDENCIES: + - file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - printing (from `Flutter/ephemeral/.symlinks/plugins/printing/macos`) - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) @@ -44,6 +50,8 @@ DEPENDENCIES: - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) EXTERNAL SOURCES: + file_saver: + :path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos FlutterMacOS: :path: Flutter/ephemeral mobile_scanner: @@ -54,6 +62,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/pasteboard/macos path_provider_foundation: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + printing: + :path: Flutter/ephemeral/.symlinks/plugins/printing/macos screen_retriever: :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos share_plus: @@ -72,11 +82,13 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: + file_saver: 44e6fbf666677faf097302460e214e977fdd977b FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 mobile_scanner: 1efac1e53c294b24e3bb55bcc7f4deee0233a86b package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + printing: 1dd6a1fce2209ec240698e2439a4adbb9b427637 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index c55503e..fe015f3 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -12,6 +12,8 @@ com.apple.security.network.server + com.apple.security.print + keychain-access-groups $(AppIdentifierPrefix)com.veilid.veilidchat diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements index d5ed4c2..8d195f4 100644 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -10,6 +10,8 @@ com.apple.security.network.server + com.apple.security.print + keychain-access-groups $(AppIdentifierPrefix)com.veilid.veilidchat diff --git a/packages/veilid_support/lib/identity_support/identity_instance.dart b/packages/veilid_support/lib/identity_support/identity_instance.dart index 978a30d..1b6bf1f 100644 --- a/packages/veilid_support/lib/identity_support/identity_instance.dart +++ b/packages/veilid_support/lib/identity_support/identity_instance.dart @@ -19,7 +19,9 @@ class IdentityInstance with _$IdentityInstance { required PublicKey publicKey, // Secret key of identity instance - // Encrypted with DH(publicKey, SuperIdentity.secret) with appended salt + // Encrypted with appended salt, key is DeriveSharedSecret( + // password = SuperIdentity.secret, + // salt = publicKey) // Used to recover accounts without generating a new instance @Uint8ListJsonConverter() required Uint8List encryptedSecretKey, diff --git a/packages/veilid_support/lib/identity_support/writable_super_identity.dart b/packages/veilid_support/lib/identity_support/writable_super_identity.dart index 3d88742..093073f 100644 --- a/packages/veilid_support/lib/identity_support/writable_super_identity.dart +++ b/packages/veilid_support/lib/identity_support/writable_super_identity.dart @@ -69,6 +69,12 @@ class WritableSuperIdentity { /// Delete a super identity with secrets Future delete() async => superIdentity.delete(); + /// Produce a recovery key for this superIdentity + Uint8List get recoveryKey => (BytesBuilder() + ..add(superIdentity.recordKey.decode()) + ..add(superSecret.decode())) + .toBytes(); + /// xxx: migration support, new identities, reveal identity secret etc //////////////////////////////////////////////////////////////////////////// @@ -113,8 +119,8 @@ class WritableSuperIdentity { // Make encrypted secret key final cs = await Veilid.instance.getCryptoSystem(identityRecordKey.kind); - final encryptionKey = await cs.generateSharedSecret( - identityPublicKey, superSecret, identityCryptoDomain); + final encryptionKey = await cs.deriveSharedSecret( + superSecret.decode(), identityPublicKey.decode()); final encryptedSecretKey = await cs.encryptNoAuthWithNonce( identitySecretKey.decode(), encryptionKey); diff --git a/pubspec.lock b/pubspec.lock index 736e2bc..8ab1f26 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -97,6 +97,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + barcode: + dependency: transitive + description: + name: barcode + sha256: ab180ce22c6555d77d45f0178a523669db67f95856e3378259ef2ffeb43e6003 + url: "https://pub.dev" + source: hosted + version: "2.2.8" basic_utils: dependency: "direct main" description: @@ -105,6 +113,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.7.0" + bidi: + dependency: transitive + description: + name: bidi + sha256: "1a7d0c696324b2089f72e7671fd1f1f64fef44c980f3cebc84e803967c597b63" + url: "https://pub.dev" + source: hosted + version: "2.0.10" bloc: dependency: "direct main" description: @@ -237,10 +253,10 @@ packages: dependency: transitive description: name: camera_android - sha256: "3af7f0b55f184d392d2eec238aaa30552ebeef2915e5e094f5488bf50d6d7ca2" + sha256: "981654e0e56a4c735f7ecc7bd3921385eb5f7dd13deaf4a6431255d9731df01a" url: "https://pub.dev" source: hosted - version: "0.10.9+3" + version: "0.10.9+7" camera_avfoundation: dependency: transitive description: @@ -409,6 +425,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.1" + dio: + dependency: transitive + description: + name: dio + sha256: e17f6b3097b8c51b72c74c9f071a605c47bcc8893839bd66732457a5ebe73714 + url: "https://pub.dev" + source: hosted + version: "5.5.0+1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "36c5b2d79eb17cdae41e974b7a8284fec631651d2a6f39a8a2ff22327e90aeac" + url: "https://pub.dev" + source: hosted + version: "1.0.1" equatable: dependency: "direct main" description: @@ -441,6 +473,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + file_saver: + dependency: "direct main" + description: + name: file_saver + sha256: d375b351e3331663abbaf99747abd72f159260c58fbbdbca9f926f02c01bdc48 + url: "https://pub.dev" + source: hosted + version: "0.2.13" fixnum: dependency: "direct main" description: @@ -466,10 +506,10 @@ packages: dependency: "direct main" description: name: flutter_bloc - sha256: f0ecf6e6eb955193ca60af2d5ca39565a86b8a142452c5b24d96fb477428f4d2 + sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a url: "https://pub.dev" source: hosted - version: "8.1.5" + version: "8.1.6" flutter_cache_manager: dependency: transitive description: @@ -489,9 +529,11 @@ packages: flutter_chat_ui: dependency: "direct main" description: - path: "../flutter_chat_ui" - relative: true - source: path + path: "." + ref: main + resolved-ref: d4b9d507d10f5d640156cacfd754f661f8c0f4c1 + url: "https://gitlab.com/veilid/flutter-chat-ui.git" + source: git version: "1.6.14" flutter_form_builder: dependency: "direct main" @@ -534,10 +576,10 @@ packages: dependency: "direct main" description: name: flutter_native_splash - sha256: edf39bcf4d74aca1eb2c1e43c3e445fd9f494013df7f0da752fefe72020eedc0 + sha256: aa06fec78de2190f3db4319dd60fdc8d12b2626e93ef9828633928c2dcaea840 url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" flutter_parsed_text: dependency: transitive description: @@ -627,10 +669,10 @@ packages: dependency: "direct main" description: name: freezed_annotation - sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + sha256: f54946fdb1fa7b01f780841937b1a80783a20b393485f3f6cdf336fd6f4705f2 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" frontend_server_client: dependency: transitive description: @@ -659,10 +701,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: abec47eb8c8c36ebf41d0a4c64dbbe7f956e39a012b3aafc530e951bdc12fe3f + sha256: cdae1b9c8bd7efadcef6112e81c903662ef2ce105cbd220a04bbb7c3425b5554 url: "https://pub.dev" source: hosted - version: "14.1.4" + version: "14.2.0" graphs: dependency: transitive description: @@ -931,10 +973,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514" + sha256: bca87b0165ffd7cdb9cad8edd22d18d2201e886d9a9f19b4fb3452ea7df3a72a url: "https://pub.dev" source: hosted - version: "2.2.5" + version: "2.2.6" path_provider_foundation: dependency: transitive description: @@ -967,6 +1009,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.1" + pdf: + dependency: "direct main" + description: + name: pdf + sha256: "81d5522bddc1ef5c28e8f0ee40b71708761753c163e0c93a40df56fd515ea0f0" + url: "https://pub.dev" + source: hosted + version: "3.11.0" + pdf_widget_wrapper: + dependency: transitive + description: + name: pdf_widget_wrapper + sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5 + url: "https://pub.dev" + source: hosted + version: "1.0.4" petitparser: dependency: transitive description: @@ -995,10 +1053,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -1031,6 +1089,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" + printing: + dependency: "direct main" + description: + name: printing + sha256: cc4b256a5a89d5345488e3318897b595867f5181b8c5ed6fc63bfa5f2044aec3 + url: "https://pub.dev" + source: hosted + version: "5.13.1" protobuf: dependency: "direct main" description: @@ -1075,10 +1141,10 @@ packages: dependency: "direct main" description: name: qr_code_dart_scan - sha256: "948271f8dc39ab3798341783f0ab7bfdb723054fdc9ea0928c0a5be8503ee01c" + sha256: "52912da40f5e40a197b890108af9d2a6baa0c5812b77bfb085c8ee9e3c4f1f52" url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.8.1" qr_flutter: dependency: "direct main" description: @@ -1135,6 +1201,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.9" + screenshot: + dependency: "direct main" + description: + name: screenshot + sha256: "63817697a7835e6ce82add4228e15d233b74d42975c143ad8cfe07009fab866b" + url: "https://pub.dev" + source: hosted + version: "3.0.0" scroll_to_index: dependency: "direct main" description: @@ -1547,7 +1621,7 @@ packages: path: "../veilid/veilid-flutter" relative: true source: path - version: "0.3.2" + version: "0.3.3" veilid_support: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index c5c6a3f..801d5ae 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,6 +28,7 @@ dependencies: cupertino_icons: ^1.0.8 equatable: ^2.0.5 fast_immutable_collections: ^10.2.4 + file_saver: ^0.2.13 fixnum: ^1.1.0 flutter: sdk: flutter @@ -63,8 +64,10 @@ dependencies: pasteboard: ^0.2.0 path: ^1.9.0 path_provider: ^2.1.3 + pdf: ^3.11.0 pinput: ^4.0.0 preload_page_view: ^0.2.0 + printing: ^5.13.1 protobuf: ^3.1.0 provider: ^6.1.2 qr_code_dart_scan: ^0.8.0 @@ -72,6 +75,7 @@ dependencies: quickalert: ^1.1.0 radix_colors: ^1.0.4 reorderable_grid: ^1.0.10 + screenshot: ^3.0.0 scroll_to_index: ^3.0.1 searchable_listview: ^2.14.0 share_plus: ^9.0.0 @@ -95,13 +99,13 @@ dependencies: xterm: ^4.0.0 zxing2: ^0.2.3 -dependency_overrides: +# dependency_overrides: # async_tools: # path: ../dart_async_tools # bloc_advanced_tools: # path: ../bloc_advanced_tools - flutter_chat_ui: - path: ../flutter_chat_ui +# flutter_chat_ui: +# path: ../flutter_chat_ui dev_dependencies: build_runner: ^2.4.11 @@ -146,6 +150,8 @@ flutter: - assets/images/title.svg - assets/images/vlogo.svg - assets/images/ellet.png + # Printing + - assets/js/pdf/3.2.146/pdf.min.js # Fonts fonts: - family: Source Code Pro diff --git a/web/index.html b/web/index.html index a9c0bf2..dfd1680 100644 --- a/web/index.html +++ b/web/index.html @@ -56,7 +56,10 @@ } run(); + - + \ No newline at end of file diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index c453077..041a716 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,7 +6,9 @@ #include "generated_plugin_registrant.h" +#include #include +#include #include #include #include @@ -15,8 +17,12 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSaverPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSaverPlugin")); PasteboardPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PasteboardPlugin")); + PrintingPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PrintingPlugin")); ScreenRetrieverPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); SharePlusWindowsPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index e705509..17a8144 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,7 +3,9 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_saver pasteboard + printing screen_retriever share_plus smart_auth From 68f7a26ed196f31d6657f0a44d0aee733eae9fa6 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 9 Jul 2024 12:00:15 -0400 Subject: [PATCH 159/270] fix phone layout --- lib/chat/views/no_conversation_widget.dart | 1 - .../home_account_ready_main.dart | 94 ++++++++++++------- lib/layout/home/home_screen.dart | 57 +---------- 3 files changed, 62 insertions(+), 90 deletions(-) diff --git a/lib/chat/views/no_conversation_widget.dart b/lib/chat/views/no_conversation_widget.dart index e05cbf0..ccbc888 100644 --- a/lib/chat/views/no_conversation_widget.dart +++ b/lib/chat/views/no_conversation_widget.dart @@ -20,7 +20,6 @@ class NoConversationWidget extends StatelessWidget { ), child: Column( mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Icon( Icons.diversity_3, 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 4b79771..440f4dc 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 @@ -76,14 +76,11 @@ class _HomeAccountReadyMainState extends State { ])); }); - Widget buildPhone(BuildContext context) => - Material(color: Colors.transparent, child: buildUserPanel()); - - Widget buildTabletLeftPane(BuildContext context) => Builder( + Widget buildLeftPane(BuildContext context) => Builder( builder: (context) => Material(color: Colors.transparent, child: buildUserPanel())); - Widget buildTabletRightPane(BuildContext context) { + Widget buildRightPane(BuildContext context) { final activeChatLocalConversationKey = context.watch().state; if (activeChatLocalConversationKey == null) { @@ -94,42 +91,73 @@ class _HomeAccountReadyMainState extends State { key: ValueKey(activeChatLocalConversationKey)); } - // ignore: prefer_expression_function_bodies - Widget buildTablet(BuildContext context) { + @override + Widget build(BuildContext context) { + final isLarge = responsiveVisibility( + context: context, + phone: false, + ); + final w = MediaQuery.of(context).size.width; final theme = Theme.of(context); final scale = theme.extension()!; final scaleConfig = theme.extension()!; - final children = [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 300, maxWidth: 300), + final activeChat = context.watch().state; + final hasActiveChat = activeChat != null; + // if (hasActiveChat) { + // _chatAnimationController.forward(); + // } else { + // _chatAnimationController.reset(); + // } + + late final bool offstageLeft; + late final bool offstageRight; + late final double leftWidth; + late final double rightWidth; + if (isLarge) { + leftWidth = 300; + rightWidth = w - 300 - 2; + offstageLeft = false; + offstageRight = false; + } else { + leftWidth = w; + rightWidth = w; + if (hasActiveChat) { + offstageLeft = true; + offstageRight = false; + } else { + offstageLeft = false; + offstageRight = true; + } + } + + return Row(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + Offstage( + offstage: offstageLeft, child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: w / 2), - child: buildTabletLeftPane(context))), - SizedBox( - width: 2, - height: double.infinity, - child: ColoredBox( - color: scaleConfig.preferBorders - ? scale.primaryScale.subtleBorder - : scale.primaryScale.subtleBackground)), - Expanded(child: buildTabletRightPane(context)), - ]; - - return Row( - children: children, - ); + constraints: + BoxConstraints(minWidth: leftWidth, maxWidth: leftWidth), + child: buildLeftPane(context))), + Offstage( + offstage: offstageLeft || offstageRight, + child: SizedBox( + width: 2, + height: double.infinity, + child: ColoredBox( + color: scaleConfig.preferBorders + ? scale.primaryScale.subtleBorder + : scale.primaryScale.subtleBackground))), + Offstage( + offstage: offstageRight, + child: ConstrainedBox( + constraints: + BoxConstraints(minWidth: rightWidth, maxWidth: rightWidth), + child: buildRightPane(context), + )), + ]); } - @override - Widget build(BuildContext context) => responsiveVisibility( - context: context, - phone: false, - ) - ? buildTablet(context) - : buildPhone(context); - //////////////////////////////////////////////////////////////////////////// final _mainPagerKey = GlobalKey(debugLabel: '_mainPagerKey'); } diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index 018c3be..9083790 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -1,14 +1,12 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart'; import 'package:provider/provider.dart'; import 'package:transitioned_indexed_stack/transitioned_indexed_stack.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 'drawer_menu/drawer_menu.dart'; @@ -32,19 +30,6 @@ class HomeScreenState extends State with SingleTickerProviderStateMixin { @override void initState() { - // Chat animation setup (open in phone mode) - _chatAnimationController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 250), - ); - _chatAnimation = Tween( - begin: const Offset(1, 0), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: _chatAnimationController, - curve: Curves.easeInOut, - )); - WidgetsBinding.instance.addPostFrameCallback((_) async { final localAccounts = context.read().state; final activeLocalAccount = context.read().state; @@ -62,44 +47,6 @@ class HomeScreenState extends State super.initState(); } - @override - void dispose() { - _chatAnimationController.dispose(); - super.dispose(); - } - - Widget _buildAccountReadyDeviceSpecific(BuildContext context) { - if (responsiveVisibility( - context: context, - tablet: false, - tabletLandscape: false, - desktop: false)) { - final activeChatCubit = context.watch(); - - return BlocConsumer( - bloc: activeChatCubit, - listener: (context, activeChat) { - final hasActiveChat = activeChat != null; - if (hasActiveChat) { - _chatAnimationController.forward(); - } else { - _chatAnimationController.reset(); - } - }, - builder: (context, activeChat) => Stack( - children: [ - const HomeAccountReadyMain(), - Offstage( - offstage: activeChat == null, - child: SlideTransition( - position: _chatAnimation, - child: const HomeAccountReadyChat())), - ], - )); - } - return const HomeAccountReadyMain(); - } - Widget _buildAccountPage( BuildContext context, TypedKey superIdentityRecordKey, @@ -117,7 +64,7 @@ class HomeScreenState extends State // Re-export all ready blocs to the account display subtree return perAccountCollectionState.provide( - child: Builder(builder: _buildAccountReadyDeviceSpecific)); + child: const HomeAccountReadyMain()); } } @@ -217,6 +164,4 @@ class HomeScreenState extends State //////////////////////////////////////////////////////////////////////////// final _zoomDrawerController = ZoomDrawerController(); - late final Animation _chatAnimation; - late final AnimationController _chatAnimationController; } From 67812b3c6fc2e83d2b8cf18475fcfb9f98264c96 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 9 Jul 2024 13:27:54 -0400 Subject: [PATCH 160/270] more layout cleanup --- lib/chat/views/chat_component_widget.dart | 282 ++++++++---------- lib/chat/views/no_conversation_widget.dart | 1 + lib/layout/home/home.dart | 2 +- ...eady_main.dart => home_account_ready.dart} | 110 +++---- .../home_account_ready.dart | 2 - .../home_account_ready_chat.dart | 40 --- lib/layout/home/home_screen.dart | 4 +- .../main_pager/account_page.dart | 6 +- .../bottom_sheet_action_button.dart | 0 .../main_pager/chats_page.dart | 2 +- .../main_pager/main_pager.dart | 6 +- lib/layout/layout.dart | 2 +- 12 files changed, 197 insertions(+), 260 deletions(-) rename lib/layout/home/{home_account_ready/home_account_ready_main.dart => home_account_ready.dart} (67%) delete mode 100644 lib/layout/home/home_account_ready/home_account_ready.dart delete mode 100644 lib/layout/home/home_account_ready/home_account_ready_chat.dart rename lib/layout/home/{home_account_ready => }/main_pager/account_page.dart (94%) 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 (92%) rename lib/layout/home/{home_account_ready => }/main_pager/main_pager.dart (98%) diff --git a/lib/chat/views/chat_component_widget.dart b/lib/chat/views/chat_component_widget.dart index f95cebf..34ee220 100644 --- a/lib/chat/views/chat_component_widget.dart +++ b/lib/chat/views/chat_component_widget.dart @@ -192,161 +192,135 @@ class ChatComponentWidget extends StatelessWidget { chatComponentCubit.scrollOffset = 0; } - return 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(title, - textAlign: TextAlign.start, - style: textTheme.titleMedium!.copyWith( - color: scale.primaryScale.borderText)), - )), - const Spacer(), - IconButton( - icon: Icon(Icons.close, - color: scale.primaryScale.borderText), - onPressed: () async { - context.read().setActiveChat(null); - }).paddingLTRB(16, 0, 16, 0) - ]), - ), - Expanded( - child: DecoratedBox( - decoration: BoxDecoration( - color: scale.primaryScale.subtleBackground), - child: NotificationListener( - onNotification: (notification) { - if (chatComponentCubit.scrollOffset != 0) { - return false; - } - - if (!isFirstPage && - notification.metrics.pixels <= - ((notification.metrics.maxScrollExtent - - notification - .metrics.minScrollExtent) * - (1.0 - onEndReachedThreshold) + - notification.metrics.minScrollExtent)) { - // - final scrollOffset = - (notification.metrics.maxScrollExtent - - notification.metrics.minScrollExtent) * - (1.0 - onEndReachedThreshold); - - chatComponentCubit.scrollOffset = scrollOffset; - - // - singleFuture(chatComponentState.chatKey, () async { - await _handlePageForward(chatComponentCubit, - messageWindow, notification); - }); - } else if (!isLastPage && - notification.metrics.pixels >= - ((notification.metrics.maxScrollExtent - - notification - .metrics.minScrollExtent) * - onEndReachedThreshold + - notification.metrics.minScrollExtent)) { - // - final scrollOffset = - -(notification.metrics.maxScrollExtent - - notification.metrics.minScrollExtent) * - (1.0 - onEndReachedThreshold); - - chatComponentCubit.scrollOffset = scrollOffset; - // - singleFuture(chatComponentState.chatKey, () async { - await _handlePageBackward(chatComponentCubit, - messageWindow, notification); - }); - } - return false; - }, - child: ValueListenableBuilder( - valueListenable: - chatComponentState.textEditingController, - builder: (context, textEditingValue, __) { - final messageIsValid = utf8 - .encode(textEditingValue.text) - .lengthInBytes < - 2048; - - return Chat( - key: chatComponentState.chatKey, - theme: messageIsValid - ? chatTheme - : errorChatTheme, - messages: messageWindow.window.toList(), - scrollToBottomOnSend: isFirstPage, - scrollController: - chatComponentState.scrollController, - inputOptions: InputOptions( - inputClearMode: messageIsValid - ? InputClearMode.always - : InputClearMode.never, - textEditingController: - chatComponentState - .textEditingController), - // isLastPage: isLastPage, - // onEndReached: () async { - // await _handlePageBackward( - // chatComponentCubit, messageWindow); - // }, - //onEndReachedThreshold: onEndReachedThreshold, - //onAttachmentPressed: _handleAttachmentPressed, - //onMessageTap: _handleMessageTap, - //onPreviewDataFetched: _handlePreviewDataFetched, - onSendPressed: (pt) { - try { - if (!messageIsValid) { - showErrorToast( - context, - translate( - 'chat.message_too_long')); - return; - } - _handleSendPressed( - chatComponentCubit, pt); - } on FormatException { - showErrorToast( - context, - translate( - 'chat.message_too_long')); - } - }, - listBottomWidget: messageIsValid - ? null - : Text( - translate( - 'chat.message_too_long'), - style: TextStyle( - color: scale - .errorScale.primary)) - .toCenter(), - //showUserAvatars: false, - //showUserNames: true, - user: localUser, - emptyState: const EmptyChatWidget()) - .paddingLTRB(0, 2, 0, 0); - }))), - ), - ], + return 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(title, + textAlign: TextAlign.start, + style: textTheme.titleMedium! + .copyWith(color: scale.primaryScale.borderText)), + )), + const Spacer(), + IconButton( + icon: Icon(Icons.close, color: scale.primaryScale.borderText), + onPressed: () async { + context.read().setActiveChat(null); + }).paddingLTRB(16, 0, 16, 0) + ]), + ), + DecoratedBox( + decoration: + BoxDecoration(color: scale.primaryScale.subtleBackground), + child: NotificationListener( + onNotification: (notification) { + if (chatComponentCubit.scrollOffset != 0) { + return false; + } + + if (!isFirstPage && + notification.metrics.pixels <= + ((notification.metrics.maxScrollExtent - + notification.metrics.minScrollExtent) * + (1.0 - onEndReachedThreshold) + + notification.metrics.minScrollExtent)) { + // + final scrollOffset = (notification.metrics.maxScrollExtent - + notification.metrics.minScrollExtent) * + (1.0 - onEndReachedThreshold); + + chatComponentCubit.scrollOffset = scrollOffset; + + // + singleFuture(chatComponentState.chatKey, () async { + await _handlePageForward( + chatComponentCubit, messageWindow, notification); + }); + } else if (!isLastPage && + notification.metrics.pixels >= + ((notification.metrics.maxScrollExtent - + notification.metrics.minScrollExtent) * + onEndReachedThreshold + + notification.metrics.minScrollExtent)) { + // + final scrollOffset = + -(notification.metrics.maxScrollExtent - + notification.metrics.minScrollExtent) * + (1.0 - onEndReachedThreshold); + + chatComponentCubit.scrollOffset = scrollOffset; + // + singleFuture(chatComponentState.chatKey, () async { + await _handlePageBackward( + chatComponentCubit, messageWindow, notification); + }); + } + return false; + }, + child: ValueListenableBuilder( + valueListenable: chatComponentState.textEditingController, + builder: (context, textEditingValue, __) { + final messageIsValid = + utf8.encode(textEditingValue.text).lengthInBytes < + 2048; + + return Chat( + key: chatComponentState.chatKey, + theme: + messageIsValid ? chatTheme : errorChatTheme, + messages: messageWindow.window.toList(), + scrollToBottomOnSend: isFirstPage, + scrollController: + chatComponentState.scrollController, + inputOptions: InputOptions( + inputClearMode: messageIsValid + ? InputClearMode.always + : InputClearMode.never, + textEditingController: + chatComponentState.textEditingController), + // isLastPage: isLastPage, + // onEndReached: () async { + // await _handlePageBackward( + // chatComponentCubit, messageWindow); + // }, + //onEndReachedThreshold: onEndReachedThreshold, + //onAttachmentPressed: _handleAttachmentPressed, + //onMessageTap: _handleMessageTap, + //onPreviewDataFetched: _handlePreviewDataFetched, + onSendPressed: (pt) { + try { + if (!messageIsValid) { + showErrorToast(context, + translate('chat.message_too_long')); + return; + } + _handleSendPressed(chatComponentCubit, pt); + } on FormatException { + showErrorToast(context, + translate('chat.message_too_long')); + } + }, + listBottomWidget: messageIsValid + ? null + : Text(translate('chat.message_too_long'), + style: TextStyle( + color: scale.errorScale.primary)) + .toCenter(), + //showUserAvatars: false, + //showUserNames: true, + user: localUser, + emptyState: const EmptyChatWidget()) + .paddingLTRB(0, 2, 0, 0); + }))).expanded(), + ], ); } } diff --git a/lib/chat/views/no_conversation_widget.dart b/lib/chat/views/no_conversation_widget.dart index ccbc888..77502e1 100644 --- a/lib/chat/views/no_conversation_widget.dart +++ b/lib/chat/views/no_conversation_widget.dart @@ -19,6 +19,7 @@ class NoConversationWidget extends StatelessWidget { color: scale.primaryScale.appBackground, ), child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( diff --git a/lib/layout/home/home.dart b/lib/layout/home/home.dart index 74990ef..741483b 100644 --- a/lib/layout/home/home.dart +++ b/lib/layout/home/home.dart @@ -2,6 +2,6 @@ export 'drawer_menu/drawer_menu.dart'; 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_account_ready.dart'; export 'home_no_active.dart'; export 'home_screen.dart'; diff --git a/lib/layout/home/home_account_ready/home_account_ready_main.dart b/lib/layout/home/home_account_ready.dart similarity index 67% rename from lib/layout/home/home_account_ready/home_account_ready_main.dart rename to lib/layout/home/home_account_ready.dart index 440f4dc..5c7dd73 100644 --- a/lib/layout/home/home_account_ready/home_account_ready_main.dart +++ b/lib/layout/home/home_account_ready.dart @@ -4,20 +4,20 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart'; -import '../../../account_manager/account_manager.dart'; -import '../../../chat/chat.dart'; -import '../../../proto/proto.dart' as proto; -import '../../../theme/theme.dart'; +import '../../account_manager/account_manager.dart'; +import '../../chat/chat.dart'; +import '../../proto/proto.dart' as proto; +import '../../theme/theme.dart'; import 'main_pager/main_pager.dart'; -class HomeAccountReadyMain extends StatefulWidget { - const HomeAccountReadyMain({super.key}); +class HomeAccountReady extends StatefulWidget { + const HomeAccountReady({super.key}); @override - State createState() => _HomeAccountReadyMainState(); + State createState() => _HomeAccountReadyState(); } -class _HomeAccountReadyMainState extends State { +class _HomeAccountReadyState extends State { @override void initState() { super.initState(); @@ -98,7 +98,6 @@ class _HomeAccountReadyMainState extends State { phone: false, ); - final w = MediaQuery.of(context).size.width; final theme = Theme.of(context); final scale = theme.extension()!; final scaleConfig = theme.extension()!; @@ -111,51 +110,56 @@ class _HomeAccountReadyMainState extends State { // _chatAnimationController.reset(); // } - late final bool offstageLeft; - late final bool offstageRight; - late final double leftWidth; - late final double rightWidth; - if (isLarge) { - leftWidth = 300; - rightWidth = w - 300 - 2; - offstageLeft = false; - offstageRight = false; - } else { - leftWidth = w; - rightWidth = w; - if (hasActiveChat) { - offstageLeft = true; - offstageRight = false; - } else { - offstageLeft = false; - offstageRight = true; - } - } + return LayoutBuilder(builder: (context, constraints) { + const leftColumnSize = 300.0; - return Row(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Offstage( - offstage: offstageLeft, - child: ConstrainedBox( - constraints: - BoxConstraints(minWidth: leftWidth, maxWidth: leftWidth), - child: buildLeftPane(context))), - Offstage( - offstage: offstageLeft || offstageRight, - child: SizedBox( - width: 2, - height: double.infinity, - child: ColoredBox( - color: scaleConfig.preferBorders - ? scale.primaryScale.subtleBorder - : scale.primaryScale.subtleBackground))), - Offstage( - offstage: offstageRight, - child: ConstrainedBox( - constraints: - BoxConstraints(minWidth: rightWidth, maxWidth: rightWidth), - child: buildRightPane(context), - )), - ]); + late final bool visibleLeft; + late final bool visibleRight; + late final double leftWidth; + late final double rightWidth; + if (isLarge) { + visibleLeft = true; + visibleRight = true; + leftWidth = leftColumnSize; + rightWidth = constraints.maxWidth - leftColumnSize - 2; + } else { + if (hasActiveChat) { + visibleLeft = false; + visibleRight = true; + leftWidth = leftColumnSize; + rightWidth = constraints.maxWidth; + } else { + visibleLeft = true; + visibleRight = false; + leftWidth = constraints.maxWidth; + rightWidth = 400; // whatever + } + } + + return Row(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + Offstage( + offstage: !visibleLeft, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: leftWidth), + child: buildLeftPane(context))), + Offstage( + offstage: !(visibleLeft && visibleRight), + child: SizedBox( + width: 2, + height: double.infinity, + child: ColoredBox( + color: scaleConfig.preferBorders + ? scale.primaryScale.subtleBorder + : scale.primaryScale.subtleBackground))), + Offstage( + offstage: !visibleRight, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: constraints.maxHeight, maxWidth: rightWidth), + child: buildRightPane(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 deleted file mode 100644 index 5171239..0000000 --- a/lib/layout/home/home_account_ready/home_account_ready.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'home_account_ready_chat.dart'; -export 'home_account_ready_main.dart'; 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 deleted file mode 100644 index 625e01f..0000000 --- a/lib/layout/home/home_account_ready/home_account_ready_chat.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../../../chat/chat.dart'; - -class HomeAccountReadyChat extends StatefulWidget { - const HomeAccountReadyChat({super.key}); - - @override - HomeAccountReadyChatState createState() => HomeAccountReadyChatState(); -} - -class HomeAccountReadyChatState extends State { - @override - void initState() { - super.initState(); - } - - @override - void dispose() { - super.dispose(); - } - - Widget buildChatComponent(BuildContext context) { - final activeChatLocalConversationKey = - context.watch().state; - if (activeChatLocalConversationKey == null) { - return const NoConversationWidget(); - } - return ChatComponentWidget.builder( - localConversationRecordKey: activeChatLocalConversationKey, - key: ValueKey(activeChatLocalConversationKey)); - } - - @override - Widget build(BuildContext context) => SafeArea( - bottom: false, - child: buildChatComponent(context), - ); -} diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index 9083790..c9fe1e5 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -13,7 +13,7 @@ import 'drawer_menu/drawer_menu.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_account_ready.dart'; import 'home_no_active.dart'; class HomeScreen extends StatefulWidget { @@ -64,7 +64,7 @@ class HomeScreenState extends State // Re-export all ready blocs to the account display subtree return perAccountCollectionState.provide( - child: const HomeAccountReadyMain()); + child: const HomeAccountReady()); } } diff --git a/lib/layout/home/home_account_ready/main_pager/account_page.dart b/lib/layout/home/main_pager/account_page.dart similarity index 94% rename from lib/layout/home/home_account_ready/main_pager/account_page.dart rename to lib/layout/home/main_pager/account_page.dart index 1b48e5b..c9a30b1 100644 --- a/lib/layout/home/home_account_ready/main_pager/account_page.dart +++ b/lib/layout/home/main_pager/account_page.dart @@ -4,9 +4,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import '../../../../contact_invitation/contact_invitation.dart'; -import '../../../../contacts/contacts.dart'; -import '../../../../theme/theme.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/home/home_account_ready/main_pager/bottom_sheet_action_button.dart b/lib/layout/home/main_pager/bottom_sheet_action_button.dart similarity index 100% rename from lib/layout/home/home_account_ready/main_pager/bottom_sheet_action_button.dart rename to lib/layout/home/main_pager/bottom_sheet_action_button.dart diff --git a/lib/layout/home/home_account_ready/main_pager/chats_page.dart b/lib/layout/home/main_pager/chats_page.dart similarity index 92% rename from lib/layout/home/home_account_ready/main_pager/chats_page.dart rename to lib/layout/home/main_pager/chats_page.dart index 8811607..1765b62 100644 --- a/lib/layout/home/home_account_ready/main_pager/chats_page.dart +++ b/lib/layout/home/main_pager/chats_page.dart @@ -1,7 +1,7 @@ import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/material.dart'; -import '../../../../chat_list/chat_list.dart'; +import '../../../chat_list/chat_list.dart'; class ChatsPage extends StatefulWidget { const ChatsPage({super.key}); diff --git a/lib/layout/home/home_account_ready/main_pager/main_pager.dart b/lib/layout/home/main_pager/main_pager.dart similarity index 98% rename from lib/layout/home/home_account_ready/main_pager/main_pager.dart rename to lib/layout/home/main_pager/main_pager.dart index 75ca6ed..bfa476f 100644 --- a/lib/layout/home/home_account_ready/main_pager/main_pager.dart +++ b/lib/layout/home/main_pager/main_pager.dart @@ -10,9 +10,9 @@ import 'package:flutter_translate/flutter_translate.dart'; import 'package:preload_page_view/preload_page_view.dart'; import 'package:provider/provider.dart'; -import '../../../../chat/chat.dart'; -import '../../../../contact_invitation/contact_invitation.dart'; -import '../../../../theme/theme.dart'; +import '../../../chat/chat.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'; diff --git a/lib/layout/layout.dart b/lib/layout/layout.dart index 985a099..27975d5 100644 --- a/lib/layout/layout.dart +++ b/lib/layout/layout.dart @@ -1,4 +1,4 @@ export 'default_app_bar.dart'; export 'home/home.dart'; -export 'home/home_account_ready/main_pager/main_pager.dart'; +export 'home/main_pager/main_pager.dart'; export 'splash.dart'; From 2fa3cbd21c865edf3517ae67d3cf572f78c0aa74 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 11 Jul 2024 23:04:08 -0400 Subject: [PATCH 161/270] sliver refactor --- assets/i18n/en.json | 8 +- ios/Podfile.lock | 6 + .../views/edit_account_page.dart | 28 +-- .../views/new_account_page.dart | 23 ++- lib/account_manager/views/profile_widget.dart | 2 +- .../views/show_recovery_key_page.dart | 18 +- lib/app.dart | 3 +- lib/chat_list/views/chat_list_widget.dart | 57 +++--- .../views/contact_invitation_list_widget.dart | 91 +++++----- lib/contacts/views/contact_list_widget.dart | 79 ++++----- lib/layout/home/drawer_menu/drawer_menu.dart | 28 +-- lib/layout/home/home_screen.dart | 5 - lib/layout/home/main_pager/account_page.dart | 86 --------- lib/layout/home/main_pager/chats_page.dart | 5 +- lib/layout/home/main_pager/contacts_page.dart | 59 +++++++ lib/layout/home/main_pager/main_pager.dart | 51 ++---- lib/layout/splash.dart | 15 +- lib/settings/settings_page.dart | 6 - lib/theme/views/styled_scaffold.dart | 6 +- lib/theme/views/widget_helpers.dart | 165 +++++++++++++++++- lib/tools/native_safe_area.dart | 111 ++++++++++++ lib/tools/window_control.dart | 53 +++++- lib/veilid_processor/views/developer.dart | 113 ++++++------ pubspec.lock | 72 ++++++++ pubspec.yaml | 7 + 25 files changed, 710 insertions(+), 387 deletions(-) delete mode 100644 lib/layout/home/main_pager/account_page.dart create mode 100644 lib/layout/home/main_pager/contacts_page.dart create mode 100644 lib/tools/native_safe_area.dart diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 577d6bb..e24d560 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -97,8 +97,9 @@ "invalid_account_title": "Invalid Account", "invalid_account_text": "Account is invalid, removing from list" }, - "account_page": { - "contact_invitations": "Contact Invitations" + "contacts_page": { + "contacts": "Contacts", + "invitations": "Invitations" }, "add_contact_sheet": { "new_contact": "New Contact", @@ -176,14 +177,13 @@ "password_does_not_match": "Password does not match" }, "contact_list": { - "title": "Contacts", "invite_people": "Invite people to VeilidChat", "search": "Search contacts", "invitation": "Invitation" }, "chat_list": { "search": "Search chats", - "start_a_conversation": "Start a conversation", + "start_a_conversation": "Start A Conversation", "chats": "Chats", "groups": "Groups" }, diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f9cadda..3a17844 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -58,6 +58,8 @@ PODS: - nanopb/encode (= 2.30910.0) - nanopb/decode (2.30910.0) - nanopb/encode (2.30910.0) + - native_device_orientation (0.0.1): + - Flutter - package_info_plus (0.4.5): - Flutter - pasteboard (0.0.1): @@ -91,6 +93,7 @@ DEPENDENCIES: - Flutter (from `Flutter`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`) + - native_device_orientation (from `.symlinks/plugins/native_device_orientation/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - pasteboard (from `.symlinks/plugins/pasteboard/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) @@ -129,6 +132,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_native_splash/ios" mobile_scanner: :path: ".symlinks/plugins/mobile_scanner/ios" + native_device_orientation: + :path: ".symlinks/plugins/native_device_orientation/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" pasteboard: @@ -169,6 +174,7 @@ SPEC CHECKSUMS: MLKitVision: e858c5f125ecc288e4a31127928301eaba9ae0c1 mobile_scanner: 8564358885a9253c43f822435b70f9345c87224f nanopb: 438bc412db1928dac798aa6fd75726007be04262 + native_device_orientation: 348b10c346a60ebbc62fb235a4fdb5d1b61a8f55 package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 diff --git a/lib/account_manager/views/edit_account_page.dart b/lib/account_manager/views/edit_account_page.dart index 47e2ab2..59adeaf 100644 --- a/lib/account_manager/views/edit_account_page.dart +++ b/lib/account_manager/views/edit_account_page.dart @@ -44,25 +44,11 @@ class EditAccountPage extends StatefulWidget { } } -class _EditAccountPageState extends State { - bool _isInAsyncCall = false; - - @override - void initState() { - super.initState(); - - WidgetsBinding.instance.addPostFrameCallback((_) async { - await changeWindowSetup( - TitleBarStyle.normal, OrientationCapability.portraitOnly); - }); - } - - @override - void dispose() { - unawaited( - changeWindowSetup(TitleBarStyle.normal, OrientationCapability.normal)); - super.dispose(); - } +class _EditAccountPageState extends WindowSetupState { + _EditAccountPageState() + : super( + titleBarStyle: TitleBarStyle.normal, + orientationCapability: OrientationCapability.portraitOnly); Widget _editAccountForm(BuildContext context, {required Future Function(GlobalKey) @@ -314,4 +300,8 @@ class _EditAccountPageState extends State { ]).paddingSymmetric(horizontal: 24, vertical: 8))) .withModalHUD(context, displayModalHUD); } + + //////////////////////////////////////////////////////////////////////////// + + bool _isInAsyncCall = false; } diff --git a/lib/account_manager/views/new_account_page.dart b/lib/account_manager/views/new_account_page.dart index cc2a333..943e79e 100644 --- a/lib/account_manager/views/new_account_page.dart +++ b/lib/account_manager/views/new_account_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -20,18 +22,11 @@ class NewAccountPage extends StatefulWidget { State createState() => _NewAccountPageState(); } -class _NewAccountPageState extends State { - bool _isInAsyncCall = false; - - @override - void initState() { - super.initState(); - - WidgetsBinding.instance.addPostFrameCallback((_) async { - await changeWindowSetup( - TitleBarStyle.normal, OrientationCapability.portraitOnly); - }); - } +class _NewAccountPageState extends WindowSetupState { + _NewAccountPageState() + : super( + titleBarStyle: TitleBarStyle.normal, + orientationCapability: OrientationCapability.portraitOnly); Widget _newAccountForm(BuildContext context, {required Future Function(GlobalKey) onSubmit}) { @@ -120,4 +115,8 @@ class _NewAccountPageState extends State { )).paddingSymmetric(horizontal: 24, vertical: 8), ).withModalHUD(context, displayModalHUD); } + + //////////////////////////////////////////////////////////////////////////// + + bool _isInAsyncCall = false; } diff --git a/lib/account_manager/views/profile_widget.dart b/lib/account_manager/views/profile_widget.dart index 9d06648..672b13a 100644 --- a/lib/account_manager/views/profile_widget.dart +++ b/lib/account_manager/views/profile_widget.dart @@ -43,7 +43,7 @@ class ProfileWidget extends StatelessWidget { : scale.primaryScale.borderText, width: 2), borderRadius: BorderRadius.all( - Radius.circular(16 * scaleConfig.borderRadiusScale))), + Radius.circular(12 * scaleConfig.borderRadiusScale))), ), child: Row(children: [ const Spacer(), diff --git a/lib/account_manager/views/show_recovery_key_page.dart b/lib/account_manager/views/show_recovery_key_page.dart index 33de475..2649bcf 100644 --- a/lib/account_manager/views/show_recovery_key_page.dart +++ b/lib/account_manager/views/show_recovery_key_page.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; @@ -29,22 +30,17 @@ class ShowRecoveryKeyPage extends StatefulWidget { _name = name; @override - ShowRecoveryKeyPageState createState() => ShowRecoveryKeyPageState(); + State createState() => _ShowRecoveryKeyPageState(); final WritableSuperIdentity _writableSuperIdentity; final String _name; } -class ShowRecoveryKeyPageState extends State { - @override - void initState() { - super.initState(); - - WidgetsBinding.instance.addPostFrameCallback((_) async { - await changeWindowSetup( - TitleBarStyle.normal, OrientationCapability.portraitOnly); - }); - } +class _ShowRecoveryKeyPageState extends WindowSetupState { + _ShowRecoveryKeyPageState() + : super( + titleBarStyle: TitleBarStyle.normal, + orientationCapability: OrientationCapability.portraitOnly); Future _shareRecoveryKey( BuildContext context, Uint8List recoveryKey, String name) async { diff --git a/lib/app.dart b/lib/app.dart index 3480b18..ec5c9be 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -15,7 +15,6 @@ import 'init.dart'; import 'layout/splash.dart'; import 'router/router.dart'; import 'settings/settings.dart'; -import 'theme/models/theme_preference.dart'; import 'theme/theme.dart'; import 'tick.dart'; import 'tools/loggy.dart'; @@ -92,7 +91,7 @@ class VeilidChatApp extends StatelessWidget { Widget build(BuildContext context) => FutureProvider( initialData: null, create: (context) async => VeilidChatGlobalInit.initialize(), - builder: (context, child) { + builder: (context, __) { final globalInit = context.watch(); if (globalInit == null) { // Splash screen until we're done with init diff --git a/lib/chat_list/views/chat_list_widget.dart b/lib/chat_list/views/chat_list_widget.dart index e91cbba..67bcc5d 100644 --- a/lib/chat_list/views/chat_list_widget.dart +++ b/lib/chat_list/views/chat_list_widget.dart @@ -60,36 +60,33 @@ class ChatListWidget extends StatelessWidget { 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( - initialList: chatList.map((x) => x.value).toList(), - itemBuilder: (c) { - switch (c.whichKind()) { - case proto.Chat_Kind.direct: - return _itemBuilderDirect( - c.direct, - contactMap, - contactListV.busy || chatListV.busy); - case proto.Chat_Kind.group: - return const Text( - 'group chats not yet supported!'); - case proto.Chat_Kind.notSet: - throw StateError('unknown chat kind'); - } - }, - filter: (value) => - _itemFilter(contactMap, chatList, value), - spaceBetweenSearchAndList: 4, - inputDecoration: InputDecoration( - labelText: translate('chat_list.search'), - ), - ), - ).paddingAll(8)))) + child: styledTitleContainer( + context: context, + title: translate('chat_list.chats'), + child: (chatList.isEmpty) + ? const SizedBox.expand(child: EmptyChatListWidget()) + : SearchableList( + initialList: chatList.map((x) => x.value).toList(), + itemBuilder: (c) { + switch (c.whichKind()) { + case proto.Chat_Kind.direct: + return _itemBuilderDirect(c.direct, contactMap, + contactListV.busy || chatListV.busy); + case proto.Chat_Kind.group: + return const Text( + 'group chats not yet supported!'); + case proto.Chat_Kind.notSet: + throw StateError('unknown chat kind'); + } + }, + filter: (value) => + _itemFilter(contactMap, chatList, value), + spaceBetweenSearchAndList: 4, + inputDecoration: InputDecoration( + labelText: translate('chat_list.search'), + ), + ).paddingAll(8), + ))) .paddingLTRB(8, 0, 8, 8); }); } diff --git a/lib/contact_invitation/views/contact_invitation_list_widget.dart b/lib/contact_invitation/views/contact_invitation_list_widget.dart index ad72027..56a6b9a 100644 --- a/lib/contact_invitation/views/contact_invitation_list_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_list_widget.dart @@ -2,6 +2,7 @@ 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_translate/flutter_translate.dart'; import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; @@ -31,58 +32,58 @@ class ContactInvitationListWidget extends StatefulWidget { } class ContactInvitationListWidgetState - extends State { - final ScrollController _scrollController = ScrollController(); + extends State + with SingleTickerProviderStateMixin { + late final _controller = AnimationController( + vsync: this, duration: const Duration(milliseconds: 250), value: 1); + late final _animation = + CurvedAnimation(parent: _controller, curve: Curves.easeInOut); + bool _expanded = true; @override // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { final theme = Theme.of(context); - //final textTheme = theme.textTheme; + // final textTheme = theme.textTheme; final scale = theme.extension()!; final scaleConfig = theme.extension()!; - return Container( - width: double.infinity, - margin: const EdgeInsets.fromLTRB(4, 0, 4, 4), - decoration: ShapeDecoration( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16 * scaleConfig.borderRadiusScale), - )), - constraints: const BoxConstraints(maxHeight: 100), - child: Container( - width: double.infinity, - decoration: ShapeDecoration( - color: scale.primaryScale.subtleBackground, - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(16 * scaleConfig.borderRadiusScale), - )), - child: ListView.builder( - shrinkWrap: true, - controller: _scrollController, - itemCount: widget.contactInvitationRecordList.length, - itemBuilder: (context, index) { - if (index < 0 || - index >= widget.contactInvitationRecordList.length) { - return null; - } - return ContactInvitationItemWidget( - contactInvitationRecord: - widget.contactInvitationRecordList[index], - disabled: widget.disabled, - key: ObjectKey(widget.contactInvitationRecordList[index])) - .paddingLTRB(4, 2, 4, 2); - }, - findChildIndexCallback: (key) { - final index = widget.contactInvitationRecordList.indexOf( - (key as ObjectKey).value! as proto.ContactInvitationRecord); - if (index == -1) { - return null; - } - return index; - }, - ).paddingLTRB(4, 6, 4, 6)), - ); + return styledExpandingSliver( + context: context, + animation: _animation, + expanded: _expanded, + backgroundColor: scaleConfig.preferBorders + ? scale.primaryScale.subtleBackground + : scale.primaryScale.subtleBorder, + onTap: () { + setState(() { + _expanded = !_expanded; + }); + _controller.animateTo(_expanded ? 1 : 0); + }, + title: translate('contacts_page.invitations'), + sliver: SliverList.builder( + itemCount: widget.contactInvitationRecordList.length, + itemBuilder: (context, index) { + if (index < 0 || + index >= widget.contactInvitationRecordList.length) { + return null; + } + return ContactInvitationItemWidget( + contactInvitationRecord: + widget.contactInvitationRecordList[index], + disabled: widget.disabled, + key: ObjectKey(widget.contactInvitationRecordList[index])) + .paddingLTRB(4, 2, 4, 2); + }, + findChildIndexCallback: (key) { + final index = widget.contactInvitationRecordList.indexOf( + (key as ObjectKey).value! as proto.ContactInvitationRecord); + if (index == -1) { + return null; + } + return index; + }, + )); } } diff --git a/lib/contacts/views/contact_list_widget.dart b/lib/contacts/views/contact_list_widget.dart index eb9958d..f5d4e46 100644 --- a/lib/contacts/views/contact_list_widget.dart +++ b/lib/contacts/views/contact_list_widget.dart @@ -28,49 +28,50 @@ class ContactListWidget extends StatefulWidget { } } -class _ContactListWidgetState extends State { - @override - void initState() { - super.initState(); - } - - @override - void dispose() { - super.dispose(); - } - +class _ContactListWidgetState extends State + with SingleTickerProviderStateMixin { @override Widget build(BuildContext context) { final theme = Theme.of(context); + //final textTheme = theme.textTheme; final scale = theme.extension()!; + final scaleConfig = theme.extension()!; - return styledTitleContainer( - context: context, - title: translate('contact_list.title'), - child: SearchableList( - shrinkWrap: true, - initialList: widget.contactList.toList(), - itemBuilder: (c) => - ContactItemWidget(contact: c, disabled: widget.disabled) - .paddingLTRB(0, 4, 0, 0), - filter: (value) { - final lowerValue = value.toLowerCase(); - return widget.contactList - .where((element) => - element.nickname.toLowerCase().contains(lowerValue) || - element.profile.name.toLowerCase().contains(lowerValue) || - element.profile.pronouns.toLowerCase().contains(lowerValue)) - .toList(); - }, - searchFieldHeight: 40, - spaceBetweenSearchAndList: 4, - emptyWidget: const EmptyContactListWidget(), - defaultSuffixIconColor: scale.primaryScale.border, - closeKeyboardWhenScrolling: true, - inputDecoration: InputDecoration( - labelText: translate('contact_list.search'), - ), - ).paddingAll(8), - ).paddingLTRB(8, 0, 8, 8); + return SliverLayoutBuilder( + builder: (context, constraints) => styledHeaderSliver( + context: context, + backgroundColor: scaleConfig.preferBorders + ? scale.primaryScale.subtleBackground + : scale.primaryScale.subtleBorder, + title: translate('contacts_page.contacts'), + sliver: SliverFillRemaining( + child: SearchableList.sliver( + initialList: widget.contactList.toList(), + itemBuilder: (c) => + ContactItemWidget(contact: c, disabled: widget.disabled) + .paddingLTRB(0, 4, 0, 0), + filter: (value) { + final lowerValue = value.toLowerCase(); + return widget.contactList + .where((element) => + element.nickname.toLowerCase().contains(lowerValue) || + element.profile.name + .toLowerCase() + .contains(lowerValue) || + element.profile.pronouns + .toLowerCase() + .contains(lowerValue)) + .toList(); + }, + searchFieldHeight: 40, + spaceBetweenSearchAndList: 4, + emptyWidget: const EmptyContactListWidget(), + defaultSuffixIconColor: scale.primaryScale.border, + closeKeyboardWhenScrolling: true, + inputDecoration: InputDecoration( + labelText: translate('contact_list.search'), + ), + ), + ))); } } diff --git a/lib/layout/home/drawer_menu/drawer_menu.dart b/lib/layout/home/drawer_menu/drawer_menu.dart index 8f1ec07..6d7209c 100644 --- a/lib/layout/home/drawer_menu/drawer_menu.dart +++ b/lib/layout/home/drawer_menu/drawer_menu.dart @@ -143,7 +143,7 @@ class _DrawerMenuState extends State { (scaleConfig.preferBorders || scaleConfig.useVisualIndicators) ? activeBorder : null, - borderRadius: 16 * scaleConfig.borderRadiusScale, + borderRadius: 12 * scaleConfig.borderRadiusScale, callback: callback, footerButtonIcon: loggedIn ? Icons.edit_outlined : null, footerCallback: footerCallback, @@ -197,11 +197,11 @@ class _DrawerMenuState extends State { loading: () => _wrapInBox( child: buildProgressIndicator(), color: scaleScheme.grayScale.subtleBorder, - borderRadius: 16 * scaleConfig.borderRadiusScale), + borderRadius: 12 * scaleConfig.borderRadiusScale), error: (err, st) => _wrapInBox( child: errorPage(err, st), color: scaleScheme.errorScale.subtleBorder, - borderRadius: 16 * scaleConfig.borderRadiusScale), + borderRadius: 12 * scaleConfig.borderRadiusScale), ); loggedInAccounts.add(loggedInAccount.paddingLTRB(0, 0, 0, 8)); } else { @@ -254,7 +254,7 @@ class _DrawerMenuState extends State { return IconButton( icon: icon, color: border, - constraints: const BoxConstraints.expand(height: 64, width: 64), + constraints: const BoxConstraints.expand(height: 48, width: 48), style: ButtonStyle( backgroundColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.hovered)) { @@ -269,18 +269,18 @@ class _DrawerMenuState extends State { return RoundedRectangleBorder( side: BorderSide(color: hoverBorder, width: 2), borderRadius: BorderRadius.all( - Radius.circular(16 * scaleConfig.borderRadiusScale))); + Radius.circular(12 * scaleConfig.borderRadiusScale))); } if (states.contains(WidgetState.focused)) { return RoundedRectangleBorder( side: BorderSide(color: activeBorder, width: 2), borderRadius: BorderRadius.all( - Radius.circular(16 * scaleConfig.borderRadiusScale))); + Radius.circular(12 * scaleConfig.borderRadiusScale))); } return RoundedRectangleBorder( side: BorderSide(color: border, width: 2), borderRadius: BorderRadius.all( - Radius.circular(16 * scaleConfig.borderRadiusScale))); + Radius.circular(12 * scaleConfig.borderRadiusScale))); })), tooltip: tooltip, onPressed: onPressed); @@ -413,12 +413,18 @@ class _DrawerMenuState extends State { _getBottomButtons(), Row(children: [ Text('${translate('menu.version')} $packageInfoVersion', - style: theme.textTheme.labelMedium! - .copyWith(color: scale.tertiaryScale.hoverBorder)), + style: theme.textTheme.labelMedium!.copyWith( + color: scaleConfig.preferBorders + ? scale.tertiaryScale.hoverBorder + : scale.tertiaryScale.subtleBackground)), const Spacer(), SignalStrengthMeterWidget( - color: scale.tertiaryScale.hoverBorder, - inactiveColor: scale.tertiaryScale.border, + color: scaleConfig.preferBorders + ? scale.tertiaryScale.hoverBorder + : scale.tertiaryScale.subtleBackground, + inactiveColor: scaleConfig.preferBorders + ? scale.tertiaryScale.border + : scale.tertiaryScale.elementBackground, ), ]) ]).paddingAll(16), diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index c9fe1e5..f482757 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -8,7 +8,6 @@ import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../theme/theme.dart'; -import '../../tools/tools.dart'; import 'drawer_menu/drawer_menu.dart'; import 'home_account_invalid.dart'; import 'home_account_locked.dart'; @@ -37,9 +36,6 @@ class HomeScreenState extends State .indexWhere((x) => x.superIdentity.recordKey == activeLocalAccount); final canClose = activeIndex != -1; - await changeWindowSetup( - TitleBarStyle.normal, OrientationCapability.normal); - if (!canClose) { await _zoomDrawerController.open!(); } @@ -129,7 +125,6 @@ class HomeScreenState extends State final canClose = activeIndex != -1; return SafeArea( - bottom: false, child: DefaultTextStyle( style: theme.textTheme.bodySmall!, child: ZoomDrawer( diff --git a/lib/layout/home/main_pager/account_page.dart b/lib/layout/home/main_pager/account_page.dart deleted file mode 100644 index c9a30b1..0000000 --- a/lib/layout/home/main_pager/account_page.dart +++ /dev/null @@ -1,86 +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_bloc/flutter_bloc.dart'; -import 'package:flutter_translate/flutter_translate.dart'; - -import '../../../contact_invitation/contact_invitation.dart'; -import '../../../contacts/contacts.dart'; -import '../../../theme/theme.dart'; - -class AccountPage extends StatefulWidget { - const AccountPage({ - super.key, - }); - - @override - AccountPageState createState() => AccountPageState(); -} - -class AccountPageState extends State { - @override - void initState() { - super.initState(); - } - - @override - void dispose() { - super.dispose(); - } - - @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - final theme = Theme.of(context); - final textTheme = theme.textTheme; - final scale = theme.extension()!; - final scaleConfig = theme.extension()!; - - final cilState = context.watch().state; - final cilBusy = cilState.busy; - final contactInvitationRecordList = - cilState.state.asData?.value.map((x) => x.value).toIList() ?? - const IListConst([]); - - final ciState = context.watch().state; - final ciBusy = ciState.busy; - final contactList = - ciState.state.asData?.value.map((x) => x.value).toIList() ?? - const IListConst([]); - - return SizedBox( - child: Column(children: [ - if (contactInvitationRecordList.isNotEmpty) - ExpansionTile( - tilePadding: const EdgeInsets.fromLTRB(8, 0, 8, 0), - backgroundColor: scale.primaryScale.border, - collapsedBackgroundColor: scale.primaryScale.border, - dense: true, - minTileHeight: 16, - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(16 * scaleConfig.borderRadiusScale), - ), - collapsedShape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(16 * scaleConfig.borderRadiusScale), - ), - title: Text( - translate('account_page.contact_invitations'), - textAlign: TextAlign.center, - style: textTheme.titleSmall! - .copyWith(color: scale.primaryScale.borderText), - ), - iconColor: scale.primaryScale.borderText, - collapsedIconColor: scale.primaryScale.borderText, - initiallyExpanded: true, - children: [ - ContactInvitationListWidget( - contactInvitationRecordList: contactInvitationRecordList, - disabled: cilBusy) - ], - ).paddingLTRB(8, 0, 8, 8), - ContactListWidget(contactList: contactList, disabled: ciBusy).expanded(), - ])); - } -} diff --git a/lib/layout/home/main_pager/chats_page.dart b/lib/layout/home/main_pager/chats_page.dart index 1765b62..2146b3d 100644 --- a/lib/layout/home/main_pager/chats_page.dart +++ b/lib/layout/home/main_pager/chats_page.dart @@ -1,4 +1,3 @@ -import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/material.dart'; import '../../../chat_list/chat_list.dart'; @@ -24,8 +23,6 @@ class ChatsPageState extends State { @override // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { - return Column(children: [ - const ChatListWidget().expanded(), - ]); + return const ChatListWidget(); } } diff --git a/lib/layout/home/main_pager/contacts_page.dart b/lib/layout/home/main_pager/contacts_page.dart new file mode 100644 index 0000000..d858270 --- /dev/null +++ b/lib/layout/home/main_pager/contacts_page.dart @@ -0,0 +1,59 @@ +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 '../../../contact_invitation/contact_invitation.dart'; +import '../../../contacts/contacts.dart'; + +class ContactsPage extends StatefulWidget { + const ContactsPage({ + super.key, + }); + + @override + ContactsPageState createState() => ContactsPageState(); +} + +class ContactsPageState extends State { + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + // ignore: prefer_expression_function_bodies + Widget build(BuildContext context) { + // final theme = Theme.of(context); + // final textTheme = theme.textTheme; + // final scale = theme.extension()!; + // final scaleConfig = theme.extension()!; + + final cilState = context.watch().state; + final cilBusy = cilState.busy; + final contactInvitationRecordList = + cilState.state.asData?.value.map((x) => x.value).toIList() ?? + const IListConst([]); + + final ciState = context.watch().state; + final ciBusy = ciState.busy; + final contactList = + ciState.state.asData?.value.map((x) => x.value).toIList() ?? + const IListConst([]); + + return CustomScrollView(slivers: [ + if (contactInvitationRecordList.isNotEmpty) + SliverPadding( + padding: const EdgeInsets.only(bottom: 8), + sliver: ContactInvitationListWidget( + contactInvitationRecordList: contactInvitationRecordList, + disabled: cilBusy)), + ContactListWidget(contactList: contactList, disabled: ciBusy), + ]).paddingLTRB(8, 0, 8, 8); + } +} diff --git a/lib/layout/home/main_pager/main_pager.dart b/lib/layout/home/main_pager/main_pager.dart index bfa476f..68ef39b 100644 --- a/lib/layout/home/main_pager/main_pager.dart +++ b/lib/layout/home/main_pager/main_pager.dart @@ -13,9 +13,9 @@ import 'package:provider/provider.dart'; import '../../../chat/chat.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'; +import 'contacts_page.dart'; class MainPager extends StatefulWidget { const MainPager({super.key}); @@ -41,25 +41,6 @@ class MainPagerState extends State with TickerProviderStateMixin { super.dispose(); } - bool _onScrollNotification(ScrollNotification notification) { - if (notification is UserScrollNotification && - notification.metrics.axis == Axis.vertical) { - switch (notification.direction) { - case ScrollDirection.forward: - // _hideBottomBarAnimationController.reverse(); - // _fabAnimationController.forward(from: 0); - break; - case ScrollDirection.reverse: - // _hideBottomBarAnimationController.forward(); - // _fabAnimationController.reverse(from: 1); - break; - case ScrollDirection.idle: - break; - } - } - return false; - } - Future scanContactInvitationDialog(BuildContext context) async { await showDialog( context: context, @@ -162,21 +143,19 @@ class MainPagerState extends State with TickerProviderStateMixin { return Scaffold( //extendBody: true, backgroundColor: Colors.transparent, - body: NotificationListener( - onNotification: _onScrollNotification, - child: PreloadPageView( - key: _pageViewKey, - controller: pageController, - preloadPagesCount: 2, - onPageChanged: (index) { - setState(() { - currentPage = index; - }); - }, - children: const [ - AccountPage(), - ChatsPage(), - ])), + body: PreloadPageView( + key: _pageViewKey, + controller: pageController, + preloadPagesCount: 2, + onPageChanged: (index) { + setState(() { + currentPage = index; + }); + }, + children: const [ + ContactsPage(), + ChatsPage(), + ]), // appBar: AppBar( // toolbarHeight: 24, // title: Text( @@ -240,7 +219,7 @@ class MainPagerState extends State with TickerProviderStateMixin { // ]; final _fabIconList = [ Icons.person_add_sharp, - Icons.add_comment_sharp, + Icons.chat, ]; final _bottomLabelList = [ translate('pager.contacts'), diff --git a/lib/layout/splash.dart b/lib/layout/splash.dart index 2113193..c3af797 100644 --- a/lib/layout/splash.dart +++ b/lib/layout/splash.dart @@ -11,16 +11,11 @@ class Splash extends StatefulWidget { State createState() => _SplashState(); } -class _SplashState extends State { - @override - void initState() { - super.initState(); - - WidgetsBinding.instance.addPostFrameCallback((_) async { - await changeWindowSetup( - TitleBarStyle.hidden, OrientationCapability.normal); - }); - } +class _SplashState extends WindowSetupState { + _SplashState() + : super( + titleBarStyle: TitleBarStyle.hidden, + orientationCapability: OrientationCapability.portraitOnly); @override Widget build(BuildContext context) => PopScope( diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart index eefbbd2..5b92643 100644 --- a/lib/settings/settings_page.dart +++ b/lib/settings/settings_page.dart @@ -7,7 +7,6 @@ import 'package:go_router/go_router.dart'; import '../layout/default_app_bar.dart'; import '../theme/theme.dart'; -import '../tools/tools.dart'; import '../veilid_processor/veilid_processor.dart'; import 'settings.dart'; @@ -26,11 +25,6 @@ class SettingsPageState extends State { @override void initState() { super.initState(); - - WidgetsBinding.instance.addPostFrameCallback((_) async { - await changeWindowSetup( - TitleBarStyle.normal, OrientationCapability.normal); - }); } @override diff --git a/lib/theme/views/styled_scaffold.dart b/lib/theme/views/styled_scaffold.dart index 055684b..af9e4eb 100644 --- a/lib/theme/views/styled_scaffold.dart +++ b/lib/theme/views/styled_scaffold.dart @@ -12,7 +12,7 @@ class StyledScaffold extends StatelessWidget { final scale = theme.extension()!; final scaleConfig = theme.extension()!; - return isDesktop + final scaffold = isDesktop ? clipBorder( clipEnabled: true, borderEnabled: scaleConfig.useVisualIndicators, @@ -21,6 +21,10 @@ class StyledScaffold extends StatelessWidget { child: Scaffold(appBar: appBar, body: body, key: key)) .paddingAll(32) : Scaffold(appBar: appBar, body: body, key: key); + + return GestureDetector( + onTap: () => FocusManager.instance.primaryFocus?.unfocus(), + child: scaffold); } //////////////////////////////////////////////////////////////////////////// diff --git a/lib/theme/views/widget_helpers.dart b/lib/theme/views/widget_helpers.dart index 1bd71bc..6e485fc 100644 --- a/lib/theme/views/widget_helpers.dart +++ b/lib/theme/views/widget_helpers.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:async_tools/async_tools.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; @@ -5,8 +7,10 @@ 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_sticky_header/flutter_sticky_header.dart'; import 'package:motion_toast/motion_toast.dart'; import 'package:quickalert/quickalert.dart'; +import 'package:sliver_expandable/sliver_expandable.dart'; import '../theme.dart'; @@ -132,7 +136,7 @@ void showErrorToast(BuildContext context, String message) { contentPadding: const EdgeInsets.all(16), primaryColor: scale.errorScale.elementBackground, secondaryColor: scale.errorScale.calloutBackground, - borderRadius: 16 * scaleConfig.borderRadiusScale, + borderRadius: 12 * scaleConfig.borderRadiusScale, toastDuration: const Duration(seconds: 4), animationDuration: const Duration(milliseconds: 1000), displayBorder: scaleConfig.useVisualIndicators, @@ -152,7 +156,7 @@ void showInfoToast(BuildContext context, String message) { contentPadding: const EdgeInsets.all(16), primaryColor: scale.tertiaryScale.elementBackground, secondaryColor: scale.tertiaryScale.calloutBackground, - borderRadius: 16 * scaleConfig.borderRadiusScale, + borderRadius: 12 * scaleConfig.borderRadiusScale, toastDuration: const Duration(seconds: 2), animationDuration: const Duration(milliseconds: 500), displayBorder: scaleConfig.useVisualIndicators, @@ -160,6 +164,159 @@ void showInfoToast(BuildContext context, String message) { ).show(context); } +SliverAppBar styledSliverAppBar( + {required BuildContext context, required String title, Color? titleColor}) { + final theme = Theme.of(context); + final scale = theme.extension()!; + //final scaleConfig = theme.extension()!; + final textTheme = theme.textTheme; + + return SliverAppBar( + title: Text( + title, + style: textTheme.titleSmall! + .copyWith(color: titleColor ?? scale.primaryScale.borderText), + ), + pinned: true, + ); +} + +Widget styledHeaderSliver( + {required BuildContext context, + required String title, + required Widget sliver, + Color? borderColor, + Color? innerColor, + Color? titleColor, + Color? backgroundColor, + void Function()? onTap}) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + final textTheme = theme.textTheme; + + return SliverStickyHeader( + header: ColoredBox( + color: backgroundColor ?? Colors.transparent, + child: DecoratedBox( + decoration: ShapeDecoration( + color: borderColor ?? scale.primaryScale.border, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: + Radius.circular(12 * scaleConfig.borderRadiusScale), + topRight: Radius.circular( + 12 * scaleConfig.borderRadiusScale)))), + child: ListTile( + onTap: onTap, + title: Text(title, + textAlign: TextAlign.center, + style: textTheme.titleSmall!.copyWith( + color: titleColor ?? scale.primaryScale.borderText)), + ), + )), + sliver: DecoratedSliver( + decoration: ShapeDecoration( + color: borderColor ?? scale.primaryScale.border, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + bottomLeft: + Radius.circular(8 * scaleConfig.borderRadiusScale), + bottomRight: + Radius.circular(8 * scaleConfig.borderRadiusScale)))), + sliver: SliverPadding( + padding: const EdgeInsets.all(4), + sliver: DecoratedSliver( + decoration: ShapeDecoration( + color: innerColor ?? scale.primaryScale.subtleBackground, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 8 * scaleConfig.borderRadiusScale))), + sliver: SliverPadding( + padding: const EdgeInsets.all(8), + sliver: sliver, + )))), + ); +} + +Widget styledExpandingSliver( + {required BuildContext context, + required String title, + required Widget sliver, + required bool expanded, + required Animation animation, + Color? borderColor, + Color? innerColor, + Color? titleColor, + Color? backgroundColor, + void Function()? onTap}) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + final textTheme = theme.textTheme; + + return SliverStickyHeader( + header: ColoredBox( + color: backgroundColor ?? Colors.transparent, + child: DecoratedBox( + decoration: ShapeDecoration( + color: borderColor ?? scale.primaryScale.border, + shape: RoundedRectangleBorder( + borderRadius: expanded + ? BorderRadius.only( + topLeft: Radius.circular( + 12 * scaleConfig.borderRadiusScale), + topRight: Radius.circular( + 12 * scaleConfig.borderRadiusScale)) + : BorderRadius.circular( + 12 * scaleConfig.borderRadiusScale))), + child: ListTile( + onTap: onTap, + title: Text(title, + textAlign: TextAlign.center, + style: textTheme.titleSmall!.copyWith( + color: titleColor ?? scale.primaryScale.borderText)), + trailing: AnimatedBuilder( + animation: animation, + builder: (context, child) => Transform.rotate( + angle: (animation.value - 0.5) * pi, + child: child, + ), + child: Icon(Icons.chevron_left, + color: borderColor ?? scale.primaryScale.borderText), + ), + ), + )), + sliver: SliverExpandable( + sliver: DecoratedSliver( + decoration: ShapeDecoration( + color: borderColor ?? scale.primaryScale.border, + shape: RoundedRectangleBorder( + borderRadius: expanded + ? BorderRadius.only( + bottomLeft: Radius.circular( + 8 * scaleConfig.borderRadiusScale), + bottomRight: Radius.circular( + 8 * scaleConfig.borderRadiusScale)) + : BorderRadius.circular( + 8 * scaleConfig.borderRadiusScale))), + sliver: SliverPadding( + padding: const EdgeInsets.all(4), + sliver: DecoratedSliver( + decoration: ShapeDecoration( + color: + innerColor ?? scale.primaryScale.subtleBackground, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 8 * scaleConfig.borderRadiusScale))), + sliver: SliverPadding( + padding: const EdgeInsets.all(8), + sliver: sliver, + )))), + animation: animation, + )); +} + Widget styledTitleContainer({ required BuildContext context, required String title, @@ -178,7 +335,7 @@ Widget styledTitleContainer({ color: borderColor ?? scale.primaryScale.border, shape: RoundedRectangleBorder( borderRadius: - BorderRadius.circular(16 * scaleConfig.borderRadiusScale), + BorderRadius.circular(12 * scaleConfig.borderRadiusScale), )), child: Column(children: [ Text( @@ -192,7 +349,7 @@ Widget styledTitleContainer({ backgroundColor ?? scale.primaryScale.subtleBackground, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( - 16 * scaleConfig.borderRadiusScale), + 12 * scaleConfig.borderRadiusScale), )), child: child) .paddingAll(4) diff --git a/lib/tools/native_safe_area.dart b/lib/tools/native_safe_area.dart new file mode 100644 index 0000000..ed3746e --- /dev/null +++ b/lib/tools/native_safe_area.dart @@ -0,0 +1,111 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:native_device_orientation/native_device_orientation.dart'; + +class NativeSafeArea extends StatelessWidget { + const NativeSafeArea({ + required this.child, + this.left = true, + this.top = true, + this.right = true, + this.bottom = true, + this.minimum = EdgeInsets.zero, + this.maintainBottomViewPadding = false, + super.key, + }); + + /// Whether to avoid system intrusions on the left. + final bool left; + + /// Whether to avoid system intrusions at the top of the screen, typically the + /// system status bar. + final bool top; + + /// Whether to avoid system intrusions on the right. + final bool right; + + /// Whether to avoid system intrusions on the bottom side of the screen. + final bool bottom; + + /// This minimum padding to apply. + /// + /// The greater of the minimum insets and the media padding will be applied. + final EdgeInsets minimum; + + /// Specifies whether the [SafeArea] should maintain the bottom + /// [MediaQueryData.viewPadding] instead of the bottom + /// [MediaQueryData.padding], defaults to false. + /// + /// For example, if there is an onscreen keyboard displayed above the + /// SafeArea, the padding can be maintained below the obstruction rather than + /// being consumed. This can be helpful in cases where your layout contains + /// flexible widgets, which could visibly move when opening a software + /// keyboard due to the change in the padding value. Setting this to true will + /// avoid the UI shift. + final bool maintainBottomViewPadding; + + /// The widget below this widget in the tree. + /// + /// The padding on the [MediaQuery] for the [child] will be suitably adjusted + /// to zero out any sides that were avoided by this widget. + /// + /// {@macro flutter.widgets.ProxyWidget.child} + final Widget child; + + @override + Widget build(BuildContext context) { + final nativeOrientation = + NativeDeviceOrientationReader.orientation(context); + + late final bool realLeft; + late final bool realRight; + late final bool realTop; + late final bool realBottom; + + switch (nativeOrientation) { + case NativeDeviceOrientation.unknown: + case NativeDeviceOrientation.portraitUp: + realLeft = left; + realRight = right; + realTop = top; + realBottom = bottom; + case NativeDeviceOrientation.portraitDown: + realLeft = right; + realRight = left; + realTop = bottom; + realBottom = top; + case NativeDeviceOrientation.landscapeRight: + realLeft = bottom; + realRight = top; + realTop = left; + realBottom = right; + case NativeDeviceOrientation.landscapeLeft: + realLeft = top; + realRight = bottom; + realTop = right; + realBottom = left; + } + + return SafeArea( + left: realLeft, + right: realRight, + top: realTop, + bottom: realBottom, + minimum: minimum, + maintainBottomViewPadding: maintainBottomViewPadding, + child: child); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('left', left)) + ..add(DiagnosticsProperty('top', top)) + ..add(DiagnosticsProperty('right', right)) + ..add(DiagnosticsProperty('bottom', bottom)) + ..add(DiagnosticsProperty('minimum', minimum)) + ..add(DiagnosticsProperty( + 'maintainBottomViewPadding', maintainBottomViewPadding)); + } +} diff --git a/lib/tools/window_control.dart b/lib/tools/window_control.dart index 37816f3..2e9a21f 100644 --- a/lib/tools/window_control.dart +++ b/lib/tools/window_control.dart @@ -1,10 +1,13 @@ import 'dart:async'; +import 'package:async_tools/async_tools.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:window_manager/window_manager.dart'; import '../theme/views/responsive.dart'; +import 'tools.dart'; export 'package:window_manager/window_manager.dart' show TitleBarStyle; @@ -27,7 +30,7 @@ Future initializeWindowControl() async { skipTaskbar: false, ); await windowManager.waitUntilReadyToShow(windowOptions, () async { - await changeWindowSetup( + await _asyncChangeWindowSetup( TitleBarStyle.hidden, OrientationCapability.normal); await windowManager.show(); await windowManager.focus(); @@ -35,7 +38,9 @@ Future initializeWindowControl() async { } } -Future changeWindowSetup(TitleBarStyle titleBarStyle, +const kWindowSetup = '__windowSetup'; + +Future _asyncChangeWindowSetup(TitleBarStyle titleBarStyle, OrientationCapability orientationCapability) async { if (isDesktop) { await windowManager.setTitleBarStyle(titleBarStyle); @@ -59,3 +64,47 @@ Future changeWindowSetup(TitleBarStyle titleBarStyle, } } } + +void changeWindowSetup( + TitleBarStyle titleBarStyle, OrientationCapability orientationCapability) { + singleFuture( + kWindowSetup, + () async => + _asyncChangeWindowSetup(titleBarStyle, orientationCapability)); +} + +abstract class WindowSetupState extends State { + WindowSetupState( + {required this.titleBarStyle, required this.orientationCapability}); + + @override + void initState() { + changeWindowSetup(this.titleBarStyle, this.orientationCapability); + super.initState(); + } + + @override + void activate() { + changeWindowSetup(this.titleBarStyle, this.orientationCapability); + super.activate(); + } + + @override + void deactivate() { + changeWindowSetup(TitleBarStyle.normal, OrientationCapability.normal); + super.deactivate(); + } + + //////////////////////////////////////////////////////////////////////////// + final TitleBarStyle titleBarStyle; + final OrientationCapability orientationCapability; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(EnumProperty('titleBarStyle', titleBarStyle)) + ..add(EnumProperty( + 'orientationCapability', orientationCapability)); + } +} diff --git a/lib/veilid_processor/views/developer.dart b/lib/veilid_processor/views/developer.dart index ed495ae..bb0b256 100644 --- a/lib/veilid_processor/views/developer.dart +++ b/lib/veilid_processor/views/developer.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:ansicolor/ansicolor.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:cool_dropdown/cool_dropdown.dart'; @@ -45,11 +47,6 @@ class _DeveloperPageState extends State { void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) async { - await changeWindowSetup( - TitleBarStyle.normal, OrientationCapability.normal); - }); - _terminalController.addListener(() { setState(() {}); }); @@ -273,61 +270,59 @@ class _DeveloperPageState extends State { body: GestureDetector( onTap: () => FocusScope.of(context).unfocus(), child: SafeArea( - bottom: false, child: Column(children: [ - Stack(alignment: AlignmentDirectional.center, children: [ - Image.asset('assets/images/ellet.png'), - TerminalView(globalDebugTerminal, - textStyle: kDefaultTerminalStyle, - controller: _terminalController, - keyboardType: TextInputType.none, - //autofocus: true, - backgroundOpacity: _showEllet ? 0.75 : 1.0, - onSecondaryTapDown: (details, offset) async { - await copySelection(context); - }) - ]).expanded(), - TextField( - controller: _debugCommandController, - onTapOutside: (event) { - FocusManager.instance.primaryFocus?.unfocus(); - }, - decoration: InputDecoration( - filled: true, - contentPadding: const EdgeInsets.fromLTRB(8, 2, 8, 2), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular( - 8 * scaleConfig.borderRadiusScale), - borderSide: BorderSide.none), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular( - 8 * scaleConfig.borderRadiusScale), - ), - fillColor: scale.primaryScale.subtleBackground, - hintText: translate('developer.command'), - suffixIcon: IconButton( - icon: Icon(Icons.send, - color: _debugCommandController.text.isEmpty - ? scale.primaryScale.primary.withAlpha(0x3F) - : scale.primaryScale.primary), - onPressed: _debugCommandController.text.isEmpty - ? null - : () async { - final debugCommand = - _debugCommandController.text; - _debugCommandController.clear(); - await _sendDebugCommand(debugCommand); - }, - )), - onChanged: (_) { - setState(() => {}); - }, - onSubmitted: (debugCommand) async { - _debugCommandController.clear(); - await _sendDebugCommand(debugCommand); - }, - ).paddingAll(4) - ])))); + Stack(alignment: AlignmentDirectional.center, children: [ + Image.asset('assets/images/ellet.png'), + TerminalView(globalDebugTerminal, + textStyle: kDefaultTerminalStyle, + controller: _terminalController, + keyboardType: TextInputType.none, + //autofocus: true, + backgroundOpacity: _showEllet ? 0.75 : 1.0, + onSecondaryTapDown: (details, offset) async { + await copySelection(context); + }) + ]).expanded(), + TextField( + controller: _debugCommandController, + onTapOutside: (event) { + FocusManager.instance.primaryFocus?.unfocus(); + }, + decoration: InputDecoration( + filled: true, + contentPadding: const EdgeInsets.fromLTRB(8, 2, 8, 2), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular( + 8 * scaleConfig.borderRadiusScale), + borderSide: BorderSide.none), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular( + 8 * scaleConfig.borderRadiusScale), + ), + fillColor: scale.primaryScale.subtleBackground, + hintText: translate('developer.command'), + suffixIcon: IconButton( + icon: Icon(Icons.send, + color: _debugCommandController.text.isEmpty + ? scale.primaryScale.primary.withAlpha(0x3F) + : scale.primaryScale.primary), + onPressed: _debugCommandController.text.isEmpty + ? null + : () async { + final debugCommand = _debugCommandController.text; + _debugCommandController.clear(); + await _sendDebugCommand(debugCommand); + }, + )), + onChanged: (_) { + setState(() => {}); + }, + onSubmitted: (debugCommand) async { + _debugCommandController.clear(); + await _sendDebugCommand(debugCommand); + }, + ).paddingAll(4) + ])))); } @override diff --git a/pubspec.lock b/pubspec.lock index 8ab1f26..6bde90f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "67.0.0" + accordion: + dependency: "direct main" + description: + name: accordion + sha256: "0eca3d1c619c6df63d6e384010fd2ef1164e7385d7102f88a6b56f658f160cd0" + url: "https://pub.dev" + source: hosted + version: "2.6.0" analyzer: dependency: transitive description: @@ -449,6 +457,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.5" + expansion_tile_group: + dependency: "direct main" + description: + name: expansion_tile_group + sha256: "6918433891481c7d98cbc604d7b4c93509986e8134d52940853301ad6fbff404" + url: "https://pub.dev" + source: hosted + version: "1.2.4" fast_immutable_collections: dependency: "direct main" description: @@ -620,6 +636,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.2.1" + flutter_sticky_header: + dependency: "direct main" + description: + name: flutter_sticky_header + sha256: "017f398fbb45a589e01491861ca20eb6570a763fd9f3888165a978e11248c709" + url: "https://pub.dev" + source: hosted + version: "0.6.5" flutter_svg: dependency: "direct main" description: @@ -681,6 +705,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + get: + dependency: transitive + description: + name: get + sha256: e4e7335ede17452b391ed3b2ede016545706c01a02292a6c97619705e7d2a85e + url: "https://pub.dev" + source: hosted + version: "4.6.6" glob: dependency: transitive description: @@ -897,6 +929,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.10.0" + native_device_orientation: + dependency: "direct main" + description: + name: native_device_orientation + sha256: "0c330c068575e4be72cce5968ca479a3f8d5d1e5dfce7d89d5c13a1e943b338c" + url: "https://pub.dev" + source: hosted + version: "2.0.3" nested: dependency: transitive description: @@ -1326,6 +1366,30 @@ packages: description: flutter source: sdk version: "0.0.99" + sliver_expandable: + dependency: "direct main" + description: + name: sliver_expandable + sha256: ae20eb848bd0ba9dd704732ad654438ac5a5bea2b023fa3cf80a086166d96d97 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + sliver_fill_remaining_box_adapter: + dependency: "direct main" + description: + name: sliver_fill_remaining_box_adapter + sha256: "2a222c0f09eb07c37857ce2526c0fbf3b17b2bd1b1ff0e890085f2f7a9ba1927" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + sliver_tools: + dependency: "direct main" + description: + name: sliver_tools + sha256: eae28220badfb9d0559207badcbbc9ad5331aac829a88cb0964d330d2a4636a6 + url: "https://pub.dev" + source: hosted + version: "0.2.12" smart_auth: dependency: transitive description: @@ -1583,6 +1647,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.4.0" + value_layout_builder: + dependency: transitive + description: + name: value_layout_builder + sha256: "98202ec1807e94ac72725b7f0d15027afde513c55c69ff3f41bcfccb950831bc" + url: "https://pub.dev" + source: hosted + version: "0.3.1" vector_graphics: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 801d5ae..716c8b8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,6 +8,7 @@ environment: flutter: '>=3.22.1' dependencies: + accordion: ^2.6.0 animated_bottom_navigation_bar: ^1.3.3 animated_switcher_transitions: ^1.0.0 animated_theme_switcher: ^2.0.10 @@ -27,6 +28,7 @@ dependencies: cool_dropdown: ^2.1.0 cupertino_icons: ^1.0.8 equatable: ^2.0.5 + expansion_tile_group: ^1.2.4 fast_immutable_collections: ^10.2.4 file_saver: ^0.2.13 fixnum: ^1.1.0 @@ -46,6 +48,7 @@ dependencies: flutter_native_splash: ^2.4.0 flutter_slidable: ^3.1.0 flutter_spinkit: ^5.2.1 + flutter_sticky_header: ^0.6.5 flutter_svg: ^2.0.10+1 flutter_translate: ^4.1.0 flutter_zoom_drawer: ^3.2.0 @@ -60,6 +63,7 @@ dependencies: meta: ^1.12.0 mobile_scanner: ^5.1.1 motion_toast: ^2.10.0 + native_device_orientation: ^2.0.3 package_info_plus: ^8.0.0 pasteboard: ^0.2.0 path: ^1.9.0 @@ -81,6 +85,9 @@ dependencies: share_plus: ^9.0.0 shared_preferences: ^2.2.3 signal_strength_indicator: ^0.4.1 + sliver_expandable: ^1.1.1 + sliver_fill_remaining_box_adapter: ^1.0.0 + sliver_tools: ^0.2.12 sorted_list: git: url: https://gitlab.com/veilid/dart-sorted-list-improved.git From 519571628f051955e86fd97d1952878d805e1b34 Mon Sep 17 00:00:00 2001 From: TC Johnson Date: Thu, 11 Jul 2024 22:20:37 -0500 Subject: [PATCH 162/270] =?UTF-8?q?Version=20update:=20v0.2.1=20=E2=86=92?= =?UTF-8?q?=20v0.3.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.md diff --git a/.bumpversion.cfg b/.bumpversion.cfg index ca34235..ba3e47f 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.2.1+0 +current_version = 0.3.0+0 commit = False tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)\+(?P\d+) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9502be2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## v0.3.0 ## +- Beginning of changelog +- See commits/merges for history prior to v0.3.0 diff --git a/pubspec.yaml b/pubspec.yaml index 716c8b8..0187a89 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: veilidchat description: VeilidChat publish_to: 'none' -version: 0.2.1+11 +version: 0.3.0+12 environment: sdk: '>=3.2.0 <4.0.0' From ba191d3903c73a78a09f5983a73a6fd26f0ae1c3 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 17 Jul 2024 16:53:47 -0400 Subject: [PATCH 163/270] init failure retry work --- .../cubits/per_account_collection_cubit.dart | 4 +- lib/chat/cubits/chat_component_cubit.dart | 4 +- .../cubits/single_contact_messages_cubit.dart | 4 +- lib/contacts/cubits/contact_list_cubit.dart | 2 +- .../cubits/conversation_cubit.dart | 9 +- lib/tools/exceptions.dart | 1 + lib/tools/tools.dart | 1 + lib/veilid_processor/views/developer.dart | 159 ++++++++++----- .../history_text_editing_controller.dart | 69 +++++++ packages/veilid_support/example/pubspec.lock | 10 +- packages/veilid_support/example/pubspec.yaml | 2 +- .../src/dht_log/dht_log_cubit.dart | 37 +++- .../dht_support/src/dht_log/dht_log_read.dart | 5 +- .../src/dht_log/dht_log_spine.dart | 4 +- .../src/dht_log/dht_log_write.dart | 16 +- .../src/dht_record/dht_record_cubit.dart | 28 ++- .../src/dht_record/dht_record_pool.dart | 188 +++++++++++------- .../dht_short_array_cubit.dart | 35 +++- .../dht_short_array/dht_short_array_head.dart | 4 +- .../dht_short_array/dht_short_array_read.dart | 5 +- .../dht_short_array_write.dart | 10 +- .../src/interfaces/exceptions.dart | 9 +- .../lib/src/async_table_db_backed_cubit.dart | 4 +- .../lib/src/persistent_queue.dart | 4 +- packages/veilid_support/lib/src/table_db.dart | 4 +- .../lib/src/table_db_array.dart | 8 +- .../src/table_db_array_protobuf_cubit.dart | 4 +- packages/veilid_support/pubspec.lock | 10 +- packages/veilid_support/pubspec.yaml | 4 +- pubspec.lock | 8 +- pubspec.yaml | 4 +- 31 files changed, 438 insertions(+), 218 deletions(-) create mode 100644 lib/tools/exceptions.dart create mode 100644 lib/veilid_processor/views/history_text_editing_controller.dart diff --git a/lib/account_manager/cubits/per_account_collection_cubit.dart b/lib/account_manager/cubits/per_account_collection_cubit.dart index 6cb8d5d..5f208fb 100644 --- a/lib/account_manager/cubits/per_account_collection_cubit.dart +++ b/lib/account_manager/cubits/per_account_collection_cubit.dart @@ -44,7 +44,7 @@ class PerAccountCollectionCubit extends Cubit { await super.close(); } - Future _init() async { + Future _init(Completer _cancel) async { // subscribe to accountInfo changes _processor.follow(accountInfoCubit.stream, accountInfoCubit.state, _followAccountInfoState); @@ -235,7 +235,7 @@ class PerAccountCollectionCubit extends Cubit { final Locator _locator; final _processor = SingleStateProcessor(); - final _initWait = WaitSet(); + final _initWait = WaitSet(); // Per-account cubits regardless of login state final AccountInfoCubit accountInfoCubit; diff --git a/lib/chat/cubits/chat_component_cubit.dart b/lib/chat/cubits/chat_component_cubit.dart index 12ed135..2f2a2d0 100644 --- a/lib/chat/cubits/chat_component_cubit.dart +++ b/lib/chat/cubits/chat_component_cubit.dart @@ -68,7 +68,7 @@ class ChatComponentCubit extends Cubit { messagesCubit: messagesCubit, ); - Future _init() async { + Future _init(Completer _cancel) async { // Get local user info and account record cubit _localUserIdentityKey = _accountInfo.identityTypedPublicKey; @@ -420,7 +420,7 @@ class ChatComponentCubit extends Cubit { //////////////////////////////////////////////////////////////////////////// - final _initWait = WaitSet(); + final _initWait = WaitSet(); final AccountInfo _accountInfo; final AccountRecordCubit _accountRecordCubit; final ContactListCubit _contactListCubit; diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index ce4368a..97e474f 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -95,7 +95,7 @@ class SingleContactMessagesCubit extends Cubit { } // Initialize everything - Future _init() async { + Future _init(Completer _cancel) async { _unsentMessagesQueue = PersistentQueue( table: 'SingleContactUnsentMessages', key: _remoteConversationRecordKey.toString(), @@ -445,7 +445,7 @@ class SingleContactMessagesCubit extends Cubit { ///////////////////////////////////////////////////////////////////////// - final WaitSet _initWait = WaitSet(); + final WaitSet _initWait = WaitSet(); late final AccountInfo _accountInfo; final TypedKey _remoteIdentityPublicKey; final TypedKey _localConversationRecordKey; diff --git a/lib/contacts/cubits/contact_list_cubit.dart b/lib/contacts/cubits/contact_list_cubit.dart index aaecca4..76079b3 100644 --- a/lib/contacts/cubits/contact_list_cubit.dart +++ b/lib/contacts/cubits/contact_list_cubit.dart @@ -71,7 +71,7 @@ class ContactListCubit extends DHTShortArrayCubit { final updated = await writer.tryWriteItemProtobuf( proto.Contact.fromBuffer, pos, newContact); if (!updated) { - throw DHTExceptionTryAgain(); + throw DHTExceptionOutdated(); } break; } diff --git a/lib/conversation/cubits/conversation_cubit.dart b/lib/conversation/cubits/conversation_cubit.dart index 1947504..11ba12f 100644 --- a/lib/conversation/cubits/conversation_cubit.dart +++ b/lib/conversation/cubits/conversation_cubit.dart @@ -46,12 +46,13 @@ class ConversationCubit extends Cubit> { _identityWriter = _accountInfo.identityWriter; if (_localConversationRecordKey != null) { - _initWait.add(() async { + _initWait.add((_) async { await _setLocalConversation(() async { // Open local record key if it is specified final pool = DHTRecordPool.instance; final crypto = await _cachedConversationCrypto(); final writer = _identityWriter; + final record = await pool.openRecordWrite( _localConversationRecordKey!, writer, debugName: 'ConversationCubit::LocalConversation', @@ -64,16 +65,18 @@ class ConversationCubit extends Cubit> { } if (_remoteConversationRecordKey != null) { - _initWait.add(() async { + _initWait.add((cancel) async { await _setRemoteConversation(() async { // Open remote record key if it is specified final pool = DHTRecordPool.instance; final crypto = await _cachedConversationCrypto(); + final record = await pool.openRecordRead(_remoteConversationRecordKey, debugName: 'ConversationCubit::RemoteConversation', parent: pool.getParentRecordKey(_remoteConversationRecordKey) ?? accountInfo.accountRecordKey, crypto: crypto); + return record; }); }); @@ -326,5 +329,5 @@ class ConversationCubit extends Cubit> { ConversationState _incrementalState = const ConversationState( localConversation: null, remoteConversation: null); VeilidCrypto? _conversationCrypto; - final WaitSet _initWait = WaitSet(); + final WaitSet _initWait = WaitSet(); } diff --git a/lib/tools/exceptions.dart b/lib/tools/exceptions.dart new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lib/tools/exceptions.dart @@ -0,0 +1 @@ + diff --git a/lib/tools/tools.dart b/lib/tools/tools.dart index 2963df0..470b648 100644 --- a/lib/tools/tools.dart +++ b/lib/tools/tools.dart @@ -1,4 +1,5 @@ export 'animations.dart'; +export 'exceptions.dart'; export 'loggy.dart'; export 'misc.dart'; export 'package_info.dart'; diff --git a/lib/veilid_processor/views/developer.dart b/lib/veilid_processor/views/developer.dart index bb0b256..0078935 100644 --- a/lib/veilid_processor/views/developer.dart +++ b/lib/veilid_processor/views/developer.dart @@ -18,6 +18,7 @@ import 'package:xterm/xterm.dart'; import '../../layout/layout.dart'; import '../../theme/theme.dart'; import '../../tools/tools.dart'; +import 'history_text_editing_controller.dart'; final globalDebugTerminal = Terminal( maxLines: 50000, @@ -36,17 +37,12 @@ class DeveloperPage extends StatefulWidget { } class _DeveloperPageState extends State { - final _terminalController = TerminalController(); - final _debugCommandController = TextEditingController(); - final _logLevelController = DropdownController(duration: 250.ms); - final List> _logLevelDropdownItems = []; - var _logLevelDropDown = log.level.logLevel; - var _showEllet = false; - @override void initState() { super.initState(); + _historyController = HistoryTextEditingController(setState: setState); + _terminalController.addListener(() { setState(() {}); }); @@ -66,42 +62,71 @@ class _DeveloperPageState extends State { globalDebugTerminal.write(colorOut.replaceAll('\n', '\r\n')); } - Future _sendDebugCommand(String debugCommand) async { - if (debugCommand == 'pool allocations') { - DHTRecordPool.instance.debugPrintAllocations(); - return; - } - - if (debugCommand == 'pool opened') { - DHTRecordPool.instance.debugPrintOpened(); - return; - } - - if (debugCommand.startsWith('change_log_ignore ')) { - final args = debugCommand.split(' '); - if (args.length < 3) { - _debugOut('Incorrect number of arguments'); - return; - } - final layer = args[1]; - final changes = args[2].split(','); - Veilid.instance.changeLogIgnore(layer, changes); - return; - } - - if (debugCommand == 'ellet') { - setState(() { - _showEllet = !_showEllet; - }); - return; - } - - _debugOut('DEBUG >>>\n$debugCommand\n'); + Future _sendDebugCommand(String debugCommand) async { try { - final out = await Veilid.instance.debug(debugCommand); - _debugOut('<<< DEBUG\n$out\n'); - } on Exception catch (e, st) { - _debugOut('<<< ERROR\n$e\n<<< STACK\n$st'); + setState(() { + _busy = true; + }); + + if (debugCommand == 'pool allocations') { + try { + DHTRecordPool.instance.debugPrintAllocations(); + } on Exception catch (e, st) { + _debugOut('<<< ERROR\n$e\n<<< STACK\n$st'); + return false; + } + return true; + } + + if (debugCommand == 'pool opened') { + try { + DHTRecordPool.instance.debugPrintOpened(); + } on Exception catch (e, st) { + _debugOut('<<< ERROR\n$e\n<<< STACK\n$st'); + return false; + } + return true; + } + + if (debugCommand.startsWith('change_log_ignore ')) { + final args = debugCommand.split(' '); + if (args.length < 3) { + _debugOut('Incorrect number of arguments'); + return false; + } + final layer = args[1]; + final changes = args[2].split(','); + try { + Veilid.instance.changeLogIgnore(layer, changes); + } on Exception catch (e, st) { + _debugOut('<<< ERROR\n$e\n<<< STACK\n$st'); + return false; + } + + return true; + } + + if (debugCommand == 'ellet') { + setState(() { + _showEllet = !_showEllet; + }); + return true; + } + + _debugOut('DEBUG >>>\n$debugCommand\n'); + try { + final out = await Veilid.instance.debug(debugCommand); + _debugOut('<<< DEBUG\n$out\n'); + } on Exception catch (e, st) { + _debugOut('<<< ERROR\n$e\n<<< STACK\n$st'); + return false; + } + + return true; + } finally { + setState(() { + _busy = false; + }); } } @@ -284,7 +309,9 @@ class _DeveloperPageState extends State { }) ]).expanded(), TextField( - controller: _debugCommandController, + enabled: !_busy, + controller: _historyController.controller, + focusNode: _historyController.focusNode, onTapOutside: (event) { FocusManager.instance.primaryFocus?.unfocus(); }, @@ -303,28 +330,54 @@ class _DeveloperPageState extends State { hintText: translate('developer.command'), suffixIcon: IconButton( icon: Icon(Icons.send, - color: _debugCommandController.text.isEmpty + color: _historyController.controller.text.isEmpty ? scale.primaryScale.primary.withAlpha(0x3F) : scale.primaryScale.primary), - onPressed: _debugCommandController.text.isEmpty - ? null - : () async { - final debugCommand = _debugCommandController.text; - _debugCommandController.clear(); - await _sendDebugCommand(debugCommand); - }, + onPressed: + (_historyController.controller.text.isEmpty || _busy) + ? null + : () async { + final debugCommand = + _historyController.controller.text; + _historyController.controller.clear(); + await _sendDebugCommand(debugCommand); + }, )), onChanged: (_) { setState(() => {}); }, + onEditingComplete: () { + // part of the default action if onEditingComplete is null + _historyController.controller.clearComposing(); + // don't give up focus though + }, onSubmitted: (debugCommand) async { - _debugCommandController.clear(); - await _sendDebugCommand(debugCommand); + if (debugCommand.isEmpty) { + return; + } + + final ok = await _sendDebugCommand(debugCommand); + if (ok) { + setState(() { + _historyController.submit(debugCommand); + }); + } }, ).paddingAll(4) ])))); } + //////////////////////////////////////////////////////////////////////////// + + final _terminalController = TerminalController(); + late final HistoryTextEditingController _historyController; + + final _logLevelController = DropdownController(duration: 250.ms); + final List> _logLevelDropdownItems = []; + var _logLevelDropDown = log.level.logLevel; + var _showEllet = false; + var _busy = false; + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); diff --git a/lib/veilid_processor/views/history_text_editing_controller.dart b/lib/veilid_processor/views/history_text_editing_controller.dart new file mode 100644 index 0000000..f9646e5 --- /dev/null +++ b/lib/veilid_processor/views/history_text_editing_controller.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// TextField History Controller +class HistoryTextEditingController { + HistoryTextEditingController( + {required this.setState, TextEditingController? controller}) + : _controller = controller ?? TextEditingController() { + _historyFocusNode = FocusNode(onKeyEvent: (_node, event) { + if (event.runtimeType == KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.arrowUp) { + if (_historyPosition > 0) { + if (_historyPosition == _history.length) { + _historyCurrentEdit = _controller.text; + } + _historyPosition -= 1; + setState(() { + _controller.text = _history[_historyPosition]; + }); + } + return KeyEventResult.handled; + } else if (event.runtimeType == KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.arrowDown) { + if (_historyPosition < _history.length) { + _historyPosition += 1; + setState(() { + if (_historyPosition == _history.length) { + _controller.text = _historyCurrentEdit; + } else { + _controller.text = _history[_historyPosition]; + } + }); + } + return KeyEventResult.handled; + } else if (event.runtimeType == KeyDownEvent) { + _historyPosition = _history.length; + _historyCurrentEdit = _controller.text; + } + return KeyEventResult.ignored; + }); + } + + void submit(String v) { + // add to history + if (_history.isEmpty || _history.last != v) { + _history.add(v); + if (_history.length > 100) { + _history.removeAt(0); + } + } + _historyPosition = _history.length; + setState(() { + _controller.text = ''; + }); + } + + FocusNode get focusNode => _historyFocusNode; + TextEditingController get controller => _controller; + + //////////////////////////////////////////////////////////////////////////// + + late void Function(void Function()) setState; + final TextEditingController _controller; + late final FocusNode _historyFocusNode; + + final List _history = []; + int _historyPosition = 0; + String _historyCurrentEdit = ''; +} diff --git a/packages/veilid_support/example/pubspec.lock b/packages/veilid_support/example/pubspec.lock index 304670e..2c2b1ad 100644 --- a/packages/veilid_support/example/pubspec.lock +++ b/packages/veilid_support/example/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: "direct dev" description: name: async_tools - sha256: f35f5590711f1422c7eff990351e879c5433486f9b5be5df30818521bf6ab8d6 + sha256: "375da1e5b51974a9e84469b7630f36d1361fb53d3fcc818a5a0121ab51fe0343" url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "0.1.4" bloc: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: bloc_advanced_tools - sha256: "11f534f9e89561302de3bd07ab2dfe1c2dacaa8db9794ccdb57c55cfc02dffc6" + sha256: "0599d860eb096c5b12457c60bdf7f66bfcb7171bc94ccf2c7c752b6a716a79f9" url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "0.1.4" boolean_selector: dependency: transitive description: @@ -650,7 +650,7 @@ packages: path: "../../../../veilid/veilid-flutter" relative: true source: path - version: "0.3.2" + version: "0.3.3" veilid_support: dependency: "direct main" description: diff --git a/packages/veilid_support/example/pubspec.yaml b/packages/veilid_support/example/pubspec.yaml index 8f76235..a885f94 100644 --- a/packages/veilid_support/example/pubspec.yaml +++ b/packages/veilid_support/example/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: path: ../ dev_dependencies: - async_tools: ^0.1.3 + async_tools: ^0.1.4 integration_test: sdk: flutter lint_hard: ^4.0.0 diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart index 570474f..8fd565b 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart @@ -43,13 +43,26 @@ class DHTLogCubit extends Cubit> required T Function(List data) decodeElement, }) : _decodeElement = decodeElement, super(const BlocBusyState(AsyncValue.loading())) { - _initWait.add(() async { - // Open DHT record - _log = await open(); - _wantsCloseRecord = true; - + _initWait.add((cancel) async { + try { + // Do record open/create + while (!cancel.isCompleted) { + try { + // Open DHT record + _log = await open(); + _wantsCloseRecord = true; + break; + } on VeilidAPIExceptionTryAgain { + // Wait for a bit + await asyncSleep(); + } + } + } on Exception catch (e, st) { + emit(DHTLogBusyState(AsyncValue.error(e, st))); + return; + } // Make initial state update - await _refreshNoWait(); + _initialUpdate(); _subscription = await _log.listen(_update); }); } @@ -156,7 +169,7 @@ class DHTLogCubit extends Cubit> // Run at most one background update process // 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. + // or during it, so we dont have to wait for that here. // Accumulate head and tail deltas _headDelta += upd.headDelta; @@ -188,9 +201,15 @@ class DHTLogCubit extends Cubit> }); } + void _initialUpdate() { + _sspUpdate.busyUpdate>(busy, (emit) async { + await _refreshInner(emit); + }); + } + @override Future close() async { - await _initWait(); + await _initWait(cancelValue: true); await _subscription?.cancel(); _subscription = null; if (_wantsCloseRecord) { @@ -217,7 +236,7 @@ class DHTLogCubit extends Cubit> return _log.operateAppendEventual(closure, timeout: timeout); } - final WaitSet _initWait = WaitSet(); + final WaitSet _initWait = WaitSet(); late final DHTLog _log; final T Function(List data) _decodeElement; StreamSubscription? _subscription; diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart index d7b3541..90b8428 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart @@ -45,8 +45,9 @@ class _DHTLogRead implements DHTLogReadOperations { final out = []; (start, length) = _clampStartLen(start, length); - final chunks = Iterable.generate(length).slices(maxDHTConcurrency).map( - (chunk) => + final chunks = Iterable.generate(length) + .slices(kMaxDHTConcurrency) + .map((chunk) => chunk.map((pos) => get(pos + start, forceRefresh: forceRefresh))); for (final chunk in chunks) { diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index ca0074f..070c494 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -147,7 +147,7 @@ class _DHTLogSpine { if (!await writeSpineHead(old: (oldHead, oldTail))) { // Failed to write head means head got overwritten so write should // be considered failed - throw DHTExceptionTryAgain(); + throw DHTExceptionOutdated(); } return out; } on Exception { @@ -187,7 +187,7 @@ class _DHTLogSpine { try { out = await closure(this); break; - } on DHTExceptionTryAgain { + } on DHTExceptionOutdated { // Failed to write in closure resets state _head = oldHead; _tail = oldTail; diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart index ca47e00..7f4d9ce 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart @@ -26,10 +26,10 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { final success = await write.tryWriteItem(lookup.pos, newValue, output: output); if (!success) { - throw DHTExceptionTryAgain(); + throw DHTExceptionOutdated(); } })); - } on DHTExceptionTryAgain { + } on DHTExceptionOutdated { return false; } return true; @@ -71,14 +71,14 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { final success = await bWrite .tryWriteItem(bLookup.pos, aItem, output: bItem); if (!success) { - throw DHTExceptionTryAgain(); + throw DHTExceptionOutdated(); } }); } final success = await aWrite.tryWriteItem(aLookup.pos, bItem.value!); if (!success) { - throw DHTExceptionTryAgain(); + throw DHTExceptionOutdated(); } }))); } @@ -114,7 +114,7 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { _spine.allocateTail(values.length); // Look up the first position and shortarray - final dws = DelayedWaitSet(); + final dws = DelayedWaitSet(); var success = true; for (var valueIdx = 0; valueIdx < values.length;) { @@ -128,7 +128,7 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { final sacount = min(remaining, DHTShortArray.maxElements - lookup.pos); final sublistValues = values.sublist(valueIdx, valueIdx + sacount); - dws.add(() async { + dws.add((_) async { try { await lookup.scope((sa) async => sa.operateWrite((write) async { // If this a new segment, then clear it in @@ -141,7 +141,7 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { } return write.addAll(sublistValues); })); - } on DHTExceptionTryAgain { + } on DHTExceptionOutdated { success = false; } }); @@ -152,7 +152,7 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { await dws(); if (!success) { - throw DHTExceptionTryAgain(); + throw DHTExceptionOutdated(); } } 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 54d1dec..da75e06 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 @@ -12,7 +12,7 @@ typedef StateFunction = Future Function( DHTRecord, List, Uint8List?); typedef WatchFunction = Future Function(DHTRecord); -class DHTRecordCubit extends Cubit> { +abstract class DHTRecordCubit extends Cubit> { DHTRecordCubit({ required Future Function() open, required InitialStateFunction initialStateFunction, @@ -21,10 +21,24 @@ class DHTRecordCubit extends Cubit> { }) : _wantsCloseRecord = false, _stateFunction = stateFunction, super(const AsyncValue.loading()) { - initWait.add(() async { - // Do record open/create - _record = await open(); - _wantsCloseRecord = true; + initWait.add((cancel) async { + try { + // Do record open/create + while (!cancel.isCompleted) { + try { + _record = await open(); + _wantsCloseRecord = true; + break; + } on VeilidAPIExceptionKeyNotFound { + } on VeilidAPIExceptionTryAgain { + // Wait for a bit + await asyncSleep(); + } + } + } on Exception catch (e, st) { + emit(AsyncValue.error(e, st)); + return; + } await _init(initialStateFunction, stateFunction, watchFunction); }); } @@ -60,7 +74,7 @@ class DHTRecordCubit extends Cubit> { @override Future close() async { - await initWait(); + await initWait(cancelValue: true); await _record.cancelWatch(); await _subscription?.cancel(); _subscription = null; @@ -98,7 +112,7 @@ class DHTRecordCubit extends Cubit> { DHTRecord get record => _record; @protected - final WaitSet initWait = WaitSet(); + final WaitSet initWait = WaitSet(); StreamSubscription? _subscription; late DHTRecord _record; 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 b80db1f..5aa7915 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 @@ -18,8 +18,11 @@ part 'dht_record_pool.g.dart'; part 'dht_record.dart'; part 'dht_record_pool_private.dart'; -// Maximum number of concurrent DHT operations to perform on the network -const int maxDHTConcurrency = 8; +/// Maximum number of concurrent DHT operations to perform on the network +const int kMaxDHTConcurrency = 8; + +/// Number of times to retry a 'key not found' +const int kDHTKeyNotFoundRetry = 3; typedef DHTRecordPoolLogger = void Function(String message); @@ -60,6 +63,7 @@ class DHTRecordPool with TableDBBackedJson { DHTRecordPool._(Veilid veilid, VeilidRoutingContext routingContext) : _state = const DHTRecordPoolAllocations(), _mutex = Mutex(), + _recordTagLock = AsyncTagLock(), _opened = {}, _markedForDelete = {}, _routingContext = routingContext, @@ -132,24 +136,17 @@ class DHTRecordPool with TableDBBackedJson { TypedKey? parent, int defaultSubkey = 0, VeilidCrypto? crypto}) async => - _mutex.protect(() async { + _recordTagLock.protect(recordKey, closure: () async { final dhtctx = routingContext ?? _routingContext; - final openedRecordInfo = await _recordOpenInner( + final rec = await _recordOpenCommon( debugName: debugName, dhtctx: dhtctx, recordKey: recordKey, - parent: parent); - - final rec = DHTRecord._( - debugName: debugName, - routingContext: dhtctx, - defaultSubkey: defaultSubkey, - sharedDHTRecordData: openedRecordInfo.shared, + crypto: crypto ?? const VeilidCryptoPublic(), writer: null, - crypto: crypto ?? const VeilidCryptoPublic()); - - openedRecordInfo.records.add(rec); + parent: parent, + defaultSubkey: defaultSubkey); return rec; }); @@ -164,27 +161,20 @@ class DHTRecordPool with TableDBBackedJson { int defaultSubkey = 0, VeilidCrypto? crypto, }) async => - _mutex.protect(() async { + _recordTagLock.protect(recordKey, closure: () async { final dhtctx = routingContext ?? _routingContext; - final openedRecordInfo = await _recordOpenInner( - debugName: debugName, - dhtctx: dhtctx, - recordKey: recordKey, - parent: parent, - writer: writer); - - final rec = DHTRecord._( - debugName: debugName, - routingContext: dhtctx, - defaultSubkey: defaultSubkey, - writer: writer, - sharedDHTRecordData: openedRecordInfo.shared, - crypto: crypto ?? - await privateCryptoFromTypedSecret( - TypedKey(kind: recordKey.kind, value: writer.secret))); - - openedRecordInfo.records.add(rec); + final rec = await _recordOpenCommon( + debugName: debugName, + dhtctx: dhtctx, + recordKey: recordKey, + crypto: crypto ?? + await privateCryptoFromTypedSecret( + TypedKey(kind: recordKey.kind, value: writer.secret)), + writer: writer, + parent: parent, + defaultSubkey: defaultSubkey, + ); return rec; }); @@ -381,41 +371,72 @@ class DHTRecordPool with TableDBBackedJson { return openedRecordInfo; } - Future<_OpenedRecordInfo> _recordOpenInner( + Future _recordOpenCommon( {required String debugName, required VeilidRoutingContext dhtctx, required TypedKey recordKey, - KeyPair? writer, - TypedKey? parent}) async { - if (!_mutex.isLocked) { - throw StateError('should be locked here'); - } + required VeilidCrypto crypto, + required KeyPair? writer, + required TypedKey? parent, + required int defaultSubkey}) async { log('openDHTRecord: debugName=$debugName key=$recordKey'); - // If we are opening a key that already exists - // make sure we are using the same parent if one was specified - _validateParentInner(parent, recordKey); - // See if this has been opened yet - final openedRecordInfo = _opened[recordKey]; + final openedRecordInfo = await _mutex.protect(() async { + // If we are opening a key that already exists + // make sure we are using the same parent if one was specified + _validateParentInner(parent, recordKey); + + return _opened[recordKey]; + }); + if (openedRecordInfo == null) { // Fresh open, just open the record - final recordDescriptor = - await dhtctx.openDHTRecord(recordKey, writer: writer); + var retry = kDHTKeyNotFoundRetry; + late final DHTRecordDescriptor recordDescriptor; + while (true) { + try { + recordDescriptor = + await dhtctx.openDHTRecord(recordKey, writer: writer); + break; + } on VeilidAPIExceptionKeyNotFound { + await asyncSleep(); + retry--; + if (retry == 0) { + rethrow; + } + } + } + final newOpenedRecordInfo = _OpenedRecordInfo( recordDescriptor: recordDescriptor, defaultWriter: writer, defaultRoutingContext: dhtctx); - _opened[recordDescriptor.key] = newOpenedRecordInfo; - // Register the dependency - await _addDependencyInner( - parent, - recordKey, - debugName: debugName, - ); + final rec = DHTRecord._( + debugName: debugName, + routingContext: dhtctx, + defaultSubkey: defaultSubkey, + sharedDHTRecordData: newOpenedRecordInfo.shared, + writer: writer, + crypto: crypto); - return newOpenedRecordInfo; + await _mutex.protect(() async { + // Register the opened record + _opened[recordDescriptor.key] = newOpenedRecordInfo; + + // Register the dependency + await _addDependencyInner( + parent, + recordKey, + debugName: debugName, + ); + + // Register the newly opened record + newOpenedRecordInfo.records.add(rec); + }); + + return rec; } // Already opened @@ -430,37 +451,50 @@ class DHTRecordPool with TableDBBackedJson { openedRecordInfo.shared.defaultRoutingContext = dhtctx; } - // Register the dependency - await _addDependencyInner( - parent, - recordKey, - debugName: debugName, - ); + final rec = DHTRecord._( + debugName: debugName, + routingContext: dhtctx, + defaultSubkey: defaultSubkey, + sharedDHTRecordData: openedRecordInfo.shared, + writer: writer, + crypto: crypto); - return openedRecordInfo; + await _mutex.protect(() async { + // Register the dependency + await _addDependencyInner( + parent, + recordKey, + debugName: debugName, + ); + + openedRecordInfo.records.add(rec); + }); + + return rec; } // Called when a DHTRecord is closed // Cleans up the opened record housekeeping and processes any late deletions Future _recordClosed(DHTRecord record) async { - await _mutex.protect(() async { - final key = record.key; + await _recordTagLock.protect(record.key, + closure: () => _mutex.protect(() async { + final key = record.key; - log('closeDHTRecord: debugName=${record.debugName} key=$key'); + log('closeDHTRecord: debugName=${record.debugName} key=$key'); - final openedRecordInfo = _opened[key]; - if (openedRecordInfo == null || - !openedRecordInfo.records.remove(record)) { - throw StateError('record already closed'); - } - if (openedRecordInfo.records.isEmpty) { - await _watchStateProcessors.remove(key); - await _routingContext.closeDHTRecord(key); - _opened.remove(key); + final openedRecordInfo = _opened[key]; + if (openedRecordInfo == null || + !openedRecordInfo.records.remove(record)) { + throw StateError('record already closed'); + } + if (openedRecordInfo.records.isEmpty) { + await _watchStateProcessors.remove(key); + await _routingContext.closeDHTRecord(key); + _opened.remove(key); - await _checkForLateDeletesInner(key); - } - }); + await _checkForLateDeletesInner(key); + } + })); } // Check to see if this key can finally be deleted @@ -916,6 +950,8 @@ class DHTRecordPool with TableDBBackedJson { DHTRecordPoolAllocations _state; // Create/open Mutex final Mutex _mutex; + // Record key tag lock + final AsyncTagLock _recordTagLock; // Which DHT records are currently open final Map _opened; // Which DHT records are marked for deletion 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 d9e1e57..97bf1d9 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 @@ -30,13 +30,29 @@ class DHTShortArrayCubit extends Cubit> required T Function(List data) decodeElement, }) : _decodeElement = decodeElement, super(const BlocBusyState(AsyncValue.loading())) { - _initWait.add(() async { - // Open DHT record - _shortArray = await open(); - _wantsCloseRecord = true; + _initWait.add((cancel) async { + try { + // Do record open/create + while (!cancel.isCompleted) { + try { + // Open DHT record + _shortArray = await open(); + _wantsCloseRecord = true; + break; + } on VeilidAPIExceptionTryAgain { + // Wait for a bit + await asyncSleep(); + } + } + } on Exception catch (e, st) { + emit(DHTShortArrayBusyState(AsyncValue.error(e, st))); + return; + } - // Make initial state update - await _refreshNoWait(); + // Kick off initial update + _update(); + + // Subscribe to changes _subscription = await _shortArray.listen(_update); }); } @@ -82,7 +98,8 @@ class DHTShortArrayCubit extends Cubit> 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. Only called after init future has run + // still processing the last one. + // Only called after init future has run, or during it // so we dont have to wait for that here. _sspUpdate.busyUpdate>( busy, (emit) async => _refreshInner(emit)); @@ -90,7 +107,7 @@ class DHTShortArrayCubit extends Cubit> @override Future close() async { - await _initWait(); + await _initWait(cancelValue: true); await _subscription?.cancel(); _subscription = null; if (_wantsCloseRecord) { @@ -118,7 +135,7 @@ class DHTShortArrayCubit extends Cubit> return _shortArray.operateWriteEventual(closure, timeout: timeout); } - final WaitSet _initWait = WaitSet(); + final WaitSet _initWait = WaitSet(); late final DHTShortArray _shortArray; final T Function(List data) _decodeElement; StreamSubscription? _subscription; 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 45c4e71..d2aa84a 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 @@ -91,7 +91,7 @@ class _DHTShortArrayHead { if (!await _writeHead()) { // Failed to write head means head got overwritten so write should // be considered failed - throw DHTExceptionTryAgain(); + throw DHTExceptionOutdated(); } onUpdatedHead?.call(); @@ -143,7 +143,7 @@ class _DHTShortArrayHead { try { out = await closure(this); break; - } on DHTExceptionTryAgain { + } on DHTExceptionOutdated { // Failed to write in closure resets state _linkedRecords = List.of(oldLinkedRecords); _index = List.of(oldIndex); 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 index abe7198..94d58b8 100644 --- 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 @@ -54,8 +54,9 @@ class _DHTShortArrayRead implements DHTShortArrayReadOperations { final out = []; (start, length) = _clampStartLen(start, length); - final chunks = Iterable.generate(length).slices(maxDHTConcurrency).map( - (chunk) => + final chunks = Iterable.generate(length) + .slices(kMaxDHTConcurrency) + .map((chunk) => chunk.map((pos) => get(pos + start, forceRefresh: forceRefresh))); for (final chunk in chunks) { 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 index 665ea00..c52a7b2 100644 --- 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 @@ -40,7 +40,7 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead } } if (!success) { - throw DHTExceptionTryAgain(); + throw DHTExceptionOutdated(); } } @@ -66,12 +66,12 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead } // Write items in parallel - final dws = DelayedWaitSet(); + final dws = DelayedWaitSet(); for (var i = 0; i < values.length; i++) { final lookup = lookups[i]; final value = values[i]; final outSeqNum = outSeqNums[i]; - dws.add(() async { + dws.add((_) async { final outValue = await lookup.record.tryWriteBytes(value, subkey: lookup.recordSubkey, outSeqNum: outSeqNum); if (outValue != null) { @@ -80,7 +80,7 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead }); } - await dws(chunkSize: maxDHTConcurrency, onChunkDone: (_) => success); + await dws(chunkSize: kMaxDHTConcurrency, onChunkDone: (_) => success); } finally { // Update sequence numbers for (var i = 0; i < values.length; i++) { @@ -97,7 +97,7 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead } } if (!success) { - throw DHTExceptionTryAgain(); + throw DHTExceptionOutdated(); } } diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/exceptions.dart b/packages/veilid_support/lib/dht_support/src/interfaces/exceptions.dart index 529c308..bced3dd 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/exceptions.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/exceptions.dart @@ -1,5 +1,5 @@ -class DHTExceptionTryAgain implements Exception { - DHTExceptionTryAgain( +class DHTExceptionOutdated implements Exception { + DHTExceptionOutdated( [this.cause = 'operation failed due to newer dht value']); String cause; } @@ -8,3 +8,8 @@ class DHTExceptionInvalidData implements Exception { DHTExceptionInvalidData([this.cause = 'dht data structure is corrupt']); String cause; } + +class DHTExceptionCancelled implements Exception { + DHTExceptionCancelled([this.cause = 'operation was cancelled']); + String cause; +} 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 710eec4..d637ee1 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 @@ -22,7 +22,7 @@ abstract class AsyncTableDBBackedCubit extends Cubit> await super.close(); } - Future _build() async { + Future _build(_) async { try { await _mutex.protect(() async { emit(AsyncValue.data(await load())); @@ -42,6 +42,6 @@ abstract class AsyncTableDBBackedCubit extends Cubit> } } - final WaitSet _initWait = WaitSet(); + final WaitSet _initWait = WaitSet(); final Mutex _mutex = Mutex(); } diff --git a/packages/veilid_support/lib/src/persistent_queue.dart b/packages/veilid_support/lib/src/persistent_queue.dart index 598d8a7..e59a470 100644 --- a/packages/veilid_support/lib/src/persistent_queue.dart +++ b/packages/veilid_support/lib/src/persistent_queue.dart @@ -45,7 +45,7 @@ class PersistentQueue } } - Future _init() async { + Future _init(_) async { // Start the processor unawaited(Future.delayed(Duration.zero, () async { await _initWait(); @@ -202,7 +202,7 @@ class PersistentQueue final String _key; final T Function(Uint8List) _fromBuffer; final bool _deleteOnClose; - final WaitSet _initWait = WaitSet(); + final WaitSet _initWait = WaitSet(); final Mutex _queueMutex = Mutex(); IList _queue = IList.empty(); final StreamController> _syncAddController = StreamController(); diff --git a/packages/veilid_support/lib/src/table_db.dart b/packages/veilid_support/lib/src/table_db.dart index 773309b..522a837 100644 --- a/packages/veilid_support/lib/src/table_db.dart +++ b/packages/veilid_support/lib/src/table_db.dart @@ -134,7 +134,7 @@ class TableDBValue extends TableDBBackedJson { _tableKeyName = tableKeyName, _makeInitialValue = makeInitialValue, _streamController = StreamController.broadcast() { - _initWait.add(() async { + _initWait.add((_) async { await get(); }); } @@ -172,7 +172,7 @@ class TableDBValue extends TableDBBackedJson { final T? Function(Object? obj) _valueFromJson; final Object? Function(T? obj) _valueToJson; final StreamController _streamController; - final WaitSet _initWait = WaitSet(); + final WaitSet _initWait = WaitSet(); ////////////////////////////////////////////////////////////// /// AsyncTableDBBacked diff --git a/packages/veilid_support/lib/src/table_db_array.dart b/packages/veilid_support/lib/src/table_db_array.dart index ad4c586..7c3d4d4 100644 --- a/packages/veilid_support/lib/src/table_db_array.dart +++ b/packages/veilid_support/lib/src/table_db_array.dart @@ -45,7 +45,7 @@ class _TableDBArrayBase { await _initWait(); } - Future _init() async { + Future _init(_) async { // Load the array details await _mutex.protect(() async { _tableDB = await Veilid.instance.openTableDB(_table, 1); @@ -259,10 +259,10 @@ class _TableDBArrayBase { for (var pos = start; pos < end;) { var batchLen = min(batchSize, end - pos); - final dws = DelayedWaitSet(); + final dws = DelayedWaitSet(); while (batchLen > 0) { final entry = await _getIndexEntry(pos); - dws.add(() async => (await _loadEntry(entry))!); + dws.add((_) async => (await _loadEntry(entry))!); pos++; batchLen--; } @@ -613,7 +613,7 @@ class _TableDBArrayBase { var _open = true; var _initDone = false; final VeilidCrypto _crypto; - final WaitSet _initWait = WaitSet(); + final WaitSet _initWait = WaitSet(); final Mutex _mutex = Mutex(); // Change tracking diff --git a/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart b/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart index 606ded5..89408ac 100644 --- a/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart +++ b/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart @@ -46,7 +46,7 @@ class TableDBArrayProtobufCubit TableDBArrayProtobufCubit({ required Future> Function() open, }) : super(const BlocBusyState(AsyncValue.loading())) { - _initWait.add(() async { + _initWait.add((_) async { // Open table db array _array = await open(); _wantsCloseArray = true; @@ -180,7 +180,7 @@ class TableDBArrayProtobufCubit return closure(_array); } - final WaitSet _initWait = WaitSet(); + final WaitSet _initWait = WaitSet(); late final TableDBArrayProtobuf _array; StreamSubscription? _subscription; bool _wantsCloseArray = false; diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index 2f71a50..7f08359 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: "direct main" description: name: async_tools - sha256: f35f5590711f1422c7eff990351e879c5433486f9b5be5df30818521bf6ab8d6 + sha256: "375da1e5b51974a9e84469b7630f36d1361fb53d3fcc818a5a0121ab51fe0343" url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "0.1.4" bloc: dependency: "direct main" description: @@ -53,10 +53,10 @@ packages: dependency: "direct main" description: name: bloc_advanced_tools - sha256: "11f534f9e89561302de3bd07ab2dfe1c2dacaa8db9794ccdb57c55cfc02dffc6" + sha256: "0599d860eb096c5b12457c60bdf7f66bfcb7171bc94ccf2c7c752b6a716a79f9" url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "0.1.4" boolean_selector: dependency: transitive description: @@ -718,7 +718,7 @@ packages: path: "../../../veilid/veilid-flutter" relative: true source: path - version: "0.3.2" + version: "0.3.3" vm_service: dependency: transitive description: diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index 5fb9af9..90d7ad8 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -7,9 +7,9 @@ environment: sdk: '>=3.2.0 <4.0.0' dependencies: - async_tools: ^0.1.3 + async_tools: ^0.1.4 bloc: ^8.1.4 - bloc_advanced_tools: ^0.1.3 + bloc_advanced_tools: ^0.1.4 charcode: ^1.3.1 collection: ^1.18.0 equatable: ^2.0.5 diff --git a/pubspec.lock b/pubspec.lock index 6bde90f..42ac495 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -85,10 +85,10 @@ packages: dependency: "direct main" description: name: async_tools - sha256: f35f5590711f1422c7eff990351e879c5433486f9b5be5df30818521bf6ab8d6 + sha256: "375da1e5b51974a9e84469b7630f36d1361fb53d3fcc818a5a0121ab51fe0343" url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "0.1.4" awesome_extensions: dependency: "direct main" description: @@ -141,10 +141,10 @@ packages: dependency: "direct main" description: name: bloc_advanced_tools - sha256: "11f534f9e89561302de3bd07ab2dfe1c2dacaa8db9794ccdb57c55cfc02dffc6" + sha256: "0599d860eb096c5b12457c60bdf7f66bfcb7171bc94ccf2c7c752b6a716a79f9" url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "0.1.4" blurry_modal_progress_hud: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 0187a89..ca47d9d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,12 +14,12 @@ dependencies: animated_theme_switcher: ^2.0.10 ansicolor: ^2.0.2 archive: ^3.6.1 - async_tools: ^0.1.3 + async_tools: ^0.1.4 awesome_extensions: ^2.0.16 badges: ^3.1.2 basic_utils: ^5.7.0 bloc: ^8.1.4 - bloc_advanced_tools: ^0.1.3 + bloc_advanced_tools: ^0.1.4 blurry_modal_progress_hud: ^1.1.1 change_case: ^2.1.0 charcode: ^1.3.1 From 6080c2f0c6826a4169e61a3977a5a76db97e1bf6 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 24 Jul 2024 15:20:29 -0400 Subject: [PATCH 164/270] beta warning dialog --- assets/i18n/en.json | 7 +- .../reconciliation/author_input_source.dart | 11 +- lib/chat/views/chat_component_widget.dart | 264 +++++++++--------- lib/contacts/views/contact_list_widget.dart | 17 +- ...ve_single_contact_chat_bloc_map_cubit.dart | 4 +- lib/layout/home/home_account_ready.dart | 7 +- lib/layout/home/home_screen.dart | 37 +++ lib/layout/home/main_pager/contacts_page.dart | 5 +- lib/layout/splash.dart | 4 + lib/theme/views/widget_helpers.dart | 26 +- .../src/dht_log/dht_log_cubit.dart | 78 ++---- .../dht_support/src/dht_log/dht_log_read.dart | 6 +- .../src/dht_log/dht_log_spine.dart | 143 +++++----- .../src/dht_log/dht_log_write.dart | 22 +- .../src/dht_record/dht_record.dart | 33 ++- .../src/dht_record/dht_record_cubit.dart | 3 +- .../src/dht_record/dht_record_pool.dart | 21 +- .../src/dht_short_array/dht_short_array.dart | 6 +- .../dht_short_array_cubit.dart | 14 +- .../dht_short_array/dht_short_array_head.dart | 2 +- .../dht_short_array/dht_short_array_read.dart | 36 ++- .../src/interfaces/exceptions.dart | 18 +- .../src/interfaces/interfaces.dart | 1 + .../src/interfaces/refreshable_cubit.dart | 16 ++ pubspec.lock | 2 +- pubspec.yaml | 1 + 26 files changed, 445 insertions(+), 339 deletions(-) create mode 100644 packages/veilid_support/lib/dht_support/src/interfaces/refreshable_cubit.dart diff --git a/assets/i18n/en.json b/assets/i18n/en.json index e24d560..d4d2b4b 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -8,6 +8,10 @@ "accounts": "Accounts", "version": "Version" }, + "splash": { + "beta_title": "VeilidChat is BETA SOFTWARE", + "beta_text": "DO NOT USE THIS FOR ANYTHING IMPORTANT\n\nUntil 1.0 is released:\n\n• You should have no expectations of actual privacy, or guarantees of security.\n• You will likely lose accounts, contacts, and messages and need to recreate them.\n\nPlease read our BETA PARTICIPATION GUIDE located here:\n\n" + }, "pager": { "chats": "Chats", "contacts": "Contacts" @@ -99,7 +103,8 @@ }, "contacts_page": { "contacts": "Contacts", - "invitations": "Invitations" + "invitations": "Invitations", + "loading_contacts": "Loading contacts..." }, "add_contact_sheet": { "new_contact": "New Contact", diff --git a/lib/chat/cubits/reconciliation/author_input_source.dart b/lib/chat/cubits/reconciliation/author_input_source.dart index 32a750e..0bd1afb 100644 --- a/lib/chat/cubits/reconciliation/author_input_source.dart +++ b/lib/chat/cubits/reconciliation/author_input_source.dart @@ -57,16 +57,11 @@ class AuthorInputSource { // Get another input batch futher back final nextWindow = await cubit.loadElementsFromReader( reader, last + 1, (last + 1) - first); - final asErr = nextWindow.asError; - if (asErr != null) { - return AsyncValue.error(asErr.error, asErr.stackTrace); - } - final asLoading = nextWindow.asLoading; - if (asLoading != null) { + if (nextWindow == null) { return const AsyncValue.loading(); } - _currentWindow = InputWindow( - elements: nextWindow.asData!.value, first: first, last: last); + _currentWindow = + InputWindow(elements: nextWindow, first: first, last: last); return const AsyncValue.data(true); }); diff --git a/lib/chat/views/chat_component_widget.dart b/lib/chat/views/chat_component_widget.dart index 34ee220..aa87ecf 100644 --- a/lib/chat/views/chat_component_widget.dart +++ b/lib/chat/views/chat_component_widget.dart @@ -19,143 +19,66 @@ import '../chat.dart'; const onEndReachedThreshold = 0.75; class ChatComponentWidget extends StatelessWidget { - const ChatComponentWidget._({required super.key}); - - // Builder wrapper function that takes care of state management requirements - static Widget builder( - {required TypedKey localConversationRecordKey, Key? key}) => - Builder(builder: (context) { - // Get the account info - final accountInfo = context.watch().state; - - // Get the account record cubit - final accountRecordCubit = context.read(); - - // Get the contact list cubit - final contactListCubit = context.watch(); - - // Get the active conversation cubit - final activeConversationCubit = context - .select( - (x) => x.tryOperateSync(localConversationRecordKey, - closure: (cubit) => cubit)); - if (activeConversationCubit == null) { - return waitingPage(); - } - - // Get the messages cubit - final messagesCubit = context.select< - ActiveSingleContactChatBlocMapCubit, - SingleContactMessagesCubit?>( - (x) => x.tryOperateSync(localConversationRecordKey, - closure: (cubit) => cubit)); - if (messagesCubit == null) { - return waitingPage(); - } - - // Make chat component state - return BlocProvider( - key: key, - create: (context) => ChatComponentCubit.singleContact( - accountInfo: accountInfo, - accountRecordCubit: accountRecordCubit, - contactListCubit: contactListCubit, - activeConversationCubit: activeConversationCubit, - messagesCubit: messagesCubit, - ), - child: ChatComponentWidget._(key: key)); - }); + const ChatComponentWidget( + {required super.key, required TypedKey localConversationRecordKey}) + : _localConversationRecordKey = localConversationRecordKey; ///////////////////////////////////////////////////////////////////// - void _handleSendPressed( - ChatComponentCubit chatComponentCubit, types.PartialText message) { - final text = message.text; - - if (text.startsWith('/')) { - chatComponentCubit.runCommand(text); - return; - } - - chatComponentCubit.sendMessage(message); - } - - // void _handleAttachmentPressed() async { - // // - // } - - Future _handlePageForward( - ChatComponentCubit chatComponentCubit, - WindowState messageWindow, - ScrollNotification notification) async { - print( - '_handlePageForward: messagesState.length=${messageWindow.length} messagesState.windowTail=${messageWindow.windowTail} messagesState.windowCount=${messageWindow.windowCount} ScrollNotification=$notification'); - - // Go forward a page - final tail = min(messageWindow.length, - messageWindow.windowTail + (messageWindow.windowCount ~/ 4)) % - messageWindow.length; - - // Set follow - final follow = messageWindow.length == 0 || - tail == 0; // xxx incorporate scroll position - - // final scrollOffset = (notification.metrics.maxScrollExtent - - // notification.metrics.minScrollExtent) * - // (1.0 - onEndReachedThreshold); - - // chatComponentCubit.scrollOffset = scrollOffset; - - await chatComponentCubit.setWindow( - tail: tail, count: messageWindow.windowCount, follow: follow); - - // chatComponentCubit.state.scrollController.position.jumpTo( - // chatComponentCubit.state.scrollController.offset + scrollOffset); - - //chatComponentCubit.scrollOffset = 0; - } - - Future _handlePageBackward( - ChatComponentCubit chatComponentCubit, - WindowState messageWindow, - ScrollNotification notification, - ) async { - print( - '_handlePageBackward: messagesState.length=${messageWindow.length} messagesState.windowTail=${messageWindow.windowTail} messagesState.windowCount=${messageWindow.windowCount} ScrollNotification=$notification'); - - // Go back a page - final tail = max( - messageWindow.windowCount, - (messageWindow.windowTail - (messageWindow.windowCount ~/ 4)) % - messageWindow.length); - - // Set follow - final follow = messageWindow.length == 0 || - tail == 0; // xxx incorporate scroll position - - // final scrollOffset = -(notification.metrics.maxScrollExtent - - // notification.metrics.minScrollExtent) * - // (1.0 - onEndReachedThreshold); - - // chatComponentCubit.scrollOffset = scrollOffset; - - await chatComponentCubit.setWindow( - tail: tail, count: messageWindow.windowCount, follow: follow); - - // chatComponentCubit.scrollOffset = scrollOffset; - - // chatComponentCubit.state.scrollController.position.jumpTo( - // chatComponentCubit.state.scrollController.offset + scrollOffset); - - //chatComponentCubit.scrollOffset = 0; - } - @override Widget build(BuildContext context) { final theme = Theme.of(context); final scale = theme.extension()!; final scaleConfig = theme.extension()!; final textTheme = theme.textTheme; + + // Get the account info + final accountInfo = context.watch().state; + + // Get the account record cubit + final accountRecordCubit = context.read(); + + // Get the contact list cubit + final contactListCubit = context.watch(); + + // Get the active conversation cubit + final activeConversationCubit = context + .select( + (x) => x.tryOperateSync(_localConversationRecordKey, + closure: (cubit) => cubit)); + if (activeConversationCubit == null) { + return waitingPage(); + } + + // Get the messages cubit + final messagesCubit = context.select( + (x) => x.tryOperateSync(_localConversationRecordKey, + closure: (cubit) => cubit)); + if (messagesCubit == null) { + return waitingPage(); + } + + // Make chat component state + return BlocProvider( + key: key, + create: (context) => ChatComponentCubit.singleContact( + accountInfo: accountInfo, + accountRecordCubit: accountRecordCubit, + contactListCubit: contactListCubit, + activeConversationCubit: activeConversationCubit, + messagesCubit: messagesCubit, + ), + child: Builder(builder: _buildChatComponent)); + } + + ///////////////////////////////////////////////////////////////////// + + Widget _buildChatComponent(BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + final textTheme = theme.textTheme; final chatTheme = makeChatTheme(scale, scaleConfig, textTheme); final errorChatTheme = (ChatThemeEditor(chatTheme) ..inputTextColor = scale.errorScale.primary @@ -323,4 +246,89 @@ class ChatComponentWidget extends StatelessWidget { ], ); } + + void _handleSendPressed( + ChatComponentCubit chatComponentCubit, types.PartialText message) { + final text = message.text; + + if (text.startsWith('/')) { + chatComponentCubit.runCommand(text); + return; + } + + chatComponentCubit.sendMessage(message); + } + + // void _handleAttachmentPressed() async { + // // + // } + + Future _handlePageForward( + ChatComponentCubit chatComponentCubit, + WindowState messageWindow, + ScrollNotification notification) async { + print( + '_handlePageForward: messagesState.length=${messageWindow.length} messagesState.windowTail=${messageWindow.windowTail} messagesState.windowCount=${messageWindow.windowCount} ScrollNotification=$notification'); + + // Go forward a page + final tail = min(messageWindow.length, + messageWindow.windowTail + (messageWindow.windowCount ~/ 4)) % + messageWindow.length; + + // Set follow + final follow = messageWindow.length == 0 || + tail == 0; // xxx incorporate scroll position + + // final scrollOffset = (notification.metrics.maxScrollExtent - + // notification.metrics.minScrollExtent) * + // (1.0 - onEndReachedThreshold); + + // chatComponentCubit.scrollOffset = scrollOffset; + + await chatComponentCubit.setWindow( + tail: tail, count: messageWindow.windowCount, follow: follow); + + // chatComponentCubit.state.scrollController.position.jumpTo( + // chatComponentCubit.state.scrollController.offset + scrollOffset); + + //chatComponentCubit.scrollOffset = 0; + } + + Future _handlePageBackward( + ChatComponentCubit chatComponentCubit, + WindowState messageWindow, + ScrollNotification notification, + ) async { + print( + '_handlePageBackward: messagesState.length=${messageWindow.length} messagesState.windowTail=${messageWindow.windowTail} messagesState.windowCount=${messageWindow.windowCount} ScrollNotification=$notification'); + + // Go back a page + final tail = max( + messageWindow.windowCount, + (messageWindow.windowTail - (messageWindow.windowCount ~/ 4)) % + messageWindow.length); + + // Set follow + final follow = messageWindow.length == 0 || + tail == 0; // xxx incorporate scroll position + + // final scrollOffset = -(notification.metrics.maxScrollExtent - + // notification.metrics.minScrollExtent) * + // (1.0 - onEndReachedThreshold); + + // chatComponentCubit.scrollOffset = scrollOffset; + + await chatComponentCubit.setWindow( + tail: tail, count: messageWindow.windowCount, follow: follow); + + // chatComponentCubit.scrollOffset = scrollOffset; + + // chatComponentCubit.state.scrollController.position.jumpTo( + // chatComponentCubit.state.scrollController.offset + scrollOffset); + + //chatComponentCubit.scrollOffset = 0; + } + + //////////////////////////////////////////////////////////////////////////// + final TypedKey _localConversationRecordKey; } diff --git a/lib/contacts/views/contact_list_widget.dart b/lib/contacts/views/contact_list_widget.dart index f5d4e46..f818892 100644 --- a/lib/contacts/views/contact_list_widget.dart +++ b/lib/contacts/views/contact_list_widget.dart @@ -13,7 +13,7 @@ import 'empty_contact_list_widget.dart'; class ContactListWidget extends StatefulWidget { const ContactListWidget( {required this.contactList, required this.disabled, super.key}); - final IList contactList; + final IList? contactList; final bool disabled; @override @@ -46,13 +46,18 @@ class _ContactListWidgetState extends State title: translate('contacts_page.contacts'), sliver: SliverFillRemaining( child: SearchableList.sliver( - initialList: widget.contactList.toList(), + initialList: widget.contactList == null + ? [] + : widget.contactList!.toList(), itemBuilder: (c) => ContactItemWidget(contact: c, disabled: widget.disabled) .paddingLTRB(0, 4, 0, 0), filter: (value) { final lowerValue = value.toLowerCase(); - return widget.contactList + if (widget.contactList == null) { + return []; + } + return widget.contactList! .where((element) => element.nickname.toLowerCase().contains(lowerValue) || element.profile.name @@ -65,9 +70,13 @@ class _ContactListWidgetState extends State }, searchFieldHeight: 40, spaceBetweenSearchAndList: 4, - emptyWidget: const EmptyContactListWidget(), + emptyWidget: widget.contactList == null + ? waitingPage( + text: translate('contacts_page.loading_contacts')) + : const EmptyContactListWidget(), defaultSuffixIconColor: scale.primaryScale.border, closeKeyboardWhenScrolling: true, + searchFieldEnabled: widget.contactList != null, inputDecoration: InputDecoration( labelText: translate('contact_list.search'), ), diff --git a/lib/conversation/cubits/active_single_contact_chat_bloc_map_cubit.dart b/lib/conversation/cubits/active_single_contact_chat_bloc_map_cubit.dart index 88860c4..02202ed 100644 --- a/lib/conversation/cubits/active_single_contact_chat_bloc_map_cubit.dart +++ b/lib/conversation/cubits/active_single_contact_chat_bloc_map_cubit.dart @@ -37,8 +37,8 @@ class _SingleContactChatState extends Equatable { ]; } -// Map of localConversationRecordKey to MessagesCubit -// Wraps a MessagesCubit to stream the latest messages to the state +// Map of localConversationRecordKey to SingleContactMessagesCubit +// Wraps a SingleContactMessagesCubit to stream the latest messages to the state // Automatically follows the state of a ActiveConversationsBlocMapCubit. class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit diff --git a/lib/layout/home/home_account_ready.dart b/lib/layout/home/home_account_ready.dart index 5c7dd73..a5dba61 100644 --- a/lib/layout/home/home_account_ready.dart +++ b/lib/layout/home/home_account_ready.dart @@ -86,7 +86,7 @@ class _HomeAccountReadyState extends State { if (activeChatLocalConversationKey == null) { return const NoConversationWidget(); } - return ChatComponentWidget.builder( + return ChatComponentWidget( localConversationRecordKey: activeChatLocalConversationKey, key: ValueKey(activeChatLocalConversationKey)); } @@ -104,11 +104,6 @@ class _HomeAccountReadyState extends State { final activeChat = context.watch().state; final hasActiveChat = activeChat != null; - // if (hasActiveChat) { - // _chatAnimationController.forward(); - // } else { - // _chatAnimationController.reset(); - // } return LayoutBuilder(builder: (context, constraints) { const leftColumnSize = 300.0; diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index f482757..8e3bf48 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -1,9 +1,14 @@ +import 'dart:async'; import 'dart:math'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart'; import 'package:provider/provider.dart'; +import 'package:quickalert/quickalert.dart'; import 'package:transitioned_indexed_stack/transitioned_indexed_stack.dart'; +import 'package:url_launcher/url_launcher_string.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; @@ -36,6 +41,8 @@ class HomeScreenState extends State .indexWhere((x) => x.superIdentity.recordKey == activeLocalAccount); final canClose = activeIndex != -1; + unawaited(_doBetaDialog(context)); + if (!canClose) { await _zoomDrawerController.open!(); } @@ -43,6 +50,36 @@ class HomeScreenState extends State super.initState(); } + Future _doBetaDialog(BuildContext context) async { + await QuickAlert.show( + context: context, + title: translate('splash.beta_title'), + widget: RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: translate('splash.beta_text'), + style: const TextStyle( + color: Colors.black87, + ), + ), + TextSpan( + text: 'https://veilid.com/chat/beta', + style: const TextStyle( + color: Colors.blue, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = + () => launchUrlString('https://veilid.com/chat/beta'), + ), + ], + ), + ), + type: QuickAlertType.warning); + } + Widget _buildAccountPage( BuildContext context, TypedKey superIdentityRecordKey, diff --git a/lib/layout/home/main_pager/contacts_page.dart b/lib/layout/home/main_pager/contacts_page.dart index d858270..c3699f2 100644 --- a/lib/layout/home/main_pager/contacts_page.dart +++ b/lib/layout/home/main_pager/contacts_page.dart @@ -43,8 +43,7 @@ class ContactsPageState extends State { final ciState = context.watch().state; final ciBusy = ciState.busy; final contactList = - ciState.state.asData?.value.map((x) => x.value).toIList() ?? - const IListConst([]); + ciState.state.asData?.value.map((x) => x.value).toIList(); return CustomScrollView(slivers: [ if (contactInvitationRecordList.isNotEmpty) @@ -53,7 +52,7 @@ class ContactsPageState extends State { sliver: ContactInvitationListWidget( contactInvitationRecordList: contactInvitationRecordList, disabled: cilBusy)), - ContactListWidget(contactList: contactList, disabled: ciBusy), + ContactListWidget(contactList: contactList, disabled: ciBusy) ]).paddingLTRB(8, 0, 8, 8); } } diff --git a/lib/layout/splash.dart b/lib/layout/splash.dart index c3af797..97d4a70 100644 --- a/lib/layout/splash.dart +++ b/lib/layout/splash.dart @@ -1,5 +1,9 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_translate/flutter_translate.dart'; +import 'package:quickalert/quickalert.dart'; import 'package:radix_colors/radix_colors.dart'; import '../tools/tools.dart'; diff --git a/lib/theme/views/widget_helpers.dart b/lib/theme/views/widget_helpers.dart index 6e485fc..7f9e57f 100644 --- a/lib/theme/views/widget_helpers.dart +++ b/lib/theme/views/widget_helpers.dart @@ -37,10 +37,12 @@ extension ModalProgressExt on Widget { Widget buildProgressIndicator() => Builder(builder: (context) { final theme = Theme.of(context); final scale = theme.extension()!; - return SpinKitFoldingCube( - color: scale.tertiaryScale.primary, - size: 80, - ); + return FittedBox( + fit: BoxFit.scaleDown, + child: SpinKitFoldingCube( + color: scale.tertiaryScale.primary, + size: 80, + )); }); Widget waitingPage({String? text}) => Builder(builder: (context) { @@ -48,11 +50,17 @@ Widget waitingPage({String? text}) => Builder(builder: (context) { final scale = theme.extension()!; return ColoredBox( color: scale.tertiaryScale.appBackground, - child: Center( - child: Column(children: [ - buildProgressIndicator().expanded(), - if (text != null) Text(text) - ]))); + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + buildProgressIndicator(), + if (text != null) + Text(text, + textAlign: TextAlign.center, + style: theme.textTheme.bodySmall! + .copyWith(color: scale.tertiaryScale.appText)) + ])); }); Widget debugPage(String text) => Builder( diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart index 8fd565b..08501e6 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart @@ -37,7 +37,7 @@ typedef DHTLogState = AsyncValue>; typedef DHTLogBusyState = BlocBusyState>; class DHTLogCubit extends Cubit> - with BlocBusyWrapper> { + with BlocBusyWrapper>, RefreshableCubit { DHTLogCubit({ required Future Function() open, required T Function(List data) decodeElement, @@ -52,7 +52,7 @@ class DHTLogCubit extends Cubit> _log = await open(); _wantsCloseRecord = true; break; - } on VeilidAPIExceptionTryAgain { + } on DHTExceptionNotAvailable { // Wait for a bit await asyncSleep(); } @@ -91,6 +91,7 @@ class DHTLogCubit extends Cubit> await _refreshNoWait(forceRefresh: forceRefresh); } + @override Future refresh({bool forceRefresh = false}) async { await _initWait(); await _refreshNoWait(forceRefresh: forceRefresh); @@ -101,68 +102,51 @@ class DHTLogCubit extends Cubit> Future _refreshInner(void Function(AsyncValue>) emit, {bool forceRefresh = false}) async { - late final AsyncValue>> avElements; late final int length; - await _log.operate((reader) async { + final window = await _log.operate((reader) async { length = reader.length; - avElements = - await loadElementsFromReader(reader, _windowTail, _windowSize); + return loadElementsFromReader(reader, _windowTail, _windowSize); }); - final err = avElements.asError; - if (err != null) { - emit(AsyncValue.error(err.error, err.stackTrace)); + if (window == null) { + setWantsRefresh(); return; } - final loading = avElements.asLoading; - if (loading != null) { - emit(const AsyncValue.loading()); - return; - } - final window = avElements.asData!.value; emit(AsyncValue.data(DHTLogStateData( length: length, window: window, windowTail: _windowTail, windowSize: _windowSize, follow: _follow))); + setRefreshed(); } // Tail is one past the last element to load - Future>>> loadElementsFromReader( + Future>?> loadElementsFromReader( DHTLogReadOperations reader, int tail, int count, {bool forceRefresh = false}) async { - try { - final length = reader.length; - if (length == 0) { - return const AsyncValue.data(IList.empty()); - } - final end = ((tail - 1) % length) + 1; - final start = (count < end) ? end - count : 0; - - // If this is writeable get the offline positions - Set? offlinePositions; - if (_log.writer != null) { - offlinePositions = await reader.getOfflinePositions(); - if (offlinePositions == null) { - return const AsyncValue.loading(); - } - } - - // Get the items - final allItems = (await reader.getRange(start, - length: end - start, forceRefresh: forceRefresh)) - ?.indexed - .map((x) => OnlineElementState( - value: _decodeElement(x.$2), - isOffline: offlinePositions?.contains(x.$1) ?? false)) - .toIList(); - if (allItems == null) { - return const AsyncValue.loading(); - } - return AsyncValue.data(allItems); - } on Exception catch (e, st) { - return AsyncValue.error(e, st); + final length = reader.length; + if (length == 0) { + return const IList.empty(); } + final end = ((tail - 1) % length) + 1; + final start = (count < end) ? end - count : 0; + + // If this is writeable get the offline positions + Set? offlinePositions; + if (_log.writer != null) { + offlinePositions = await reader.getOfflinePositions(); + } + + // Get the items + final allItems = (await reader.getRange(start, + length: end - start, forceRefresh: forceRefresh)) + ?.indexed + .map((x) => OnlineElementState( + value: _decodeElement(x.$2), + isOffline: offlinePositions?.contains(x.$1) ?? false)) + .toIList(); + + return allItems; } void _update(DHTLogUpdate upd) { diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart index 90b8428..6281d6e 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart @@ -47,11 +47,13 @@ class _DHTLogRead implements DHTLogReadOperations { final chunks = Iterable.generate(length) .slices(kMaxDHTConcurrency) - .map((chunk) => - chunk.map((pos) => get(pos + start, forceRefresh: forceRefresh))); + .map((chunk) => chunk + .map((pos) async => get(pos + start, forceRefresh: forceRefresh))); for (final chunk in chunks) { final elems = await chunk.wait; + + // If any element was unavailable, return null if (elems.contains(null)) { return null; } diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index 070c494..e9442f0 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -296,7 +296,7 @@ class _DHTLogSpine { segmentKeyBytes); } - Future _openOrCreateSegment(int segmentNumber) async { + Future _openOrCreateSegment(int segmentNumber) async { assert(_spineMutex.isLocked, 'should be in mutex here'); assert(_spineRecord.writer != null, 'should be writable'); @@ -306,51 +306,56 @@ class _DHTLogSpine { final subkey = l.subkey; final segment = l.segment; - var subkeyData = await _spineRecord.get(subkey: subkey); - subkeyData ??= _makeEmptySubkey(); - while (true) { - final segmentKey = _getSegmentKey(subkeyData!, segment); - if (segmentKey == null) { - // Create a shortarray segment - final segmentRec = await DHTShortArray.create( - debugName: '${_spineRecord.debugName}_spine_${subkey}_$segment', - stride: _segmentStride, - crypto: _spineRecord.crypto, - parent: _spineRecord.key, - routingContext: _spineRecord.routingContext, - writer: _spineRecord.writer, - ); - var success = false; - try { - // Write it back to the spine record - _setSegmentKey(subkeyData, segment, segmentRec.recordKey); - subkeyData = - await _spineRecord.tryWriteBytes(subkeyData, subkey: subkey); - // If the write was successful then we're done - if (subkeyData == null) { - // Return it - success = true; - return segmentRec; - } - } finally { - if (!success) { - await segmentRec.close(); - await segmentRec.delete(); + try { + var subkeyData = await _spineRecord.get(subkey: subkey); + subkeyData ??= _makeEmptySubkey(); + + while (true) { + final segmentKey = _getSegmentKey(subkeyData!, segment); + if (segmentKey == null) { + // Create a shortarray segment + final segmentRec = await DHTShortArray.create( + debugName: '${_spineRecord.debugName}_spine_${subkey}_$segment', + stride: _segmentStride, + crypto: _spineRecord.crypto, + parent: _spineRecord.key, + routingContext: _spineRecord.routingContext, + writer: _spineRecord.writer, + ); + var success = false; + try { + // Write it back to the spine record + _setSegmentKey(subkeyData, segment, segmentRec.recordKey); + subkeyData = + await _spineRecord.tryWriteBytes(subkeyData, subkey: subkey); + // If the write was successful then we're done + if (subkeyData == null) { + // Return it + success = true; + return segmentRec; + } + } finally { + if (!success) { + await segmentRec.close(); + await segmentRec.delete(); + } } + } else { + // Open a shortarray segment + final segmentRec = await DHTShortArray.openWrite( + segmentKey, + _spineRecord.writer!, + debugName: '${_spineRecord.debugName}_spine_${subkey}_$segment', + crypto: _spineRecord.crypto, + parent: _spineRecord.key, + routingContext: _spineRecord.routingContext, + ); + return segmentRec; } - } else { - // Open a shortarray segment - final segmentRec = await DHTShortArray.openWrite( - segmentKey, - _spineRecord.writer!, - debugName: '${_spineRecord.debugName}_spine_${subkey}_$segment', - crypto: _spineRecord.crypto, - parent: _spineRecord.key, - routingContext: _spineRecord.routingContext, - ); - return segmentRec; + // Loop if we need to try again with the new data from the network } - // Loop if we need to try again with the new data from the network + } on DHTExceptionNotAvailable { + return null; } } @@ -364,34 +369,38 @@ class _DHTLogSpine { final segment = l.segment; // See if we have the segment key locally - TypedKey? segmentKey; - var subkeyData = await _spineRecord.get( - subkey: subkey, refreshMode: DHTRecordRefreshMode.local); - if (subkeyData != null) { - segmentKey = _getSegmentKey(subkeyData, segment); - } - if (segmentKey == null) { - // If not, try from the network - subkeyData = await _spineRecord.get( - subkey: subkey, refreshMode: DHTRecordRefreshMode.network); - if (subkeyData == null) { - return null; + try { + TypedKey? segmentKey; + var subkeyData = await _spineRecord.get( + subkey: subkey, refreshMode: DHTRecordRefreshMode.local); + if (subkeyData != null) { + segmentKey = _getSegmentKey(subkeyData, segment); } - segmentKey = _getSegmentKey(subkeyData, segment); if (segmentKey == null) { - return null; + // If not, try from the network + subkeyData = await _spineRecord.get( + subkey: subkey, refreshMode: DHTRecordRefreshMode.network); + if (subkeyData == null) { + return null; + } + segmentKey = _getSegmentKey(subkeyData, segment); + if (segmentKey == null) { + return null; + } } - } - // Open a shortarray segment - final segmentRec = await DHTShortArray.openRead( - segmentKey, - debugName: '${_spineRecord.debugName}_spine_${subkey}_$segment', - crypto: _spineRecord.crypto, - parent: _spineRecord.key, - routingContext: _spineRecord.routingContext, - ); - return segmentRec; + // Open a shortarray segment + final segmentRec = await DHTShortArray.openRead( + segmentKey, + debugName: '${_spineRecord.debugName}_spine_${subkey}_$segment', + crypto: _spineRecord.crypto, + parent: _spineRecord.key, + routingContext: _spineRecord.routingContext, + ); + return segmentRec; + } on DHTExceptionNotAvailable { + return null; + } } _DHTLogSegmentLookup _lookupSegment(int segmentNumber) { diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart index 7f4d9ce..1b5c09f 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart @@ -17,7 +17,7 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { } final lookup = await _spine.lookupPosition(pos); if (lookup == null) { - throw DHTExceptionInvalidData(); + throw const DHTExceptionInvalidData(); } // Write item to the segment @@ -26,7 +26,7 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { final success = await write.tryWriteItem(lookup.pos, newValue, output: output); if (!success) { - throw DHTExceptionOutdated(); + throw const DHTExceptionOutdated(); } })); } on DHTExceptionOutdated { @@ -45,12 +45,12 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { } final aLookup = await _spine.lookupPosition(aPos); if (aLookup == null) { - throw DHTExceptionInvalidData(); + throw const DHTExceptionInvalidData(); } final bLookup = await _spine.lookupPosition(bPos); if (bLookup == null) { await aLookup.close(); - throw DHTExceptionInvalidData(); + throw const DHTExceptionInvalidData(); } // Swap items in the segments @@ -65,20 +65,20 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { if (bItem.value == null) { final aItem = await aWrite.get(aLookup.pos); if (aItem == null) { - throw DHTExceptionInvalidData(); + throw const DHTExceptionInvalidData(); } await sb.operateWriteEventual((bWrite) async { final success = await bWrite .tryWriteItem(bLookup.pos, aItem, output: bItem); if (!success) { - throw DHTExceptionOutdated(); + throw const DHTExceptionOutdated(); } }); } final success = await aWrite.tryWriteItem(aLookup.pos, bItem.value!); if (!success) { - throw DHTExceptionOutdated(); + throw const DHTExceptionOutdated(); } }))); } @@ -101,7 +101,7 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { await write.clear(); } else if (lookup.pos != write.length) { // We should always be appending at the length - throw DHTExceptionInvalidData(); + throw const DHTExceptionInvalidData(); } return write.add(value); })); @@ -122,7 +122,7 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { final lookup = await _spine.lookupPosition(insertPos + valueIdx); if (lookup == null) { - throw DHTExceptionInvalidData(); + throw const DHTExceptionInvalidData(); } final sacount = min(remaining, DHTShortArray.maxElements - lookup.pos); @@ -137,7 +137,7 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { await write.clear(); } else if (lookup.pos != write.length) { // We should always be appending at the length - throw DHTExceptionInvalidData(); + throw const DHTExceptionInvalidData(); } return write.addAll(sublistValues); })); @@ -152,7 +152,7 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { await dws(); if (!success) { - throw DHTExceptionOutdated(); + throw const DHTExceptionOutdated(); } } 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 e04af10..a0994bf 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 @@ -134,11 +134,25 @@ class DHTRecord implements DHTDeleteable { return null; } - final valueData = await _routingContext.getDHTValue(key, subkey, - forceRefresh: refreshMode._forceRefresh); + var retry = kDHTTryAgainTries; + ValueData? valueData; + while (true) { + try { + valueData = await _routingContext.getDHTValue(key, subkey, + forceRefresh: refreshMode._forceRefresh); + break; + } on VeilidAPIExceptionTryAgain { + retry--; + if (retry == 0) { + throw const DHTExceptionNotAvailable(); + } + await asyncSleep(); + } + } if (valueData == null) { return null; } + // See if this get resulted in a newer sequence number if (refreshMode == DHTRecordRefreshMode.update && lastSeq != null && @@ -415,10 +429,10 @@ class DHTRecord implements DHTDeleteable { Timestamp? expiration, int? count}) async { // Set up watch requirements which will get picked up by the next tick - final oldWatchState = watchState; - watchState = + final oldWatchState = _watchState; + _watchState = _WatchState(subkeys: subkeys, expiration: expiration, count: count); - if (oldWatchState != watchState) { + if (oldWatchState != _watchState) { _sharedDHTRecordData.needsWatchStateUpdate = true; } } @@ -476,8 +490,8 @@ class DHTRecord implements DHTDeleteable { /// Takes effect on the next DHTRecordPool tick Future cancelWatch() async { // Tear down watch requirements - if (watchState != null) { - watchState = null; + if (_watchState != null) { + _watchState = null; _sharedDHTRecordData.needsWatchStateUpdate = true; } } @@ -503,7 +517,7 @@ class DHTRecord implements DHTDeleteable { {required bool local, required Uint8List? data, required List subkeys}) { - final ws = watchState; + final ws = _watchState; if (ws != null) { final watchedSubkeys = ws.subkeys; if (watchedSubkeys == null) { @@ -551,6 +565,5 @@ class DHTRecord implements DHTDeleteable { final _mutex = Mutex(); int _openCount; StreamController? _watchController; - @internal - _WatchState? watchState; + _WatchState? _watchState; } 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 da75e06..ac33716 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 @@ -29,8 +29,7 @@ abstract class DHTRecordCubit extends Cubit> { _record = await open(); _wantsCloseRecord = true; break; - } on VeilidAPIExceptionKeyNotFound { - } on VeilidAPIExceptionTryAgain { + } on DHTExceptionNotAvailable { // Wait for a bit await asyncSleep(); } 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 5aa7915..68ea53d 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 @@ -21,8 +21,11 @@ part 'dht_record_pool_private.dart'; /// Maximum number of concurrent DHT operations to perform on the network const int kMaxDHTConcurrency = 8; -/// Number of times to retry a 'key not found' -const int kDHTKeyNotFoundRetry = 3; +/// Total number of times to try in a 'VeilidAPIExceptionKeyNotFound' loop +const int kDHTKeyNotFoundTries = 3; + +/// Total number of times to try in a 'VeilidAPIExceptionTryAgain' loop +const int kDHTTryAgainTries = 3; typedef DHTRecordPoolLogger = void Function(String message); @@ -280,12 +283,12 @@ class DHTRecordPool with TableDBBackedJson { 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; + 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; + rec._watchState = null; } } } @@ -392,7 +395,7 @@ class DHTRecordPool with TableDBBackedJson { if (openedRecordInfo == null) { // Fresh open, just open the record - var retry = kDHTKeyNotFoundRetry; + var retry = kDHTKeyNotFoundTries; late final DHTRecordDescriptor recordDescriptor; while (true) { try { @@ -403,7 +406,7 @@ class DHTRecordPool with TableDBBackedJson { await asyncSleep(); retry--; if (retry == 0) { - rethrow; + throw DHTExceptionNotAvailable(); } } } @@ -705,7 +708,7 @@ class DHTRecordPool with TableDBBackedJson { var cancelWatch = true; for (final rec in records) { - final ws = rec.watchState; + final ws = rec._watchState; if (ws != null) { cancelWatch = false; final wsCount = ws.count; @@ -762,9 +765,9 @@ class DHTRecordPool with TableDBBackedJson { static void _updateWatchRealExpirations(Iterable records, Timestamp realExpiration, Timestamp renewalTime) { for (final rec in records) { - final ws = rec.watchState; + final ws = rec._watchState; if (ws != null) { - rec.watchState = _WatchState( + rec._watchState = _WatchState( subkeys: ws.subkeys, expiration: ws.expiration, count: ws.count, 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 fe291ca..d0d26a8 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 @@ -68,7 +68,7 @@ class DHTShortArray implements DHTDeleteable { } }); return dhtShortArray; - } on Exception catch (_) { + } on Exception { await dhtRecord.close(); await pool.deleteRecord(dhtRecord.key); rethrow; @@ -89,7 +89,7 @@ class DHTShortArray implements DHTDeleteable { final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); await dhtShortArray._head.operate((head) => head._loadHead()); return dhtShortArray; - } on Exception catch (_) { + } on Exception { await dhtRecord.close(); rethrow; } @@ -113,7 +113,7 @@ class DHTShortArray implements DHTDeleteable { final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); await dhtShortArray._head.operate((head) => head._loadHead()); return dhtShortArray; - } on Exception catch (_) { + } on Exception { await dhtRecord.close(); rethrow; } 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 97bf1d9..30309a3 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 @@ -8,6 +8,7 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:meta/meta.dart'; import '../../../veilid_support.dart'; +import '../interfaces/refreshable_cubit.dart'; @immutable class DHTShortArrayElementState extends Equatable { @@ -24,7 +25,7 @@ typedef DHTShortArrayState = AsyncValue>>; typedef DHTShortArrayBusyState = BlocBusyState>; class DHTShortArrayCubit extends Cubit> - with BlocBusyWrapper> { + with BlocBusyWrapper>, RefreshableCubit { DHTShortArrayCubit({ required Future Function() open, required T Function(List data) decodeElement, @@ -39,7 +40,7 @@ class DHTShortArrayCubit extends Cubit> _shortArray = await open(); _wantsCloseRecord = true; break; - } on VeilidAPIExceptionTryAgain { + } on DHTExceptionNotAvailable { // Wait for a bit await asyncSleep(); } @@ -57,6 +58,7 @@ class DHTShortArrayCubit extends Cubit> }); } + @override Future refresh({bool forceRefresh = false}) async { await _initWait(); await _refreshNoWait(forceRefresh: forceRefresh); @@ -87,9 +89,13 @@ class DHTShortArrayCubit extends Cubit> .toIList(); return allItems; }); - if (newState != null) { - emit(AsyncValue.data(newState)); + if (newState == null) { + // Mark us as needing refresh + setWantsRefresh(); + return; } + emit(AsyncValue.data(newState)); + setRefreshed(); } on Exception catch (e) { emit(AsyncValue.error(e)); } 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 d2aa84a..ff550e8 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 @@ -91,7 +91,7 @@ class _DHTShortArrayHead { if (!await _writeHead()) { // Failed to write head means head got overwritten so write should // be considered failed - throw DHTExceptionOutdated(); + throw const DHTExceptionOutdated(); } onUpdatedHead?.call(); 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 index 94d58b8..ddfdedc 100644 --- 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 @@ -17,21 +17,25 @@ class _DHTShortArrayRead implements DHTShortArrayReadOperations { throw IndexError.withLength(pos, length); } - final lookup = await _head.lookupPosition(pos, false); + try { + final lookup = await _head.lookupPosition(pos, false); - final refresh = forceRefresh || _head.positionNeedsRefresh(pos); - final outSeqNum = Output(); - final out = lookup.record.get( - subkey: lookup.recordSubkey, - refreshMode: refresh - ? DHTRecordRefreshMode.network - : DHTRecordRefreshMode.cached, - outSeqNum: outSeqNum); - if (outSeqNum.value != null) { - _head.updatePositionSeq(pos, false, outSeqNum.value!); + final refresh = forceRefresh || _head.positionNeedsRefresh(pos); + final outSeqNum = Output(); + final out = await lookup.record.get( + subkey: lookup.recordSubkey, + refreshMode: refresh + ? DHTRecordRefreshMode.network + : DHTRecordRefreshMode.cached, + outSeqNum: outSeqNum); + if (outSeqNum.value != null) { + _head.updatePositionSeq(pos, false, outSeqNum.value!); + } + return out; + } on DHTExceptionNotAvailable { + // If any element is not available, return null + return null; } - - return out; } (int, int) _clampStartLen(int start, int? len) { @@ -56,11 +60,13 @@ class _DHTShortArrayRead implements DHTShortArrayReadOperations { final chunks = Iterable.generate(length) .slices(kMaxDHTConcurrency) - .map((chunk) => - chunk.map((pos) => get(pos + start, forceRefresh: forceRefresh))); + .map((chunk) => chunk + .map((pos) async => get(pos + start, forceRefresh: forceRefresh))); for (final chunk in chunks) { final elems = await chunk.wait; + + // If any element was unavailable, return null if (elems.contains(null)) { return null; } diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/exceptions.dart b/packages/veilid_support/lib/dht_support/src/interfaces/exceptions.dart index bced3dd..b17dbee 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/exceptions.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/exceptions.dart @@ -1,15 +1,21 @@ class DHTExceptionOutdated implements Exception { - DHTExceptionOutdated( + const DHTExceptionOutdated( [this.cause = 'operation failed due to newer dht value']); - String cause; + final String cause; } class DHTExceptionInvalidData implements Exception { - DHTExceptionInvalidData([this.cause = 'dht data structure is corrupt']); - String cause; + const DHTExceptionInvalidData([this.cause = 'dht data structure is corrupt']); + final String cause; } class DHTExceptionCancelled implements Exception { - DHTExceptionCancelled([this.cause = 'operation was cancelled']); - String cause; + const DHTExceptionCancelled([this.cause = 'operation was cancelled']); + final String cause; +} + +class DHTExceptionNotAvailable implements Exception { + const DHTExceptionNotAvailable( + [this.cause = 'request could not be completed at this time']); + final String cause; } diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart b/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart index 57d0979..a162dc8 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart @@ -6,3 +6,4 @@ export 'dht_random_read.dart'; export 'dht_random_write.dart'; export 'dht_truncate.dart'; export 'exceptions.dart'; +export 'refreshable_cubit.dart'; diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/refreshable_cubit.dart b/packages/veilid_support/lib/dht_support/src/interfaces/refreshable_cubit.dart new file mode 100644 index 0000000..a04e1bb --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/interfaces/refreshable_cubit.dart @@ -0,0 +1,16 @@ +abstract mixin class RefreshableCubit { + Future refresh({bool forceRefresh = false}); + + void setWantsRefresh() { + _wantsRefresh = true; + } + + void setRefreshed() { + _wantsRefresh = false; + } + + bool get wantsRefresh => _wantsRefresh; + + //////////////////////////////////////////////////////////////////////////// + bool _wantsRefresh = false; +} diff --git a/pubspec.lock b/pubspec.lock index 42ac495..2b3149b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1576,7 +1576,7 @@ packages: source: hosted version: "1.1.0" url_launcher: - dependency: transitive + dependency: "direct main" description: name: url_launcher sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" diff --git a/pubspec.yaml b/pubspec.yaml index ca47d9d..edfa7b0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -96,6 +96,7 @@ dependencies: stack_trace: ^1.11.1 stream_transform: ^2.1.0 transitioned_indexed_stack: ^1.0.2 + url_launcher: ^6.3.0 uuid: ^4.4.0 veilid: # veilid: ^0.0.1 From 1455aabe6cc5da09ec67fd0903fad2240636a172 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 25 Jul 2024 14:37:51 -0400 Subject: [PATCH 165/270] contact invitation accept notifications --- assets/i18n/en.json | 4 + .../cubits/per_account_collection_cubit.dart | 6 +- .../views/edit_account_page.dart | 21 +- lib/app.dart | 28 +- lib/chat/views/chat_component_widget.dart | 32 +- .../cubits/contact_invitation_list_cubit.dart | 8 +- .../cubits/invitation_generator_cubit.dart | 3 +- .../waiting_invitations_bloc_map_cubit.dart | 20 +- .../views/contact_invitation_display.dart | 142 +++++---- .../views/contact_invitation_item_widget.dart | 8 +- .../views/create_invitation_dialog.dart | 31 +- .../views/invitation_dialog.dart | 13 +- .../views/scan_invitation_dialog.dart | 27 +- lib/layout/splash.dart | 4 - .../cubits/notifications_cubit.dart | 26 ++ .../models/notifications_state.dart | 23 ++ .../models/notifications_state.freezed.dart | 290 ++++++++++++++++++ lib/notifications/notifications.dart | 3 + .../views/notifications_widget.dart | 91 ++++++ .../{cubit => cubits}/router_cubit.dart | 76 ++--- .../router_cubit.freezed.dart | 0 .../{cubit => cubits}/router_cubit.g.dart | 0 lib/router/router.dart | 3 +- lib/router/views/router_shell.dart | 12 + lib/theme/views/widget_helpers.dart | 41 --- lib/veilid_processor/views/developer.dart | 14 +- .../identity_instance.freezed.dart | 12 +- 27 files changed, 718 insertions(+), 220 deletions(-) create mode 100644 lib/notifications/cubits/notifications_cubit.dart create mode 100644 lib/notifications/models/notifications_state.dart create mode 100644 lib/notifications/models/notifications_state.freezed.dart create mode 100644 lib/notifications/notifications.dart create mode 100644 lib/notifications/views/notifications_widget.dart rename lib/router/{cubit => cubits}/router_cubit.dart (65%) rename lib/router/{cubit => cubits}/router_cubit.freezed.dart (100%) rename lib/router/{cubit => cubits}/router_cubit.g.dart (100%) create mode 100644 lib/router/views/router_shell.dart diff --git a/assets/i18n/en.json b/assets/i18n/en.json index d4d2b4b..7185fab 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -153,6 +153,10 @@ "invalid_pin": "Invalid PIN", "invalid_password": "Invalid password" }, + "waiting_invitation": { + "accepted": "Contact invitation accepted from {name}", + "reject": "Contact invitation was rejected" + }, "paste_invitation_dialog": { "title": "Paste Contact Invite", "paste_invite_here": "Paste your contact invite here:", diff --git a/lib/account_manager/cubits/per_account_collection_cubit.dart b/lib/account_manager/cubits/per_account_collection_cubit.dart index 5f208fb..226d213 100644 --- a/lib/account_manager/cubits/per_account_collection_cubit.dart +++ b/lib/account_manager/cubits/per_account_collection_cubit.dart @@ -11,6 +11,7 @@ import '../../chat_list/chat_list.dart'; import '../../contact_invitation/contact_invitation.dart'; import '../../contacts/contacts.dart'; import '../../conversation/conversation.dart'; +import '../../notifications/notifications.dart'; import '../../proto/proto.dart' as proto; import '../account_manager.dart'; @@ -146,6 +147,7 @@ class PerAccountCollectionCubit extends Cubit { accountRecordCubit!, contactInvitationListCubit, contactListCubit, + _locator(), )); // ActiveChatCubit @@ -262,13 +264,15 @@ class PerAccountCollectionCubit extends Cubit { AccountInfo, AccountRecordCubit, ContactInvitationListCubit, - ContactListCubit + ContactListCubit, + NotificationsCubit, )>( create: (params) => WaitingInvitationsBlocMapCubit( accountInfo: params.$1, accountRecordCubit: params.$2, contactInvitationListCubit: params.$3, contactListCubit: params.$4, + notificationsCubit: params.$5, )); final activeChatCubitUpdater = BlocUpdater(create: (_) => ActiveChatCubit(null)); diff --git a/lib/account_manager/views/edit_account_page.dart b/lib/account_manager/views/edit_account_page.dart index 59adeaf..0554def 100644 --- a/lib/account_manager/views/edit_account_page.dart +++ b/lib/account_manager/views/edit_account_page.dart @@ -11,6 +11,7 @@ import 'package:protobuf/protobuf.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../layout/default_app_bar.dart'; +import '../../notifications/notifications.dart'; import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; import '../../tools/tools.dart'; @@ -106,12 +107,14 @@ class _EditAccountPageState extends WindowSetupState { final success = await AccountRepository.instance.deleteLocalAccount( widget.superIdentityRecordKey, widget.accountRecord); if (success && mounted) { - showInfoToast( - context, translate('edit_account_page.account_removed')); + context + .read() + .info(text: translate('edit_account_page.account_removed')); GoRouterHelper(context).pop(); } else if (mounted) { - showErrorToast( - context, translate('edit_account_page.failed_to_remove')); + context + .read() + .error(text: translate('edit_account_page.failed_to_remove')); } } finally { if (mounted) { @@ -172,12 +175,14 @@ class _EditAccountPageState extends WindowSetupState { final success = await AccountRepository.instance.destroyAccount( widget.superIdentityRecordKey, widget.accountRecord); if (success && mounted) { - showInfoToast( - context, translate('edit_account_page.account_destroyed')); + context + .read() + .info(text: translate('edit_account_page.account_destroyed')); GoRouterHelper(context).pop(); } else if (mounted) { - showErrorToast( - context, translate('edit_account_page.failed_to_destroy')); + context + .read() + .error(text: translate('edit_account_page.failed_to_destroy')); } } finally { if (mounted) { diff --git a/lib/app.dart b/lib/app.dart index ec5c9be..5c47fa3 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,5 +1,6 @@ import 'package:animated_theme_switcher/animated_theme_switcher.dart'; 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/services.dart'; @@ -13,6 +14,7 @@ import 'package:veilid_support/veilid_support.dart'; import 'account_manager/account_manager.dart'; import 'init.dart'; import 'layout/splash.dart'; +import 'notifications/notifications.dart'; import 'router/router.dart'; import 'settings/settings.dart'; import 'theme/theme.dart'; @@ -24,8 +26,8 @@ class ReloadThemeIntent extends Intent { const ReloadThemeIntent(); } -class AttachDetachThemeIntent extends Intent { - const AttachDetachThemeIntent(); +class AttachDetachIntent extends Intent { + const AttachDetachIntent(); } class VeilidChatApp extends StatelessWidget { @@ -55,7 +57,7 @@ class VeilidChatApp extends StatelessWidget { }); } - void _attachDetachTheme(BuildContext context) { + void _attachDetach(BuildContext context) { singleFuture(this, () async { if (ProcessorRepository.instance.processorConnectionState.isAttached) { log.info('Detaching'); @@ -77,14 +79,13 @@ class VeilidChatApp extends StatelessWidget { const ReloadThemeIntent(), LogicalKeySet( LogicalKeyboardKey.alt, LogicalKeyboardKey.keyD): - const AttachDetachThemeIntent(), + const AttachDetachIntent(), }, child: Actions(actions: >{ ReloadThemeIntent: CallbackAction( onInvoke: (intent) => _reloadTheme(context)), - AttachDetachThemeIntent: - CallbackAction( - onInvoke: (intent) => _attachDetachTheme(context)), + AttachDetachIntent: CallbackAction( + onInvoke: (intent) => _attachDetach(context)), }, child: Focus(autofocus: true, child: builder(context))))); @override @@ -101,10 +102,17 @@ class VeilidChatApp extends StatelessWidget { final localizationDelegate = LocalizedApp.of(context).delegate; return ThemeProvider( initTheme: initialThemeData, - builder: (_, theme) => LocalizationProvider( + builder: (context, theme) => LocalizationProvider( state: LocalizationProvider.of(context).state, child: MultiBlocProvider( providers: [ + BlocProvider( + create: (context) => + PreferencesCubit(PreferencesRepository.instance), + ), + BlocProvider( + create: (context) => NotificationsCubit( + const NotificationsState(queue: IList.empty()))), BlocProvider( create: (context) => ConnectionStateCubit(ProcessorRepository.instance)), @@ -124,10 +132,6 @@ class VeilidChatApp extends StatelessWidget { create: (context) => ActiveLocalAccountCubit(AccountRepository.instance), ), - BlocProvider( - create: (context) => - PreferencesCubit(PreferencesRepository.instance), - ), BlocProvider( create: (context) => PerAccountCollectionBlocMapCubit( accountRepository: AccountRepository.instance, diff --git a/lib/chat/views/chat_component_widget.dart b/lib/chat/views/chat_component_widget.dart index aa87ecf..5de02a7 100644 --- a/lib/chat/views/chat_component_widget.dart +++ b/lib/chat/views/chat_component_widget.dart @@ -13,6 +13,7 @@ import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../contacts/contacts.dart'; import '../../conversation/conversation.dart'; +import '../../notifications/notifications.dart'; import '../../theme/theme.dart'; import '../chat.dart'; @@ -27,10 +28,10 @@ class ChatComponentWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final scale = theme.extension()!; - final scaleConfig = theme.extension()!; - final textTheme = theme.textTheme; + // final theme = Theme.of(context); + // final scale = theme.extension()!; + // final scaleConfig = theme.extension()!; + // final textTheme = theme.textTheme; // Get the account info final accountInfo = context.watch().state; @@ -221,14 +222,15 @@ class ChatComponentWidget extends StatelessWidget { onSendPressed: (pt) { try { if (!messageIsValid) { - showErrorToast(context, - translate('chat.message_too_long')); + context.read().error( + text: + translate('chat.message_too_long')); return; } _handleSendPressed(chatComponentCubit, pt); } on FormatException { - showErrorToast(context, - translate('chat.message_too_long')); + context.read().error( + text: translate('chat.message_too_long')); } }, listBottomWidget: messageIsValid @@ -267,8 +269,11 @@ class ChatComponentWidget extends StatelessWidget { ChatComponentCubit chatComponentCubit, WindowState messageWindow, ScrollNotification notification) async { - print( - '_handlePageForward: messagesState.length=${messageWindow.length} messagesState.windowTail=${messageWindow.windowTail} messagesState.windowCount=${messageWindow.windowCount} ScrollNotification=$notification'); + debugPrint( + '_handlePageForward: messagesState.length=${messageWindow.length} ' + 'messagesState.windowTail=${messageWindow.windowTail} ' + 'messagesState.windowCount=${messageWindow.windowCount} ' + 'ScrollNotification=$notification'); // Go forward a page final tail = min(messageWindow.length, @@ -299,8 +304,11 @@ class ChatComponentWidget extends StatelessWidget { WindowState messageWindow, ScrollNotification notification, ) async { - print( - '_handlePageBackward: messagesState.length=${messageWindow.length} messagesState.windowTail=${messageWindow.windowTail} messagesState.windowCount=${messageWindow.windowCount} ScrollNotification=$notification'); + debugPrint( + '_handlePageBackward: messagesState.length=${messageWindow.length} ' + 'messagesState.windowTail=${messageWindow.windowTail} ' + 'messagesState.windowCount=${messageWindow.windowCount} ' + 'ScrollNotification=$notification'); // Go back a page final tail = max( diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index 9bd589e..f3e9521 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -54,7 +54,7 @@ class ContactInvitationListCubit return dhtRecord; } - Future createInvitation( + Future<(Uint8List, TypedKey)> createInvitation( {required proto.Profile profile, required EncryptionKeyType encryptionKeyType, required String encryptionKey, @@ -82,6 +82,7 @@ class ContactInvitationListCubit // to and it will be eventually encrypted with the DH of the contact's // identity key late final Uint8List signedContactInvitationBytes; + late final TypedKey contactRequestInboxKey; await (await pool.createRecord( debugName: 'ContactInvitationListCubit::createInvitation::' 'LocalConversation', @@ -119,6 +120,9 @@ class ContactInvitationListCubit ]), crypto: const VeilidCryptoPublic())) .deleteScope((contactRequestInbox) async { + // Keep the contact request inbox key + contactRequestInboxKey = contactRequestInbox.key; + // Store ContactRequest in owner subkey await contactRequestInbox.eventualWriteProtobuf(creq); // Store an empty invitation response @@ -158,7 +162,7 @@ class ContactInvitationListCubit }); }); - return signedContactInvitationBytes; + return (signedContactInvitationBytes, contactRequestInboxKey); } Future deleteInvitation( diff --git a/lib/contact_invitation/cubits/invitation_generator_cubit.dart b/lib/contact_invitation/cubits/invitation_generator_cubit.dart index cd785fa..5c0fa15 100644 --- a/lib/contact_invitation/cubits/invitation_generator_cubit.dart +++ b/lib/contact_invitation/cubits/invitation_generator_cubit.dart @@ -1,8 +1,9 @@ import 'dart:typed_data'; import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; +import 'package:veilid_support/veilid_support.dart'; -class InvitationGeneratorCubit extends FutureCubit { +class InvitationGeneratorCubit extends FutureCubit<(Uint8List, TypedKey)> { InvitationGeneratorCubit(super.fut); InvitationGeneratorCubit.value(super.v) : super.value(); } 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 d6b089d..97e1c76 100644 --- a/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart @@ -1,9 +1,11 @@ import 'package:async_tools/async_tools.dart'; import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; +import 'package:flutter_translate/flutter_translate.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../contacts/contacts.dart'; +import '../../notifications/notifications.dart'; import '../../proto/proto.dart' as proto; import 'cubits.dart'; @@ -22,11 +24,13 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit(); } diff --git a/lib/contact_invitation/views/contact_invitation_display.dart b/lib/contact_invitation/views/contact_invitation_display.dart index 9c0f688..d816fc3 100644 --- a/lib/contact_invitation/views/contact_invitation_display.dart +++ b/lib/contact_invitation/views/contact_invitation_display.dart @@ -7,19 +7,22 @@ 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:provider/provider.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:veilid_support/veilid_support.dart'; +import '../../notifications/notifications.dart'; +import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; import '../contact_invitation.dart'; class ContactInvitationDisplayDialog extends StatelessWidget { const ContactInvitationDisplayDialog._({ - required this.modalContext, + required this.locator, required this.message, }); - final BuildContext modalContext; + final Locator locator; final String message; @override @@ -27,7 +30,7 @@ class ContactInvitationDisplayDialog extends StatelessWidget { super.debugFillProperties(properties); properties ..add(StringProperty('message', message)) - ..add(DiagnosticsProperty('modalContext', modalContext)); + ..add(DiagnosticsProperty('locator', locator)); } String makeTextInvite(String message, Uint8List data) { @@ -48,72 +51,87 @@ class ContactInvitationDisplayDialog extends StatelessWidget { final textTheme = theme.textTheme; final scaleConfig = theme.extension()!; - final signedContactInvitationBytesV = - context.watch().state; + final generatorOutputV = context.watch().state; final cardsize = min(MediaQuery.of(context).size.shortestSide - 48.0, 400); - return PopControl( - dismissible: !signedContactInvitationBytesV.isLoading, - child: Dialog( - shape: RoundedRectangleBorder( - side: const BorderSide(width: 2), - borderRadius: - BorderRadius.circular(16 * scaleConfig.borderRadiusScale)), - backgroundColor: Colors.white, - child: ConstrainedBox( - constraints: BoxConstraints( - minWidth: cardsize, - maxWidth: cardsize, - minHeight: cardsize, - maxHeight: cardsize), - child: signedContactInvitationBytesV.when( - loading: buildProgressIndicator, - data: (data) => Column(children: [ - FittedBox( - child: Text( - translate( - 'create_invitation_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(message, - softWrap: true, - style: textTheme.labelLarge! - .copyWith(color: Colors.black)) - .paddingAll(8), - ElevatedButton.icon( - icon: const Icon(Icons.copy), - style: ElevatedButton.styleFrom( - foregroundColor: Colors.black, - backgroundColor: Colors.white, - side: const BorderSide()), - label: Text(translate( - 'create_invitation_dialog.copy_invitation')), - onPressed: () async { - showInfoToast( - context, - translate( - 'create_invitation_dialog.invitation_copied')); - await Clipboard.setData(ClipboardData( - text: makeTextInvite(message, data))); - }, - ).paddingAll(16), - ]), - error: errorPage)))); + return BlocListener( + bloc: locator(), + listener: (context, state) { + final listState = state.state.asData?.value; + final data = generatorOutputV.asData?.value; + + if (listState != null && data != null) { + final idx = listState.indexWhere((x) => + x.value.contactRequestInbox.recordKey.toVeilid() == data.$2); + if (idx == -1) { + // This invitation is gone, close it + Navigator.pop(context); + } + } + }, + child: PopControl( + dismissible: !generatorOutputV.isLoading, + child: Dialog( + shape: RoundedRectangleBorder( + side: const BorderSide(width: 2), + borderRadius: BorderRadius.circular( + 16 * scaleConfig.borderRadiusScale)), + backgroundColor: Colors.white, + child: ConstrainedBox( + constraints: BoxConstraints( + minWidth: cardsize, + maxWidth: cardsize, + minHeight: cardsize, + maxHeight: cardsize), + child: generatorOutputV.when( + loading: buildProgressIndicator, + data: (data) => Column(children: [ + FittedBox( + child: Text( + translate('create_invitation_dialog' + '.contact_invitation'), + style: textTheme.headlineSmall! + .copyWith(color: Colors.black))) + .paddingAll(8), + FittedBox( + child: QrImageView.withQr( + size: 300, + qr: QrCode.fromUint8List( + data: data.$1, + errorCorrectLevel: + QrErrorCorrectLevel.L))) + .expanded(), + Text(message, + softWrap: true, + style: textTheme.labelLarge! + .copyWith(color: Colors.black)) + .paddingAll(8), + ElevatedButton.icon( + icon: const Icon(Icons.copy), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.black, + backgroundColor: Colors.white, + side: const BorderSide()), + label: Text(translate( + 'create_invitation_dialog.copy_invitation')), + onPressed: () async { + context.read().info( + text: translate('create_invitation_dialog' + '.invitation_copied')); + await Clipboard.setData(ClipboardData( + text: makeTextInvite(message, data.$1))); + }, + ).paddingAll(16), + ]), + error: errorPage))))); } static Future show( {required BuildContext context, + required Locator locator, required InvitationGeneratorCubit Function(BuildContext) create, required String message}) async { await showPopControlDialog( @@ -121,7 +139,7 @@ class ContactInvitationDisplayDialog extends StatelessWidget { builder: (context) => BlocProvider( create: create, child: ContactInvitationDisplayDialog._( - modalContext: context, + locator: locator, 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 c2a93c7..8fccc8a 100644 --- a/lib/contact_invitation/views/contact_invitation_item_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_item_widget.dart @@ -52,9 +52,13 @@ class ContactInvitationItemWidget extends StatelessWidget { } await ContactInvitationDisplayDialog.show( context: context, + locator: context.read, message: contactInvitationRecord.message, - create: (context) => InvitationGeneratorCubit.value( - Uint8List.fromList(contactInvitationRecord.invitation))); + create: (context) => InvitationGeneratorCubit.value(( + Uint8List.fromList(contactInvitationRecord.invitation), + contactInvitationRecord.contactRequestInbox.recordKey + .toVeilid() + ))); }, endActions: [ SliderTileAction( diff --git a/lib/contact_invitation/views/create_invitation_dialog.dart b/lib/contact_invitation/views/create_invitation_dialog.dart index 06503a8..9117d03 100644 --- a/lib/contact_invitation/views/create_invitation_dialog.dart +++ b/lib/contact_invitation/views/create_invitation_dialog.dart @@ -5,16 +5,17 @@ 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:provider/provider.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; +import '../../notifications/notifications.dart'; import '../../theme/theme.dart'; import '../contact_invitation.dart'; class CreateInvitationDialog extends StatefulWidget { - const CreateInvitationDialog._({required this.modalContext}); + const CreateInvitationDialog._({required this.locator}); @override CreateInvitationDialogState createState() => CreateInvitationDialogState(); @@ -23,16 +24,15 @@ class CreateInvitationDialog extends StatefulWidget { await StyledDialog.show( context: context, title: translate('create_invitation_dialog.title'), - child: CreateInvitationDialog._(modalContext: context)); + child: CreateInvitationDialog._(locator: context.read)); } - final BuildContext modalContext; + final Locator locator; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties - .add(DiagnosticsProperty('modalContext', modalContext)); + properties.add(DiagnosticsProperty('locator', locator)); } } @@ -86,8 +86,8 @@ class CreateInvitationDialogState extends State { if (!mounted) { return; } - showErrorToast( - context, translate('create_invitation_dialog.pin_does_not_match')); + context.read().error( + text: translate('create_invitation_dialog.pin_does_not_match')); setState(() { _encryptionKeyType = EncryptionKeyType.none; _encryptionKey = ''; @@ -124,8 +124,8 @@ class CreateInvitationDialogState extends State { if (!mounted) { return; } - showErrorToast(context, - translate('create_invitation_dialog.password_does_not_match')); + context.read().error( + text: translate('create_invitation_dialog.password_does_not_match')); setState(() { _encryptionKeyType = EncryptionKeyType.none; _encryptionKey = ''; @@ -138,13 +138,9 @@ class CreateInvitationDialogState extends State { // Start generation final contactInvitationListCubit = - widget.modalContext.read(); - final profile = widget.modalContext - .read() - .state - .asData - ?.value - .profile; + widget.locator(); + final profile = + widget.locator().state.asData?.value.profile; if (profile == null) { return; } @@ -158,6 +154,7 @@ class CreateInvitationDialogState extends State { await ContactInvitationDisplayDialog.show( context: context, + locator: widget.locator, message: _messageTextController.text, create: (context) => InvitationGeneratorCubit(generator)); diff --git a/lib/contact_invitation/views/invitation_dialog.dart b/lib/contact_invitation/views/invitation_dialog.dart index a8afd70..5e1ece6 100644 --- a/lib/contact_invitation/views/invitation_dialog.dart +++ b/lib/contact_invitation/views/invitation_dialog.dart @@ -9,6 +9,7 @@ import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../contacts/contacts.dart'; +import '../../notifications/notifications.dart'; import '../../theme/theme.dart'; import '../../tools/tools.dart'; import '../contact_invitation.dart'; @@ -102,7 +103,9 @@ class InvitationDialogState extends State { } } else { if (mounted) { - showErrorToast(context, 'invitation_dialog.failed_to_accept'); + context + .read() + .error(text: 'invitation_dialog.failed_to_accept'); } } } @@ -124,7 +127,9 @@ class InvitationDialogState extends State { // do nothing right now } else { if (mounted) { - showErrorToast(context, 'invitation_dialog.failed_to_reject'); + context + .read() + .error(text: 'invitation_dialog.failed_to_reject'); } } } @@ -218,7 +223,7 @@ class InvitationDialogState extends State { errorText = translate('invitation_dialog.invalid_password'); } if (mounted) { - showErrorToast(context, errorText); + context.read().error(text: errorText); } setState(() { _isValidating = false; @@ -233,7 +238,7 @@ class InvitationDialogState extends State { errorText = translate('invitation_dialog.invalid_invitation'); } if (mounted) { - showErrorToast(context, errorText); + context.read().error(text: errorText); } setState(() { _isValidating = false; diff --git a/lib/contact_invitation/views/scan_invitation_dialog.dart b/lib/contact_invitation/views/scan_invitation_dialog.dart index 9f7a878..645640e 100644 --- a/lib/contact_invitation/views/scan_invitation_dialog.dart +++ b/lib/contact_invitation/views/scan_invitation_dialog.dart @@ -12,6 +12,7 @@ import 'package:pasteboard/pasteboard.dart'; import 'package:provider/provider.dart'; import 'package:zxing2/qrcode.dart'; +import '../../notifications/notifications.dart'; import '../../theme/theme.dart'; import 'invitation_dialog.dart'; @@ -269,13 +270,18 @@ class ScanInvitationDialogState extends State { )); } on MobileScannerException catch (e) { if (e.errorCode == MobileScannerErrorCode.permissionDenied) { - showErrorToast( - context, translate('scan_invitation_dialog.permission_error')); + context + .read() + .error(text: translate('scan_invitation_dialog.permission_error')); } else { - showErrorToast(context, translate('scan_invitation_dialog.error')); + context + .read() + .error(text: translate('scan_invitation_dialog.error')); } } on Exception catch (_) { - showErrorToast(context, translate('scan_invitation_dialog.error')); + context + .read() + .error(text: translate('scan_invitation_dialog.error')); } return null; @@ -285,8 +291,9 @@ class ScanInvitationDialogState extends State { final imageBytes = await Pasteboard.image; if (imageBytes == null) { if (context.mounted) { - showErrorToast( - context, translate('scan_invitation_dialog.not_an_image')); + context + .read() + .error(text: translate('scan_invitation_dialog.not_an_image')); } return null; } @@ -294,8 +301,8 @@ class ScanInvitationDialogState extends State { final image = img.decodeImage(imageBytes); if (image == null) { if (context.mounted) { - showErrorToast(context, - translate('scan_invitation_dialog.could_not_decode_image')); + context.read().error( + text: translate('scan_invitation_dialog.could_not_decode_image')); } return null; } @@ -319,8 +326,8 @@ class ScanInvitationDialogState extends State { return Uint8List.fromList(segs[0].toList()); } on Exception catch (_) { if (context.mounted) { - showErrorToast( - context, translate('scan_invitation_dialog.not_a_valid_qr_code')); + context.read().error( + text: translate('scan_invitation_dialog.not_a_valid_qr_code')); } return null; } diff --git a/lib/layout/splash.dart b/lib/layout/splash.dart index 97d4a70..c3af797 100644 --- a/lib/layout/splash.dart +++ b/lib/layout/splash.dart @@ -1,9 +1,5 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:flutter_translate/flutter_translate.dart'; -import 'package:quickalert/quickalert.dart'; import 'package:radix_colors/radix_colors.dart'; import '../tools/tools.dart'; diff --git a/lib/notifications/cubits/notifications_cubit.dart b/lib/notifications/cubits/notifications_cubit.dart new file mode 100644 index 0000000..769e64f --- /dev/null +++ b/lib/notifications/cubits/notifications_cubit.dart @@ -0,0 +1,26 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../notifications.dart'; + +class NotificationsCubit extends Cubit { + NotificationsCubit(super.initialState); + + void info({required String text, String? title}) { + emit(state.copyWith( + queue: state.queue.add(NotificationItem( + type: NotificationType.info, text: text, title: title)))); + } + + void error({required String text, String? title}) { + emit(state.copyWith( + queue: state.queue.add(NotificationItem( + type: NotificationType.info, text: text, title: title)))); + } + + IList popAll() { + final out = state.queue; + emit(state.copyWith(queue: state.queue.clear())); + return out; + } +} diff --git a/lib/notifications/models/notifications_state.dart b/lib/notifications/models/notifications_state.dart new file mode 100644 index 0000000..d001ce2 --- /dev/null +++ b/lib/notifications/models/notifications_state.dart @@ -0,0 +1,23 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'notifications_state.freezed.dart'; + +enum NotificationType { + info, + error, +} + +@freezed +class NotificationItem with _$NotificationItem { + const factory NotificationItem( + {required NotificationType type, + required String text, + String? title}) = _NotificationItem; +} + +@freezed +class NotificationsState with _$NotificationsState { + const factory NotificationsState({required IList queue}) = + _NotificationsState; +} diff --git a/lib/notifications/models/notifications_state.freezed.dart b/lib/notifications/models/notifications_state.freezed.dart new file mode 100644 index 0000000..90893e6 --- /dev/null +++ b/lib/notifications/models/notifications_state.freezed.dart @@ -0,0 +1,290 @@ +// 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 'notifications_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#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$NotificationItem { + NotificationType get type => throw _privateConstructorUsedError; + String get text => throw _privateConstructorUsedError; + String? get title => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $NotificationItemCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $NotificationItemCopyWith<$Res> { + factory $NotificationItemCopyWith( + NotificationItem value, $Res Function(NotificationItem) then) = + _$NotificationItemCopyWithImpl<$Res, NotificationItem>; + @useResult + $Res call({NotificationType type, String text, String? title}); +} + +/// @nodoc +class _$NotificationItemCopyWithImpl<$Res, $Val extends NotificationItem> + implements $NotificationItemCopyWith<$Res> { + _$NotificationItemCopyWithImpl(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? type = null, + Object? text = null, + Object? title = freezed, + }) { + return _then(_value.copyWith( + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as NotificationType, + text: null == text + ? _value.text + : text // ignore: cast_nullable_to_non_nullable + as String, + title: freezed == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$NotificationItemImplCopyWith<$Res> + implements $NotificationItemCopyWith<$Res> { + factory _$$NotificationItemImplCopyWith(_$NotificationItemImpl value, + $Res Function(_$NotificationItemImpl) then) = + __$$NotificationItemImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({NotificationType type, String text, String? title}); +} + +/// @nodoc +class __$$NotificationItemImplCopyWithImpl<$Res> + extends _$NotificationItemCopyWithImpl<$Res, _$NotificationItemImpl> + implements _$$NotificationItemImplCopyWith<$Res> { + __$$NotificationItemImplCopyWithImpl(_$NotificationItemImpl _value, + $Res Function(_$NotificationItemImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? type = null, + Object? text = null, + Object? title = freezed, + }) { + return _then(_$NotificationItemImpl( + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as NotificationType, + text: null == text + ? _value.text + : text // ignore: cast_nullable_to_non_nullable + as String, + title: freezed == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc + +class _$NotificationItemImpl implements _NotificationItem { + const _$NotificationItemImpl( + {required this.type, required this.text, this.title}); + + @override + final NotificationType type; + @override + final String text; + @override + final String? title; + + @override + String toString() { + return 'NotificationItem(type: $type, text: $text, title: $title)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$NotificationItemImpl && + (identical(other.type, type) || other.type == type) && + (identical(other.text, text) || other.text == text) && + (identical(other.title, title) || other.title == title)); + } + + @override + int get hashCode => Object.hash(runtimeType, type, text, title); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$NotificationItemImplCopyWith<_$NotificationItemImpl> get copyWith => + __$$NotificationItemImplCopyWithImpl<_$NotificationItemImpl>( + this, _$identity); +} + +abstract class _NotificationItem implements NotificationItem { + const factory _NotificationItem( + {required final NotificationType type, + required final String text, + final String? title}) = _$NotificationItemImpl; + + @override + NotificationType get type; + @override + String get text; + @override + String? get title; + @override + @JsonKey(ignore: true) + _$$NotificationItemImplCopyWith<_$NotificationItemImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$NotificationsState { + IList get queue => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $NotificationsStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $NotificationsStateCopyWith<$Res> { + factory $NotificationsStateCopyWith( + NotificationsState value, $Res Function(NotificationsState) then) = + _$NotificationsStateCopyWithImpl<$Res, NotificationsState>; + @useResult + $Res call({IList queue}); +} + +/// @nodoc +class _$NotificationsStateCopyWithImpl<$Res, $Val extends NotificationsState> + implements $NotificationsStateCopyWith<$Res> { + _$NotificationsStateCopyWithImpl(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? queue = null, + }) { + return _then(_value.copyWith( + queue: null == queue + ? _value.queue + : queue // ignore: cast_nullable_to_non_nullable + as IList, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$NotificationsStateImplCopyWith<$Res> + implements $NotificationsStateCopyWith<$Res> { + factory _$$NotificationsStateImplCopyWith(_$NotificationsStateImpl value, + $Res Function(_$NotificationsStateImpl) then) = + __$$NotificationsStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({IList queue}); +} + +/// @nodoc +class __$$NotificationsStateImplCopyWithImpl<$Res> + extends _$NotificationsStateCopyWithImpl<$Res, _$NotificationsStateImpl> + implements _$$NotificationsStateImplCopyWith<$Res> { + __$$NotificationsStateImplCopyWithImpl(_$NotificationsStateImpl _value, + $Res Function(_$NotificationsStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? queue = null, + }) { + return _then(_$NotificationsStateImpl( + queue: null == queue + ? _value.queue + : queue // ignore: cast_nullable_to_non_nullable + as IList, + )); + } +} + +/// @nodoc + +class _$NotificationsStateImpl implements _NotificationsState { + const _$NotificationsStateImpl({required this.queue}); + + @override + final IList queue; + + @override + String toString() { + return 'NotificationsState(queue: $queue)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$NotificationsStateImpl && + const DeepCollectionEquality().equals(other.queue, queue)); + } + + @override + int get hashCode => + Object.hash(runtimeType, const DeepCollectionEquality().hash(queue)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$NotificationsStateImplCopyWith<_$NotificationsStateImpl> get copyWith => + __$$NotificationsStateImplCopyWithImpl<_$NotificationsStateImpl>( + this, _$identity); +} + +abstract class _NotificationsState implements NotificationsState { + const factory _NotificationsState( + {required final IList queue}) = + _$NotificationsStateImpl; + + @override + IList get queue; + @override + @JsonKey(ignore: true) + _$$NotificationsStateImplCopyWith<_$NotificationsStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/notifications/notifications.dart b/lib/notifications/notifications.dart new file mode 100644 index 0000000..5841483 --- /dev/null +++ b/lib/notifications/notifications.dart @@ -0,0 +1,3 @@ +export 'cubits/notifications_cubit.dart'; +export 'models/notifications_state.dart'; +export 'views/notifications_widget.dart'; diff --git a/lib/notifications/views/notifications_widget.dart b/lib/notifications/views/notifications_widget.dart new file mode 100644 index 0000000..246a570 --- /dev/null +++ b/lib/notifications/views/notifications_widget.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:motion_toast/motion_toast.dart'; + +import '../../theme/theme.dart'; +import '../notifications.dart'; + +class NotificationsWidget extends StatelessWidget { + const NotificationsWidget({required Widget child, super.key}) + : _child = child; + + //////////////////////////////////////////////////////////////////////////// + // Public API + + @override + Widget build(BuildContext context) { + final notificationsCubit = context.read(); + + return BlocListener( + bloc: notificationsCubit, + listener: (context, state) { + if (state.queue.isNotEmpty) { + final queue = notificationsCubit.popAll(); + for (final notificationItem in queue) { + switch (notificationItem.type) { + case NotificationType.info: + _info( + context: context, + text: notificationItem.text, + title: notificationItem.title); + case NotificationType.error: + _error( + context: context, + text: notificationItem.text, + title: notificationItem.title); + } + } + } + }, + child: _child); + } + + //////////////////////////////////////////////////////////////////////////// + // Private Implementation + + void _info( + {required BuildContext context, required String text, String? title}) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + + MotionToast( + title: title != null ? Text(title) : null, + description: Text(text), + constraints: BoxConstraints.loose(const Size(400, 100)), + contentPadding: const EdgeInsets.all(16), + primaryColor: scale.tertiaryScale.elementBackground, + secondaryColor: scale.tertiaryScale.calloutBackground, + borderRadius: 12 * scaleConfig.borderRadiusScale, + toastDuration: const Duration(seconds: 2), + animationDuration: const Duration(milliseconds: 500), + displayBorder: scaleConfig.useVisualIndicators, + icon: Icons.info, + ).show(context); + } + + void _error( + {required BuildContext context, required String text, String? title}) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + + MotionToast( + title: title != null ? Text(title) : null, + description: Text(text), + constraints: BoxConstraints.loose(const Size(400, 100)), + contentPadding: const EdgeInsets.all(16), + primaryColor: scale.errorScale.elementBackground, + secondaryColor: scale.errorScale.calloutBackground, + borderRadius: 12 * scaleConfig.borderRadiusScale, + toastDuration: const Duration(seconds: 4), + animationDuration: const Duration(milliseconds: 1000), + displayBorder: scaleConfig.useVisualIndicators, + icon: Icons.error, + ).show(context); + } + + //////////////////////////////////////////////////////////////////////////// + + final Widget _child; +} diff --git a/lib/router/cubit/router_cubit.dart b/lib/router/cubits/router_cubit.dart similarity index 65% rename from lib/router/cubit/router_cubit.dart rename to lib/router/cubits/router_cubit.dart index 9e9bfe6..95f2bf7 100644 --- a/lib/router/cubit/router_cubit.dart +++ b/lib/router/cubits/router_cubit.dart @@ -15,6 +15,7 @@ import '../../proto/proto.dart' as proto; import '../../settings/settings.dart'; import '../../tools/tools.dart'; import '../../veilid_processor/views/developer.dart'; +import '../views/router_shell.dart'; part 'router_cubit.freezed.dart'; part 'router_cubit.g.dart'; @@ -58,42 +59,47 @@ class RouterCubit extends Cubit { /// Our application routes List get routes => [ - GoRoute( - path: '/', - builder: (context, state) => const HomeScreen(), - ), - GoRoute( - path: '/edit_account', - builder: (context, state) { - final extra = state.extra! as List; - return EditAccountPage( - superIdentityRecordKey: extra[0]! as TypedKey, - existingProfile: extra[1]! as proto.Profile, - accountRecord: extra[2]! as OwnedDHTRecordPointer, - ); - }, - ), - GoRoute( - path: '/new_account', - builder: (context, state) => const NewAccountPage(), - ), - GoRoute( - path: '/new_account/recovery_key', - builder: (context, state) { - final extra = state.extra! as List; + ShellRoute( + builder: (context, state, child) => RouterShell(child: child), + routes: [ + GoRoute( + path: '/', + builder: (context, state) => const HomeScreen(), + ), + GoRoute( + path: '/edit_account', + builder: (context, state) { + final extra = state.extra! as List; + return EditAccountPage( + superIdentityRecordKey: extra[0]! as TypedKey, + existingProfile: extra[1]! as proto.Profile, + accountRecord: extra[2]! as OwnedDHTRecordPointer, + ); + }, + ), + GoRoute( + path: '/new_account', + builder: (context, state) => const NewAccountPage(), + ), + GoRoute( + path: '/new_account/recovery_key', + builder: (context, state) { + final extra = state.extra! as List; - return ShowRecoveryKeyPage( - writableSuperIdentity: extra[0]! as WritableSuperIdentity, - name: extra[1]! as String); - }), - GoRoute( - path: '/settings', - builder: (context, state) => const SettingsPage(), - ), - GoRoute( - path: '/developer', - builder: (context, state) => const DeveloperPage(), - ) + return ShowRecoveryKeyPage( + writableSuperIdentity: + extra[0]! as WritableSuperIdentity, + name: extra[1]! as String); + }), + GoRoute( + path: '/settings', + builder: (context, state) => const SettingsPage(), + ), + GoRoute( + path: '/developer', + builder: (context, state) => const DeveloperPage(), + ) + ]) ]; /// Redirects when our state changes diff --git a/lib/router/cubit/router_cubit.freezed.dart b/lib/router/cubits/router_cubit.freezed.dart similarity index 100% rename from lib/router/cubit/router_cubit.freezed.dart rename to lib/router/cubits/router_cubit.freezed.dart diff --git a/lib/router/cubit/router_cubit.g.dart b/lib/router/cubits/router_cubit.g.dart similarity index 100% rename from lib/router/cubit/router_cubit.g.dart rename to lib/router/cubits/router_cubit.g.dart diff --git a/lib/router/router.dart b/lib/router/router.dart index 1867a19..4fd9dc5 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -1 +1,2 @@ -export 'cubit/router_cubit.dart'; +export 'cubits/router_cubit.dart'; +export 'views/router_shell.dart'; diff --git a/lib/router/views/router_shell.dart b/lib/router/views/router_shell.dart new file mode 100644 index 0000000..f2f035b --- /dev/null +++ b/lib/router/views/router_shell.dart @@ -0,0 +1,12 @@ +import 'package:flutter/widgets.dart'; + +import '../../notifications/notifications.dart'; + +class RouterShell extends StatelessWidget { + const RouterShell({required Widget child, super.key}) : _child = child; + + @override + Widget build(BuildContext context) => NotificationsWidget(child: _child); + + final Widget _child; +} diff --git a/lib/theme/views/widget_helpers.dart b/lib/theme/views/widget_helpers.dart index 7f9e57f..91ae11a 100644 --- a/lib/theme/views/widget_helpers.dart +++ b/lib/theme/views/widget_helpers.dart @@ -8,7 +8,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_sticky_header/flutter_sticky_header.dart'; -import 'package:motion_toast/motion_toast.dart'; import 'package:quickalert/quickalert.dart'; import 'package:sliver_expandable/sliver_expandable.dart'; @@ -132,46 +131,6 @@ Future showErrorModal( ); } -void showErrorToast(BuildContext context, String message) { - final theme = Theme.of(context); - final scale = theme.extension()!; - final scaleConfig = theme.extension()!; - - MotionToast( - //title: Text(translate('toast.error')), - description: Text(message), - constraints: BoxConstraints.loose(const Size(400, 100)), - contentPadding: const EdgeInsets.all(16), - primaryColor: scale.errorScale.elementBackground, - secondaryColor: scale.errorScale.calloutBackground, - borderRadius: 12 * scaleConfig.borderRadiusScale, - toastDuration: const Duration(seconds: 4), - animationDuration: const Duration(milliseconds: 1000), - displayBorder: scaleConfig.useVisualIndicators, - icon: Icons.error, - ).show(context); -} - -void showInfoToast(BuildContext context, String message) { - final theme = Theme.of(context); - final scale = theme.extension()!; - final scaleConfig = theme.extension()!; - - MotionToast( - //title: Text(translate('toast.info')), - description: Text(message), - constraints: BoxConstraints.loose(const Size(400, 100)), - contentPadding: const EdgeInsets.all(16), - primaryColor: scale.tertiaryScale.elementBackground, - secondaryColor: scale.tertiaryScale.calloutBackground, - borderRadius: 12 * scaleConfig.borderRadiusScale, - toastDuration: const Duration(seconds: 2), - animationDuration: const Duration(milliseconds: 500), - displayBorder: scaleConfig.useVisualIndicators, - icon: Icons.info, - ).show(context); -} - SliverAppBar styledSliverAppBar( {required BuildContext context, required String title, Color? titleColor}) { final theme = Theme.of(context); diff --git a/lib/veilid_processor/views/developer.dart b/lib/veilid_processor/views/developer.dart index 0078935..f4fe836 100644 --- a/lib/veilid_processor/views/developer.dart +++ b/lib/veilid_processor/views/developer.dart @@ -8,6 +8,7 @@ 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_bloc/flutter_bloc.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:go_router/go_router.dart'; import 'package:loggy/loggy.dart'; @@ -16,6 +17,7 @@ import 'package:veilid_support/veilid_support.dart'; import 'package:xterm/xterm.dart'; import '../../layout/layout.dart'; +import '../../notifications/notifications.dart'; import '../../theme/theme.dart'; import '../../tools/tools.dart'; import 'history_text_editing_controller.dart'; @@ -133,7 +135,9 @@ class _DeveloperPageState extends State { Future clear(BuildContext context) async { globalDebugTerminal.buffer.clear(); if (context.mounted) { - showInfoToast(context, translate('developer.cleared')); + context + .read() + .info(text: translate('developer.cleared')); } } @@ -144,7 +148,9 @@ class _DeveloperPageState extends State { _terminalController.clearSelection(); await Clipboard.setData(ClipboardData(text: text)); if (context.mounted) { - showInfoToast(context, translate('developer.copied')); + context + .read() + .info(text: translate('developer.copied')); } } } @@ -153,7 +159,9 @@ class _DeveloperPageState extends State { final text = globalDebugTerminal.buffer.getText(); await Clipboard.setData(ClipboardData(text: text)); if (context.mounted) { - showInfoToast(context, translate('developer.copied_all')); + context + .read() + .info(text: translate('developer.copied_all')); } } diff --git a/packages/veilid_support/lib/identity_support/identity_instance.freezed.dart b/packages/veilid_support/lib/identity_support/identity_instance.freezed.dart index 4d6c4ad..a7c3e78 100644 --- a/packages/veilid_support/lib/identity_support/identity_instance.freezed.dart +++ b/packages/veilid_support/lib/identity_support/identity_instance.freezed.dart @@ -25,7 +25,9 @@ mixin _$IdentityInstance { throw _privateConstructorUsedError; // Public key of identity instance FixedEncodedString43 get publicKey => throw _privateConstructorUsedError; // Secret key of identity instance -// Encrypted with DH(publicKey, SuperIdentity.secret) with appended salt +// Encrypted with appended salt, key is DeriveSharedSecret( +// password = SuperIdentity.secret, +// salt = publicKey) // Used to recover accounts without generating a new instance @Uint8ListJsonConverter() Uint8List get encryptedSecretKey => @@ -179,7 +181,9 @@ class _$IdentityInstanceImpl extends _IdentityInstance { @override final FixedEncodedString43 publicKey; // Secret key of identity instance -// Encrypted with DH(publicKey, SuperIdentity.secret) with appended salt +// Encrypted with appended salt, key is DeriveSharedSecret( +// password = SuperIdentity.secret, +// salt = publicKey) // Used to recover accounts without generating a new instance @override @Uint8ListJsonConverter() @@ -257,7 +261,9 @@ abstract class _IdentityInstance extends IdentityInstance { @override // Public key of identity instance FixedEncodedString43 get publicKey; @override // Secret key of identity instance -// Encrypted with DH(publicKey, SuperIdentity.secret) with appended salt +// Encrypted with appended salt, key is DeriveSharedSecret( +// password = SuperIdentity.secret, +// salt = publicKey) // Used to recover accounts without generating a new instance @Uint8ListJsonConverter() Uint8List get encryptedSecretKey; From d962f987860b84014c84e207c7b6edca79750fa7 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 26 Jul 2024 16:51:03 -0400 Subject: [PATCH 166/270] settings / preferences upate --- assets/i18n/en.json | 21 +- assets/sounds/badeep.wav | Bin 0 -> 27712 bytes assets/sounds/beepbadeep.wav | Bin 0 -> 61680 bytes assets/sounds/bonk.wav | Bin 0 -> 19856 bytes assets/sounds/boop.wav | Bin 0 -> 17988 bytes lib/app.dart | 2 +- lib/layout/home/home_screen.dart | 45 ++- lib/main.dart | 2 +- lib/notifications/models/models.dart | 2 + .../models/notifications_preference.dart | 69 ++++ .../notifications_preference.freezed.dart | 358 ++++++++++++++++++ .../models/notifications_preference.g.dart | 50 +++ lib/notifications/notifications.dart | 4 +- .../views/notifications_preferences.dart | 285 ++++++++++++++ lib/notifications/views/views.dart | 2 + lib/settings/models/preferences.dart | 26 +- lib/settings/models/preferences.freezed.dart | 183 +++++---- lib/settings/models/preferences.g.dart | 28 +- lib/settings/settings_page.dart | 3 + lib/theme/models/theme_preference.dart | 13 +- .../models/theme_preference.freezed.dart | 15 +- lib/theme/models/theme_preference.g.dart | 11 +- lib/theme/views/brightness_preferences.dart | 4 +- lib/theme/views/color_preferences.dart | 4 +- lib/theme/views/widget_helpers.dart | 8 + pubspec.yaml | 5 + 26 files changed, 1015 insertions(+), 125 deletions(-) create mode 100644 assets/sounds/badeep.wav create mode 100644 assets/sounds/beepbadeep.wav create mode 100644 assets/sounds/bonk.wav create mode 100644 assets/sounds/boop.wav create mode 100644 lib/notifications/models/models.dart create mode 100644 lib/notifications/models/notifications_preference.dart create mode 100644 lib/notifications/models/notifications_preference.freezed.dart create mode 100644 lib/notifications/models/notifications_preference.g.dart create mode 100644 lib/notifications/views/notifications_preferences.dart create mode 100644 lib/notifications/views/views.dart diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 7185fab..0fad430 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -219,7 +219,26 @@ "settings_page": { "titlebar": "Settings", "color_theme": "Color Theme", - "brightness_mode": "Brightness Mode" + "brightness_mode": "Brightness Mode", + "display_beta_warning": "Display beta warning on startup", + "none": "None", + "in_app": "In-app", + "push": "Push", + "in_app_or_push": "In-app or Push", + "enable_badge": "Enable icon 'badge' bubble", + "enable_notifications": "Enable notifications", + "message_notification_content": "Message notification content", + "invitation_accepted": "On invitation accept/reject", + "message_received": "On message received", + "message_sent": "On message sent", + "name_and_content": "Name and content", + "name_only": "Name only", + "nothing": "Nothing", + "bonk": "Bonk", + "boop": "Boop", + "badeep": "Badeep", + "beep_badeep": "Beep-Badeep", + "custom": "Custom" }, "developer": { "title": "Developer Logs", diff --git a/assets/sounds/badeep.wav b/assets/sounds/badeep.wav new file mode 100644 index 0000000000000000000000000000000000000000..475c2100cf3ab731bb890a9f83d583cbf33d3a02 GIT binary patch literal 27712 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

g&=ve&U*g@HwZ`Caq6e)cHz7-K)q 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 Ytu@+R+{@*3otInL+T-(HUu}DqFHwX?ZvX%Q literal 0 HcmV?d00001 diff --git a/assets/sounds/beepbadeep.wav b/assets/sounds/beepbadeep.wav new file mode 100644 index 0000000000000000000000000000000000000000..8577b125fa96257fef5878a36f7a8af0cd634682 GIT binary patch literal 61680 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 literal 0 HcmV?d00001 diff --git a/assets/sounds/bonk.wav b/assets/sounds/bonk.wav new file mode 100644 index 0000000000000000000000000000000000000000..3e9236c3d5f8b677c9b23983d5984b96ab1e864d GIT binary patch literal 19856 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

)!ot+&Eje)#i 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 T$1c|23LWVgpt`XdlrjDnt08bA literal 0 HcmV?d00001 diff --git a/assets/sounds/boop.wav b/assets/sounds/boop.wav new file mode 100644 index 0000000000000000000000000000000000000000..df284bc5d942faeab211c1fbd9b05a697aeae8c4 GIT binary patch literal 17988 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 literal 0 HcmV?d00001 diff --git a/lib/app.dart b/lib/app.dart index 5c47fa3..98e349a 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -43,7 +43,7 @@ class VeilidChatApp extends StatelessWidget { void _reloadTheme(BuildContext context) { log.info('Reloading theme'); final theme = - PreferencesRepository.instance.value.themePreferences.themeData(); + PreferencesRepository.instance.value.themePreference.themeData(); ThemeSwitcher.of(context).changeTheme(theme: theme); // Hack to reload translations diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index 8e3bf48..a941220 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -12,6 +12,7 @@ import 'package:url_launcher/url_launcher_string.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; +import '../../settings/settings.dart'; import '../../theme/theme.dart'; import 'drawer_menu/drawer_menu.dart'; import 'home_account_invalid.dart'; @@ -41,7 +42,17 @@ class HomeScreenState extends State .indexWhere((x) => x.superIdentity.recordKey == activeLocalAccount); final canClose = activeIndex != -1; - unawaited(_doBetaDialog(context)); + final displayBetaWarning = context + .read() + .state + .asData + ?.value + .notificationsPreference + .displayBetaWarning ?? + true; + if (displayBetaWarning) { + await _doBetaDialog(context); + } if (!canClose) { await _zoomDrawerController.open!(); @@ -51,10 +62,13 @@ class HomeScreenState extends State } Future _doBetaDialog(BuildContext context) async { + var displayBetaWarning = true; + await QuickAlert.show( - context: context, - title: translate('splash.beta_title'), - widget: RichText( + context: context, + title: translate('splash.beta_title'), + widget: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + RichText( textAlign: TextAlign.center, text: TextSpan( children: [ @@ -77,7 +91,28 @@ class HomeScreenState extends State ], ), ), - type: QuickAlertType.warning); + Row(mainAxisSize: MainAxisSize.min, children: [ + StatefulBuilder( + builder: (context, setState) => Checkbox.adaptive( + value: displayBetaWarning, + onChanged: (value) { + setState(() { + displayBetaWarning = value ?? true; + }); + }, + )), + Text(translate('settings_page.display_beta_warning'), + style: const TextStyle(color: Colors.black)), + ]), + ]), + type: QuickAlertType.warning, + ); + + final preferencesInstance = PreferencesRepository.instance; + await preferencesInstance.set(preferencesInstance.value.copyWith( + notificationsPreference: preferencesInstance + .value.notificationsPreference + .copyWith(displayBetaWarning: displayBetaWarning))); } Widget _buildAccountPage( diff --git a/lib/main.dart b/lib/main.dart index 4edaa5b..7193e06 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -36,7 +36,7 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); await PreferencesRepository.instance.init(); final initialThemeData = - PreferencesRepository.instance.value.themePreferences.themeData(); + PreferencesRepository.instance.value.themePreference.themeData(); // Manage window on desktop platforms await initializeWindowControl(); diff --git a/lib/notifications/models/models.dart b/lib/notifications/models/models.dart new file mode 100644 index 0000000..52f3c2b --- /dev/null +++ b/lib/notifications/models/models.dart @@ -0,0 +1,2 @@ +export 'notifications_preference.dart'; +export 'notifications_state.dart'; diff --git a/lib/notifications/models/notifications_preference.dart b/lib/notifications/models/notifications_preference.dart new file mode 100644 index 0000000..35385c6 --- /dev/null +++ b/lib/notifications/models/notifications_preference.dart @@ -0,0 +1,69 @@ +import 'package:change_case/change_case.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'notifications_preference.freezed.dart'; +part 'notifications_preference.g.dart'; + +@freezed +class NotificationsPreference with _$NotificationsPreference { + const factory NotificationsPreference({ + @Default(true) bool displayBetaWarning, + @Default(true) bool enableBadge, + @Default(true) bool enableNotifications, + @Default(MessageNotificationContent.nameAndContent) + MessageNotificationContent messageNotificationContent, + @Default(NotificationMode.inAppOrPush) + NotificationMode onInvitationAcceptedMode, + @Default(SoundEffect.beepBaDeep) SoundEffect onInvitationAcceptedSound, + @Default(NotificationMode.inAppOrPush) + NotificationMode onMessageReceivedMode, + @Default(SoundEffect.boop) SoundEffect onMessageReceivedSound, + @Default(SoundEffect.bonk) SoundEffect onMessageSentSound, + }) = _NotificationsPreference; + + factory NotificationsPreference.fromJson(dynamic json) => + _$NotificationsPreferenceFromJson(json as Map); + + static const NotificationsPreference defaults = NotificationsPreference(); +} + +enum NotificationMode { + none, + inApp, + push, + inAppOrPush; + + factory NotificationMode.fromJson(dynamic j) => + NotificationMode.values.byName((j as String).toCamelCase()); + String toJson() => name.toPascalCase(); + + static const NotificationMode defaults = NotificationMode.none; +} + +enum MessageNotificationContent { + nothing, + nameOnly, + nameAndContent; + + factory MessageNotificationContent.fromJson(dynamic j) => + MessageNotificationContent.values.byName((j as String).toCamelCase()); + String toJson() => name.toPascalCase(); + + static const MessageNotificationContent defaults = + MessageNotificationContent.nothing; +} + +enum SoundEffect { + none, + bonk, + boop, + baDeep, + beepBaDeep, + custom; + + factory SoundEffect.fromJson(dynamic j) => + SoundEffect.values.byName((j as String).toCamelCase()); + String toJson() => name.toPascalCase(); + + static const SoundEffect defaults = SoundEffect.none; +} diff --git a/lib/notifications/models/notifications_preference.freezed.dart b/lib/notifications/models/notifications_preference.freezed.dart new file mode 100644 index 0000000..b2cbc67 --- /dev/null +++ b/lib/notifications/models/notifications_preference.freezed.dart @@ -0,0 +1,358 @@ +// 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 'notifications_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#adding-getters-and-methods-to-our-models'); + +NotificationsPreference _$NotificationsPreferenceFromJson( + Map json) { + return _NotificationsPreference.fromJson(json); +} + +/// @nodoc +mixin _$NotificationsPreference { + bool get displayBetaWarning => throw _privateConstructorUsedError; + bool get enableBadge => throw _privateConstructorUsedError; + bool get enableNotifications => throw _privateConstructorUsedError; + MessageNotificationContent get messageNotificationContent => + throw _privateConstructorUsedError; + NotificationMode get onInvitationAcceptedMode => + throw _privateConstructorUsedError; + SoundEffect get onInvitationAcceptedSound => + throw _privateConstructorUsedError; + NotificationMode get onMessageReceivedMode => + throw _privateConstructorUsedError; + SoundEffect get onMessageReceivedSound => throw _privateConstructorUsedError; + SoundEffect get onMessageSentSound => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $NotificationsPreferenceCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $NotificationsPreferenceCopyWith<$Res> { + factory $NotificationsPreferenceCopyWith(NotificationsPreference value, + $Res Function(NotificationsPreference) then) = + _$NotificationsPreferenceCopyWithImpl<$Res, NotificationsPreference>; + @useResult + $Res call( + {bool displayBetaWarning, + bool enableBadge, + bool enableNotifications, + MessageNotificationContent messageNotificationContent, + NotificationMode onInvitationAcceptedMode, + SoundEffect onInvitationAcceptedSound, + NotificationMode onMessageReceivedMode, + SoundEffect onMessageReceivedSound, + SoundEffect onMessageSentSound}); +} + +/// @nodoc +class _$NotificationsPreferenceCopyWithImpl<$Res, + $Val extends NotificationsPreference> + implements $NotificationsPreferenceCopyWith<$Res> { + _$NotificationsPreferenceCopyWithImpl(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? displayBetaWarning = null, + Object? enableBadge = null, + Object? enableNotifications = null, + Object? messageNotificationContent = null, + Object? onInvitationAcceptedMode = null, + Object? onInvitationAcceptedSound = null, + Object? onMessageReceivedMode = null, + Object? onMessageReceivedSound = null, + Object? onMessageSentSound = null, + }) { + return _then(_value.copyWith( + displayBetaWarning: null == displayBetaWarning + ? _value.displayBetaWarning + : displayBetaWarning // ignore: cast_nullable_to_non_nullable + as bool, + enableBadge: null == enableBadge + ? _value.enableBadge + : enableBadge // ignore: cast_nullable_to_non_nullable + as bool, + enableNotifications: null == enableNotifications + ? _value.enableNotifications + : enableNotifications // ignore: cast_nullable_to_non_nullable + as bool, + messageNotificationContent: null == messageNotificationContent + ? _value.messageNotificationContent + : messageNotificationContent // ignore: cast_nullable_to_non_nullable + as MessageNotificationContent, + onInvitationAcceptedMode: null == onInvitationAcceptedMode + ? _value.onInvitationAcceptedMode + : onInvitationAcceptedMode // ignore: cast_nullable_to_non_nullable + as NotificationMode, + onInvitationAcceptedSound: null == onInvitationAcceptedSound + ? _value.onInvitationAcceptedSound + : onInvitationAcceptedSound // ignore: cast_nullable_to_non_nullable + as SoundEffect, + onMessageReceivedMode: null == onMessageReceivedMode + ? _value.onMessageReceivedMode + : onMessageReceivedMode // ignore: cast_nullable_to_non_nullable + as NotificationMode, + onMessageReceivedSound: null == onMessageReceivedSound + ? _value.onMessageReceivedSound + : onMessageReceivedSound // ignore: cast_nullable_to_non_nullable + as SoundEffect, + onMessageSentSound: null == onMessageSentSound + ? _value.onMessageSentSound + : onMessageSentSound // ignore: cast_nullable_to_non_nullable + as SoundEffect, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$NotificationsPreferenceImplCopyWith<$Res> + implements $NotificationsPreferenceCopyWith<$Res> { + factory _$$NotificationsPreferenceImplCopyWith( + _$NotificationsPreferenceImpl value, + $Res Function(_$NotificationsPreferenceImpl) then) = + __$$NotificationsPreferenceImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {bool displayBetaWarning, + bool enableBadge, + bool enableNotifications, + MessageNotificationContent messageNotificationContent, + NotificationMode onInvitationAcceptedMode, + SoundEffect onInvitationAcceptedSound, + NotificationMode onMessageReceivedMode, + SoundEffect onMessageReceivedSound, + SoundEffect onMessageSentSound}); +} + +/// @nodoc +class __$$NotificationsPreferenceImplCopyWithImpl<$Res> + extends _$NotificationsPreferenceCopyWithImpl<$Res, + _$NotificationsPreferenceImpl> + implements _$$NotificationsPreferenceImplCopyWith<$Res> { + __$$NotificationsPreferenceImplCopyWithImpl( + _$NotificationsPreferenceImpl _value, + $Res Function(_$NotificationsPreferenceImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? displayBetaWarning = null, + Object? enableBadge = null, + Object? enableNotifications = null, + Object? messageNotificationContent = null, + Object? onInvitationAcceptedMode = null, + Object? onInvitationAcceptedSound = null, + Object? onMessageReceivedMode = null, + Object? onMessageReceivedSound = null, + Object? onMessageSentSound = null, + }) { + return _then(_$NotificationsPreferenceImpl( + displayBetaWarning: null == displayBetaWarning + ? _value.displayBetaWarning + : displayBetaWarning // ignore: cast_nullable_to_non_nullable + as bool, + enableBadge: null == enableBadge + ? _value.enableBadge + : enableBadge // ignore: cast_nullable_to_non_nullable + as bool, + enableNotifications: null == enableNotifications + ? _value.enableNotifications + : enableNotifications // ignore: cast_nullable_to_non_nullable + as bool, + messageNotificationContent: null == messageNotificationContent + ? _value.messageNotificationContent + : messageNotificationContent // ignore: cast_nullable_to_non_nullable + as MessageNotificationContent, + onInvitationAcceptedMode: null == onInvitationAcceptedMode + ? _value.onInvitationAcceptedMode + : onInvitationAcceptedMode // ignore: cast_nullable_to_non_nullable + as NotificationMode, + onInvitationAcceptedSound: null == onInvitationAcceptedSound + ? _value.onInvitationAcceptedSound + : onInvitationAcceptedSound // ignore: cast_nullable_to_non_nullable + as SoundEffect, + onMessageReceivedMode: null == onMessageReceivedMode + ? _value.onMessageReceivedMode + : onMessageReceivedMode // ignore: cast_nullable_to_non_nullable + as NotificationMode, + onMessageReceivedSound: null == onMessageReceivedSound + ? _value.onMessageReceivedSound + : onMessageReceivedSound // ignore: cast_nullable_to_non_nullable + as SoundEffect, + onMessageSentSound: null == onMessageSentSound + ? _value.onMessageSentSound + : onMessageSentSound // ignore: cast_nullable_to_non_nullable + as SoundEffect, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$NotificationsPreferenceImpl implements _NotificationsPreference { + const _$NotificationsPreferenceImpl( + {this.displayBetaWarning = true, + this.enableBadge = true, + this.enableNotifications = true, + this.messageNotificationContent = + MessageNotificationContent.nameAndContent, + this.onInvitationAcceptedMode = NotificationMode.inAppOrPush, + this.onInvitationAcceptedSound = SoundEffect.beepBaDeep, + this.onMessageReceivedMode = NotificationMode.inAppOrPush, + this.onMessageReceivedSound = SoundEffect.boop, + this.onMessageSentSound = SoundEffect.bonk}); + + factory _$NotificationsPreferenceImpl.fromJson(Map json) => + _$$NotificationsPreferenceImplFromJson(json); + + @override + @JsonKey() + final bool displayBetaWarning; + @override + @JsonKey() + final bool enableBadge; + @override + @JsonKey() + final bool enableNotifications; + @override + @JsonKey() + final MessageNotificationContent messageNotificationContent; + @override + @JsonKey() + final NotificationMode onInvitationAcceptedMode; + @override + @JsonKey() + final SoundEffect onInvitationAcceptedSound; + @override + @JsonKey() + final NotificationMode onMessageReceivedMode; + @override + @JsonKey() + final SoundEffect onMessageReceivedSound; + @override + @JsonKey() + final SoundEffect onMessageSentSound; + + @override + String toString() { + return 'NotificationsPreference(displayBetaWarning: $displayBetaWarning, enableBadge: $enableBadge, enableNotifications: $enableNotifications, messageNotificationContent: $messageNotificationContent, onInvitationAcceptedMode: $onInvitationAcceptedMode, onInvitationAcceptedSound: $onInvitationAcceptedSound, onMessageReceivedMode: $onMessageReceivedMode, onMessageReceivedSound: $onMessageReceivedSound, onMessageSentSound: $onMessageSentSound)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$NotificationsPreferenceImpl && + (identical(other.displayBetaWarning, displayBetaWarning) || + other.displayBetaWarning == displayBetaWarning) && + (identical(other.enableBadge, enableBadge) || + other.enableBadge == enableBadge) && + (identical(other.enableNotifications, enableNotifications) || + other.enableNotifications == enableNotifications) && + (identical(other.messageNotificationContent, + messageNotificationContent) || + other.messageNotificationContent == + messageNotificationContent) && + (identical( + other.onInvitationAcceptedMode, onInvitationAcceptedMode) || + other.onInvitationAcceptedMode == onInvitationAcceptedMode) && + (identical(other.onInvitationAcceptedSound, + onInvitationAcceptedSound) || + other.onInvitationAcceptedSound == onInvitationAcceptedSound) && + (identical(other.onMessageReceivedMode, onMessageReceivedMode) || + other.onMessageReceivedMode == onMessageReceivedMode) && + (identical(other.onMessageReceivedSound, onMessageReceivedSound) || + other.onMessageReceivedSound == onMessageReceivedSound) && + (identical(other.onMessageSentSound, onMessageSentSound) || + other.onMessageSentSound == onMessageSentSound)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, + displayBetaWarning, + enableBadge, + enableNotifications, + messageNotificationContent, + onInvitationAcceptedMode, + onInvitationAcceptedSound, + onMessageReceivedMode, + onMessageReceivedSound, + onMessageSentSound); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$NotificationsPreferenceImplCopyWith<_$NotificationsPreferenceImpl> + get copyWith => __$$NotificationsPreferenceImplCopyWithImpl< + _$NotificationsPreferenceImpl>(this, _$identity); + + @override + Map toJson() { + return _$$NotificationsPreferenceImplToJson( + this, + ); + } +} + +abstract class _NotificationsPreference implements NotificationsPreference { + const factory _NotificationsPreference( + {final bool displayBetaWarning, + final bool enableBadge, + final bool enableNotifications, + final MessageNotificationContent messageNotificationContent, + final NotificationMode onInvitationAcceptedMode, + final SoundEffect onInvitationAcceptedSound, + final NotificationMode onMessageReceivedMode, + final SoundEffect onMessageReceivedSound, + final SoundEffect onMessageSentSound}) = _$NotificationsPreferenceImpl; + + factory _NotificationsPreference.fromJson(Map json) = + _$NotificationsPreferenceImpl.fromJson; + + @override + bool get displayBetaWarning; + @override + bool get enableBadge; + @override + bool get enableNotifications; + @override + MessageNotificationContent get messageNotificationContent; + @override + NotificationMode get onInvitationAcceptedMode; + @override + SoundEffect get onInvitationAcceptedSound; + @override + NotificationMode get onMessageReceivedMode; + @override + SoundEffect get onMessageReceivedSound; + @override + SoundEffect get onMessageSentSound; + @override + @JsonKey(ignore: true) + _$$NotificationsPreferenceImplCopyWith<_$NotificationsPreferenceImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/notifications/models/notifications_preference.g.dart b/lib/notifications/models/notifications_preference.g.dart new file mode 100644 index 0000000..d22b4b5 --- /dev/null +++ b/lib/notifications/models/notifications_preference.g.dart @@ -0,0 +1,50 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'notifications_preference.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$NotificationsPreferenceImpl _$$NotificationsPreferenceImplFromJson( + Map json) => + _$NotificationsPreferenceImpl( + displayBetaWarning: json['display_beta_warning'] as bool? ?? true, + enableBadge: json['enable_badge'] as bool? ?? true, + enableNotifications: json['enable_notifications'] as bool? ?? true, + messageNotificationContent: json['message_notification_content'] == null + ? MessageNotificationContent.nameAndContent + : MessageNotificationContent.fromJson( + json['message_notification_content']), + onInvitationAcceptedMode: json['on_invitation_accepted_mode'] == null + ? NotificationMode.inAppOrPush + : NotificationMode.fromJson(json['on_invitation_accepted_mode']), + onInvitationAcceptedSound: json['on_invitation_accepted_sound'] == null + ? SoundEffect.beepBaDeep + : SoundEffect.fromJson(json['on_invitation_accepted_sound']), + onMessageReceivedMode: json['on_message_received_mode'] == null + ? NotificationMode.inAppOrPush + : NotificationMode.fromJson(json['on_message_received_mode']), + onMessageReceivedSound: json['on_message_received_sound'] == null + ? SoundEffect.boop + : SoundEffect.fromJson(json['on_message_received_sound']), + onMessageSentSound: json['on_message_sent_sound'] == null + ? SoundEffect.bonk + : SoundEffect.fromJson(json['on_message_sent_sound']), + ); + +Map _$$NotificationsPreferenceImplToJson( + _$NotificationsPreferenceImpl instance) => + { + 'display_beta_warning': instance.displayBetaWarning, + 'enable_badge': instance.enableBadge, + 'enable_notifications': instance.enableNotifications, + 'message_notification_content': + instance.messageNotificationContent.toJson(), + 'on_invitation_accepted_mode': instance.onInvitationAcceptedMode.toJson(), + 'on_invitation_accepted_sound': + instance.onInvitationAcceptedSound.toJson(), + 'on_message_received_mode': instance.onMessageReceivedMode.toJson(), + 'on_message_received_sound': instance.onMessageReceivedSound.toJson(), + 'on_message_sent_sound': instance.onMessageSentSound.toJson(), + }; diff --git a/lib/notifications/notifications.dart b/lib/notifications/notifications.dart index 5841483..0426651 100644 --- a/lib/notifications/notifications.dart +++ b/lib/notifications/notifications.dart @@ -1,3 +1,3 @@ export 'cubits/notifications_cubit.dart'; -export 'models/notifications_state.dart'; -export 'views/notifications_widget.dart'; +export 'models/models.dart'; +export 'views/views.dart'; diff --git a/lib/notifications/views/notifications_preferences.dart b/lib/notifications/views/notifications_preferences.dart new file mode 100644 index 0000000..12f4667 --- /dev/null +++ b/lib/notifications/views/notifications_preferences.dart @@ -0,0 +1,285 @@ +import 'package:awesome_extensions/awesome_extensions.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 '../../theme/theme.dart'; +import '../notifications.dart'; + +const String formFieldDisplayBetaWarning = 'displayBetaWarning'; +const String formFieldEnableBadge = 'enableBadge'; +const String formFieldEnableNotifications = 'enableNotifications'; +const String formFieldMessageNotificationContent = 'messageNotificationContent'; +const String formFieldInvitationAcceptMode = 'invitationAcceptMode'; +const String formFieldInvitationAcceptSound = 'invitationAcceptSound'; +const String formFieldMessageReceivedMode = 'messageReceivedMode'; +const String formFieldMessageReceivedSound = 'messageReceivedSound'; +const String formFieldMessageSentSound = 'messageSentSound'; + +Widget buildSettingsPageNotificationPreferences( + {required BuildContext context, required void Function() onChanged}) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + final textTheme = theme.textTheme; + + final preferencesRepository = PreferencesRepository.instance; + final notificationsPreference = + preferencesRepository.value.notificationsPreference; + + Future updatePreferences( + NotificationsPreference newNotificationsPreference) async { + final newPrefs = preferencesRepository.value + .copyWith(notificationsPreference: newNotificationsPreference); + await preferencesRepository.set(newPrefs); + onChanged(); + } + + List> notificationModeItems() { + final out = >[]; + final items = [ + (NotificationMode.none, true, translate('settings_page.none')), + (NotificationMode.inApp, true, translate('settings_page.in_app')), + (NotificationMode.push, false, translate('settings_page.push')), + ( + NotificationMode.inAppOrPush, + true, + translate('settings_page.in_app_or_push') + ), + ]; + for (final x in items) { + out.add(DropdownMenuItem( + value: x.$1, + enabled: x.$2, + child: Text(x.$3, style: textTheme.labelSmall))); + } + return out; + } + + List> soundEffectItems() { + final out = >[]; + final items = [ + (SoundEffect.none, true, translate('settings_page.none')), + (SoundEffect.bonk, true, translate('settings_page.bonk')), + (SoundEffect.boop, true, translate('settings_page.boop')), + (SoundEffect.baDeep, true, translate('settings_page.badeep')), + (SoundEffect.beepBaDeep, true, translate('settings_page.beep_badeep')), + (SoundEffect.custom, false, translate('settings_page.custom')), + ]; + for (final x in items) { + out.add(DropdownMenuItem( + value: x.$1, + enabled: x.$2, + child: Text(x.$3, style: textTheme.labelSmall))); + } + return out; + } + + List> + messageNotificationContentItems() { + final out = >[]; + final items = [ + ( + MessageNotificationContent.nameAndContent, + true, + translate('settings_page.name_and_content') + ), + ( + MessageNotificationContent.nameOnly, + true, + translate('settings_page.name_only') + ), + ( + MessageNotificationContent.nothing, + true, + translate('settings_page.nothing') + ), + ]; + for (final x in items) { + out.add(DropdownMenuItem( + value: x.$1, + enabled: x.$2, + child: Text(x.$3, style: textTheme.labelSmall))); + } + return out; + } + + return DecoratedBox( + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(width: 2, color: scale.primaryScale.border), + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale))), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + // Display Beta Warning + FormBuilderCheckbox( + name: formFieldDisplayBetaWarning, + side: BorderSide(color: scale.primaryScale.border, width: 2), + title: Text(translate('settings_page.display_beta_warning'), + style: textTheme.labelMedium), + initialValue: notificationsPreference.displayBetaWarning, + onChanged: (value) async { + if (value == null) { + return; + } + final newNotificationsPreference = + notificationsPreference.copyWith(displayBetaWarning: value); + + await updatePreferences(newNotificationsPreference); + }), + // Enable Badge + FormBuilderCheckbox( + name: formFieldEnableBadge, + side: BorderSide(color: scale.primaryScale.border, width: 2), + title: Text(translate('settings_page.enable_badge'), + style: textTheme.labelMedium), + initialValue: notificationsPreference.enableBadge, + onChanged: (value) async { + if (value == null) { + return; + } + final newNotificationsPreference = + notificationsPreference.copyWith(enableBadge: value); + await updatePreferences(newNotificationsPreference); + }), + // Enable Notifications + FormBuilderCheckbox( + name: formFieldEnableNotifications, + side: BorderSide(color: scale.primaryScale.border, width: 2), + title: Text(translate('settings_page.enable_notifications'), + style: textTheme.labelMedium), + initialValue: notificationsPreference.enableNotifications, + onChanged: (value) async { + if (value == null) { + return; + } + final newNotificationsPreference = + notificationsPreference.copyWith(enableNotifications: value); + await updatePreferences(newNotificationsPreference); + }), + + FormBuilderDropdown( + name: formFieldMessageNotificationContent, + isDense: false, + decoration: InputDecoration( + labelText: translate('settings_page.message_notification_content')), + enabled: notificationsPreference.enableNotifications, + initialValue: notificationsPreference.messageNotificationContent, + onChanged: (value) async { + if (value == null) { + return; + } + final newNotificationsPreference = notificationsPreference.copyWith( + messageNotificationContent: value); + await updatePreferences(newNotificationsPreference); + }, + items: messageNotificationContentItems(), + ).paddingAll(8), + + // Notifications + Table( + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + TableRow(children: [ + // Invitation accepted + Text( + textAlign: TextAlign.right, + translate('settings_page.invitation_accepted')) + .paddingAll(8), + FormBuilderDropdown( + name: formFieldInvitationAcceptMode, + isDense: false, + enabled: notificationsPreference.enableNotifications, + initialValue: notificationsPreference.onInvitationAcceptedMode, + onChanged: (value) async { + if (value == null) { + return; + } + final newNotificationsPreference = notificationsPreference + .copyWith(onInvitationAcceptedMode: value); + await updatePreferences(newNotificationsPreference); + }, + items: notificationModeItems(), + ).paddingAll(4), + FormBuilderDropdown( + name: formFieldInvitationAcceptSound, + isDense: false, + enabled: notificationsPreference.enableNotifications, + initialValue: notificationsPreference.onInvitationAcceptedSound, + onChanged: (value) async { + if (value == null) { + return; + } + final newNotificationsPreference = notificationsPreference + .copyWith(onInvitationAcceptedSound: value); + await updatePreferences(newNotificationsPreference); + }, + items: soundEffectItems(), + ).paddingAll(4) + ]), + // Message received + TableRow(children: [ + Text( + textAlign: TextAlign.right, + translate('settings_page.message_received')) + .paddingAll(8), + FormBuilderDropdown( + name: formFieldMessageReceivedMode, + isDense: false, + enabled: notificationsPreference.enableNotifications, + initialValue: notificationsPreference.onMessageReceivedMode, + onChanged: (value) async { + if (value == null) { + return; + } + final newNotificationsPreference = notificationsPreference + .copyWith(onMessageReceivedMode: value); + await updatePreferences(newNotificationsPreference); + }, + items: notificationModeItems(), + ).paddingAll(4), + FormBuilderDropdown( + name: formFieldMessageReceivedSound, + isDense: false, + enabled: notificationsPreference.enableNotifications, + initialValue: notificationsPreference.onMessageReceivedSound, + onChanged: (value) async { + if (value == null) { + return; + } + final newNotificationsPreference = notificationsPreference + .copyWith(onMessageReceivedSound: value); + await updatePreferences(newNotificationsPreference); + }, + items: soundEffectItems(), + ).paddingAll(4) + ]), + + // Message sent + TableRow(children: [ + Text( + textAlign: TextAlign.right, + translate('settings_page.message_sent')) + .paddingAll(8), + const SizedBox.shrink(), + FormBuilderDropdown( + name: formFieldMessageSentSound, + isDense: false, + enabled: notificationsPreference.enableNotifications, + initialValue: notificationsPreference.onMessageSentSound, + onChanged: (value) async { + if (value == null) { + return; + } + final newNotificationsPreference = notificationsPreference + .copyWith(onMessageSentSound: value); + await updatePreferences(newNotificationsPreference); + }, + items: soundEffectItems(), + ).paddingAll(4) + ]), + ]).paddingAll(8) + ]).paddingAll(8), + ); +} diff --git a/lib/notifications/views/views.dart b/lib/notifications/views/views.dart new file mode 100644 index 0000000..48a03b0 --- /dev/null +++ b/lib/notifications/views/views.dart @@ -0,0 +1,2 @@ +export 'notifications_preferences.dart'; +export 'notifications_widget.dart'; diff --git a/lib/settings/models/preferences.dart b/lib/settings/models/preferences.dart index 8dfcb73..e646c61 100644 --- a/lib/settings/models/preferences.dart +++ b/lib/settings/models/preferences.dart @@ -1,6 +1,7 @@ import 'package:change_case/change_case.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import '../../notifications/notifications.dart'; import '../../theme/theme.dart'; part 'preferences.freezed.dart'; @@ -11,19 +12,15 @@ part 'preferences.g.dart'; @freezed class LockPreference with _$LockPreference { const factory LockPreference({ - required int inactivityLockSecs, - required bool lockWhenSwitching, - required bool lockWithSystemLock, + @Default(0) int inactivityLockSecs, + @Default(false) bool lockWhenSwitching, + @Default(false) bool lockWithSystemLock, }) = _LockPreference; factory LockPreference.fromJson(dynamic json) => _$LockPreferenceFromJson(json as Map); - static const LockPreference defaults = LockPreference( - inactivityLockSecs: 0, - lockWhenSwitching: false, - lockWithSystemLock: false, - ); + static const LockPreference defaults = LockPreference(); } // Theme supports multiple translations @@ -42,16 +39,15 @@ enum LanguagePreference { @freezed class Preferences with _$Preferences { const factory Preferences({ - required ThemePreferences themePreferences, - required LanguagePreference language, - required LockPreference locking, + @Default(ThemePreferences.defaults) ThemePreferences themePreference, + @Default(LanguagePreference.defaults) LanguagePreference languagePreference, + @Default(LockPreference.defaults) LockPreference lockPreference, + @Default(NotificationsPreference.defaults) + NotificationsPreference notificationsPreference, }) = _Preferences; factory Preferences.fromJson(dynamic json) => _$PreferencesFromJson(json as Map); - static const Preferences defaults = Preferences( - themePreferences: ThemePreferences.defaults, - language: LanguagePreference.defaults, - locking: LockPreference.defaults); + static const Preferences defaults = Preferences(); } diff --git a/lib/settings/models/preferences.freezed.dart b/lib/settings/models/preferences.freezed.dart index e9667c8..1735d45 100644 --- a/lib/settings/models/preferences.freezed.dart +++ b/lib/settings/models/preferences.freezed.dart @@ -126,18 +126,21 @@ class __$$LockPreferenceImplCopyWithImpl<$Res> @JsonSerializable() class _$LockPreferenceImpl implements _LockPreference { const _$LockPreferenceImpl( - {required this.inactivityLockSecs, - required this.lockWhenSwitching, - required this.lockWithSystemLock}); + {this.inactivityLockSecs = 0, + this.lockWhenSwitching = false, + this.lockWithSystemLock = false}); factory _$LockPreferenceImpl.fromJson(Map json) => _$$LockPreferenceImplFromJson(json); @override + @JsonKey() final int inactivityLockSecs; @override + @JsonKey() final bool lockWhenSwitching; @override + @JsonKey() final bool lockWithSystemLock; @override @@ -180,9 +183,9 @@ class _$LockPreferenceImpl implements _LockPreference { abstract class _LockPreference implements LockPreference { const factory _LockPreference( - {required final int inactivityLockSecs, - required final bool lockWhenSwitching, - required final bool lockWithSystemLock}) = _$LockPreferenceImpl; + {final int inactivityLockSecs, + final bool lockWhenSwitching, + final bool lockWithSystemLock}) = _$LockPreferenceImpl; factory _LockPreference.fromJson(Map json) = _$LockPreferenceImpl.fromJson; @@ -205,9 +208,12 @@ Preferences _$PreferencesFromJson(Map json) { /// @nodoc mixin _$Preferences { - ThemePreferences get themePreferences => throw _privateConstructorUsedError; - LanguagePreference get language => throw _privateConstructorUsedError; - LockPreference get locking => throw _privateConstructorUsedError; + ThemePreferences get themePreference => throw _privateConstructorUsedError; + LanguagePreference get languagePreference => + throw _privateConstructorUsedError; + LockPreference get lockPreference => throw _privateConstructorUsedError; + NotificationsPreference get notificationsPreference => + throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -222,12 +228,14 @@ abstract class $PreferencesCopyWith<$Res> { _$PreferencesCopyWithImpl<$Res, Preferences>; @useResult $Res call( - {ThemePreferences themePreferences, - LanguagePreference language, - LockPreference locking}); + {ThemePreferences themePreference, + LanguagePreference languagePreference, + LockPreference lockPreference, + NotificationsPreference notificationsPreference}); - $ThemePreferencesCopyWith<$Res> get themePreferences; - $LockPreferenceCopyWith<$Res> get locking; + $ThemePreferencesCopyWith<$Res> get themePreference; + $LockPreferenceCopyWith<$Res> get lockPreference; + $NotificationsPreferenceCopyWith<$Res> get notificationsPreference; } /// @nodoc @@ -243,39 +251,53 @@ class _$PreferencesCopyWithImpl<$Res, $Val extends Preferences> @pragma('vm:prefer-inline') @override $Res call({ - Object? themePreferences = null, - Object? language = null, - Object? locking = null, + Object? themePreference = null, + Object? languagePreference = null, + Object? lockPreference = null, + Object? notificationsPreference = null, }) { return _then(_value.copyWith( - themePreferences: null == themePreferences - ? _value.themePreferences - : themePreferences // ignore: cast_nullable_to_non_nullable + themePreference: null == themePreference + ? _value.themePreference + : themePreference // ignore: cast_nullable_to_non_nullable as ThemePreferences, - language: null == language - ? _value.language - : language // ignore: cast_nullable_to_non_nullable + languagePreference: null == languagePreference + ? _value.languagePreference + : languagePreference // ignore: cast_nullable_to_non_nullable as LanguagePreference, - locking: null == locking - ? _value.locking - : locking // ignore: cast_nullable_to_non_nullable + lockPreference: null == lockPreference + ? _value.lockPreference + : lockPreference // ignore: cast_nullable_to_non_nullable as LockPreference, + notificationsPreference: null == notificationsPreference + ? _value.notificationsPreference + : notificationsPreference // ignore: cast_nullable_to_non_nullable + as NotificationsPreference, ) 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); + $ThemePreferencesCopyWith<$Res> get themePreference { + return $ThemePreferencesCopyWith<$Res>(_value.themePreference, (value) { + return _then(_value.copyWith(themePreference: value) as $Val); }); } @override @pragma('vm:prefer-inline') - $LockPreferenceCopyWith<$Res> get locking { - return $LockPreferenceCopyWith<$Res>(_value.locking, (value) { - return _then(_value.copyWith(locking: value) as $Val); + $LockPreferenceCopyWith<$Res> get lockPreference { + return $LockPreferenceCopyWith<$Res>(_value.lockPreference, (value) { + return _then(_value.copyWith(lockPreference: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $NotificationsPreferenceCopyWith<$Res> get notificationsPreference { + return $NotificationsPreferenceCopyWith<$Res>( + _value.notificationsPreference, (value) { + return _then(_value.copyWith(notificationsPreference: value) as $Val); }); } } @@ -289,14 +311,17 @@ abstract class _$$PreferencesImplCopyWith<$Res> @override @useResult $Res call( - {ThemePreferences themePreferences, - LanguagePreference language, - LockPreference locking}); + {ThemePreferences themePreference, + LanguagePreference languagePreference, + LockPreference lockPreference, + NotificationsPreference notificationsPreference}); @override - $ThemePreferencesCopyWith<$Res> get themePreferences; + $ThemePreferencesCopyWith<$Res> get themePreference; @override - $LockPreferenceCopyWith<$Res> get locking; + $LockPreferenceCopyWith<$Res> get lockPreference; + @override + $NotificationsPreferenceCopyWith<$Res> get notificationsPreference; } /// @nodoc @@ -310,23 +335,28 @@ class __$$PreferencesImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? themePreferences = null, - Object? language = null, - Object? locking = null, + Object? themePreference = null, + Object? languagePreference = null, + Object? lockPreference = null, + Object? notificationsPreference = null, }) { return _then(_$PreferencesImpl( - themePreferences: null == themePreferences - ? _value.themePreferences - : themePreferences // ignore: cast_nullable_to_non_nullable + themePreference: null == themePreference + ? _value.themePreference + : themePreference // ignore: cast_nullable_to_non_nullable as ThemePreferences, - language: null == language - ? _value.language - : language // ignore: cast_nullable_to_non_nullable + languagePreference: null == languagePreference + ? _value.languagePreference + : languagePreference // ignore: cast_nullable_to_non_nullable as LanguagePreference, - locking: null == locking - ? _value.locking - : locking // ignore: cast_nullable_to_non_nullable + lockPreference: null == lockPreference + ? _value.lockPreference + : lockPreference // ignore: cast_nullable_to_non_nullable as LockPreference, + notificationsPreference: null == notificationsPreference + ? _value.notificationsPreference + : notificationsPreference // ignore: cast_nullable_to_non_nullable + as NotificationsPreference, )); } } @@ -335,23 +365,30 @@ class __$$PreferencesImplCopyWithImpl<$Res> @JsonSerializable() class _$PreferencesImpl implements _Preferences { const _$PreferencesImpl( - {required this.themePreferences, - required this.language, - required this.locking}); + {this.themePreference = ThemePreferences.defaults, + this.languagePreference = LanguagePreference.defaults, + this.lockPreference = LockPreference.defaults, + this.notificationsPreference = NotificationsPreference.defaults}); factory _$PreferencesImpl.fromJson(Map json) => _$$PreferencesImplFromJson(json); @override - final ThemePreferences themePreferences; + @JsonKey() + final ThemePreferences themePreference; @override - final LanguagePreference language; + @JsonKey() + final LanguagePreference languagePreference; @override - final LockPreference locking; + @JsonKey() + final LockPreference lockPreference; + @override + @JsonKey() + final NotificationsPreference notificationsPreference; @override String toString() { - return 'Preferences(themePreferences: $themePreferences, language: $language, locking: $locking)'; + return 'Preferences(themePreference: $themePreference, languagePreference: $languagePreference, lockPreference: $lockPreference, notificationsPreference: $notificationsPreference)'; } @override @@ -359,17 +396,21 @@ class _$PreferencesImpl implements _Preferences { return identical(this, other) || (other.runtimeType == runtimeType && other is _$PreferencesImpl && - (identical(other.themePreferences, themePreferences) || - other.themePreferences == themePreferences) && - (identical(other.language, language) || - other.language == language) && - (identical(other.locking, locking) || other.locking == locking)); + (identical(other.themePreference, themePreference) || + other.themePreference == themePreference) && + (identical(other.languagePreference, languagePreference) || + other.languagePreference == languagePreference) && + (identical(other.lockPreference, lockPreference) || + other.lockPreference == lockPreference) && + (identical( + other.notificationsPreference, notificationsPreference) || + other.notificationsPreference == notificationsPreference)); } @JsonKey(ignore: true) @override - int get hashCode => - Object.hash(runtimeType, themePreferences, language, locking); + int get hashCode => Object.hash(runtimeType, themePreference, + languagePreference, lockPreference, notificationsPreference); @JsonKey(ignore: true) @override @@ -387,19 +428,23 @@ class _$PreferencesImpl implements _Preferences { abstract class _Preferences implements Preferences { const factory _Preferences( - {required final ThemePreferences themePreferences, - required final LanguagePreference language, - required final LockPreference locking}) = _$PreferencesImpl; + {final ThemePreferences themePreference, + final LanguagePreference languagePreference, + final LockPreference lockPreference, + final NotificationsPreference notificationsPreference}) = + _$PreferencesImpl; factory _Preferences.fromJson(Map json) = _$PreferencesImpl.fromJson; @override - ThemePreferences get themePreferences; + ThemePreferences get themePreference; @override - LanguagePreference get language; + LanguagePreference get languagePreference; @override - LockPreference get locking; + LockPreference get lockPreference; + @override + NotificationsPreference get notificationsPreference; @override @JsonKey(ignore: true) _$$PreferencesImplCopyWith<_$PreferencesImpl> get copyWith => diff --git a/lib/settings/models/preferences.g.dart b/lib/settings/models/preferences.g.dart index 23cd5bb..5813f67 100644 --- a/lib/settings/models/preferences.g.dart +++ b/lib/settings/models/preferences.g.dart @@ -8,9 +8,9 @@ part of 'preferences.dart'; _$LockPreferenceImpl _$$LockPreferenceImplFromJson(Map json) => _$LockPreferenceImpl( - inactivityLockSecs: (json['inactivity_lock_secs'] as num).toInt(), - lockWhenSwitching: json['lock_when_switching'] as bool, - lockWithSystemLock: json['lock_with_system_lock'] as bool, + inactivityLockSecs: (json['inactivity_lock_secs'] as num?)?.toInt() ?? 0, + lockWhenSwitching: json['lock_when_switching'] as bool? ?? false, + lockWithSystemLock: json['lock_with_system_lock'] as bool? ?? false, ); Map _$$LockPreferenceImplToJson( @@ -23,14 +23,24 @@ Map _$$LockPreferenceImplToJson( _$PreferencesImpl _$$PreferencesImplFromJson(Map json) => _$PreferencesImpl( - themePreferences: ThemePreferences.fromJson(json['theme_preferences']), - language: LanguagePreference.fromJson(json['language']), - locking: LockPreference.fromJson(json['locking']), + themePreference: json['theme_preference'] == null + ? ThemePreferences.defaults + : ThemePreferences.fromJson(json['theme_preference']), + languagePreference: json['language_preference'] == null + ? LanguagePreference.defaults + : LanguagePreference.fromJson(json['language_preference']), + lockPreference: json['lock_preference'] == null + ? LockPreference.defaults + : LockPreference.fromJson(json['lock_preference']), + notificationsPreference: json['notifications_preference'] == null + ? NotificationsPreference.defaults + : NotificationsPreference.fromJson(json['notifications_preference']), ); Map _$$PreferencesImplToJson(_$PreferencesImpl instance) => { - 'theme_preferences': instance.themePreferences.toJson(), - 'language': instance.language.toJson(), - 'locking': instance.locking.toJson(), + 'theme_preference': instance.themePreference.toJson(), + 'language_preference': instance.languagePreference.toJson(), + 'lock_preference': instance.lockPreference.toJson(), + 'notifications_preference': instance.notificationsPreference.toJson(), }; diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart index 5b92643..ac21fc4 100644 --- a/lib/settings/settings_page.dart +++ b/lib/settings/settings_page.dart @@ -6,6 +6,7 @@ import 'package:flutter_translate/flutter_translate.dart'; import 'package:go_router/go_router.dart'; import '../layout/default_app_bar.dart'; +import '../notifications/notifications.dart'; import '../theme/theme.dart'; import '../veilid_processor/veilid_processor.dart'; import 'settings.dart'; @@ -49,6 +50,8 @@ class SettingsPageState extends State { context: context, onChanged: () => setState(() {})), buildSettingsPageBrightnessPreferences( context: context, onChanged: () => setState(() {})), + buildSettingsPageNotificationPreferences( + context: context, onChanged: () => setState(() {})), ].map((x) => x.paddingLTRB(0, 0, 0, 8)).toList(), ), ).paddingSymmetric(horizontal: 24, vertical: 16), diff --git a/lib/theme/models/theme_preference.dart b/lib/theme/models/theme_preference.dart index 0accd8f..cfa05c9 100644 --- a/lib/theme/models/theme_preference.dart +++ b/lib/theme/models/theme_preference.dart @@ -49,19 +49,16 @@ enum ColorPreference { @freezed class ThemePreferences with _$ThemePreferences { const factory ThemePreferences({ - required BrightnessPreference brightnessPreference, - required ColorPreference colorPreference, - required double displayScale, + @Default(BrightnessPreference.system) + BrightnessPreference brightnessPreference, + @Default(ColorPreference.vapor) ColorPreference colorPreference, + @Default(1) double displayScale, }) = _ThemePreferences; factory ThemePreferences.fromJson(dynamic json) => _$ThemePreferencesFromJson(json as Map); - static const ThemePreferences defaults = ThemePreferences( - colorPreference: ColorPreference.vapor, - brightnessPreference: BrightnessPreference.system, - displayScale: 1, - ); + static const ThemePreferences defaults = ThemePreferences(); } extension ThemePreferencesExt on ThemePreferences { diff --git a/lib/theme/models/theme_preference.freezed.dart b/lib/theme/models/theme_preference.freezed.dart index 9f10955..97e3f81 100644 --- a/lib/theme/models/theme_preference.freezed.dart +++ b/lib/theme/models/theme_preference.freezed.dart @@ -127,18 +127,21 @@ class __$$ThemePreferencesImplCopyWithImpl<$Res> @JsonSerializable() class _$ThemePreferencesImpl implements _ThemePreferences { const _$ThemePreferencesImpl( - {required this.brightnessPreference, - required this.colorPreference, - required this.displayScale}); + {this.brightnessPreference = BrightnessPreference.system, + this.colorPreference = ColorPreference.vapor, + this.displayScale = 1}); factory _$ThemePreferencesImpl.fromJson(Map json) => _$$ThemePreferencesImplFromJson(json); @override + @JsonKey() final BrightnessPreference brightnessPreference; @override + @JsonKey() final ColorPreference colorPreference; @override + @JsonKey() final double displayScale; @override @@ -181,9 +184,9 @@ class _$ThemePreferencesImpl implements _ThemePreferences { abstract class _ThemePreferences implements ThemePreferences { const factory _ThemePreferences( - {required final BrightnessPreference brightnessPreference, - required final ColorPreference colorPreference, - required final double displayScale}) = _$ThemePreferencesImpl; + {final BrightnessPreference brightnessPreference, + final ColorPreference colorPreference, + final double displayScale}) = _$ThemePreferencesImpl; factory _ThemePreferences.fromJson(Map json) = _$ThemePreferencesImpl.fromJson; diff --git a/lib/theme/models/theme_preference.g.dart b/lib/theme/models/theme_preference.g.dart index 6f33c43..4cb2d71 100644 --- a/lib/theme/models/theme_preference.g.dart +++ b/lib/theme/models/theme_preference.g.dart @@ -9,10 +9,13 @@ part of 'theme_preference.dart'; _$ThemePreferencesImpl _$$ThemePreferencesImplFromJson( Map json) => _$ThemePreferencesImpl( - brightnessPreference: - BrightnessPreference.fromJson(json['brightness_preference']), - colorPreference: ColorPreference.fromJson(json['color_preference']), - displayScale: (json['display_scale'] as num).toDouble(), + brightnessPreference: json['brightness_preference'] == null + ? BrightnessPreference.system + : BrightnessPreference.fromJson(json['brightness_preference']), + colorPreference: json['color_preference'] == null + ? ColorPreference.vapor + : ColorPreference.fromJson(json['color_preference']), + displayScale: (json['display_scale'] as num?)?.toDouble() ?? 1, ); Map _$$ThemePreferencesImplToJson( diff --git a/lib/theme/views/brightness_preferences.dart b/lib/theme/views/brightness_preferences.dart index 2f3f410..0c39976 100644 --- a/lib/theme/views/brightness_preferences.dart +++ b/lib/theme/views/brightness_preferences.dart @@ -24,7 +24,7 @@ List> _getBrightnessDropdownItems() { Widget buildSettingsPageBrightnessPreferences( {required BuildContext context, required void Function() onChanged}) { final preferencesRepository = PreferencesRepository.instance; - final themePreferences = preferencesRepository.value.themePreferences; + final themePreferences = preferencesRepository.value.themePreference; return ThemeSwitcher.withTheme( builder: (_, switcher, theme) => FormBuilderDropdown( name: formFieldBrightness, @@ -36,7 +36,7 @@ Widget buildSettingsPageBrightnessPreferences( final newThemePrefs = themePreferences.copyWith( brightnessPreference: value as BrightnessPreference); final newPrefs = preferencesRepository.value - .copyWith(themePreferences: newThemePrefs); + .copyWith(themePreference: newThemePrefs); await preferencesRepository.set(newPrefs); switcher.changeTheme(theme: newThemePrefs.themeData()); diff --git a/lib/theme/views/color_preferences.dart b/lib/theme/views/color_preferences.dart index 228c2fb..ce03c0a 100644 --- a/lib/theme/views/color_preferences.dart +++ b/lib/theme/views/color_preferences.dart @@ -34,7 +34,7 @@ List> _getThemeDropdownItems() { Widget buildSettingsPageColorPreferences( {required BuildContext context, required void Function() onChanged}) { final preferencesRepository = PreferencesRepository.instance; - final themePreferences = preferencesRepository.value.themePreferences; + final themePreferences = preferencesRepository.value.themePreference; return ThemeSwitcher.withTheme( builder: (_, switcher, theme) => FormBuilderDropdown( name: formFieldTheme, @@ -46,7 +46,7 @@ Widget buildSettingsPageColorPreferences( final newThemePrefs = themePreferences.copyWith( colorPreference: value as ColorPreference); final newPrefs = preferencesRepository.value - .copyWith(themePreferences: newThemePrefs); + .copyWith(themePreference: newThemePrefs); await preferencesRepository.set(newPrefs); switcher.changeTheme(theme: newThemePrefs.themeData()); diff --git a/lib/theme/views/widget_helpers.dart b/lib/theme/views/widget_helpers.dart index 91ae11a..4beb48b 100644 --- a/lib/theme/views/widget_helpers.dart +++ b/lib/theme/views/widget_helpers.dart @@ -19,6 +19,14 @@ extension BorderExt on Widget { child: this); } +extension SizeToFixExt on Widget { + FittedBox fit({BoxFit? fit, Key? key}) => FittedBox( + key: key, + fit: fit ?? BoxFit.scaleDown, + child: this, + ); +} + extension ModalProgressExt on Widget { BlurryModalProgressHUD withModalHUD(BuildContext context, bool isLoading) { final theme = Theme.of(context); diff --git a/pubspec.yaml b/pubspec.yaml index edfa7b0..fea8acb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -160,6 +160,11 @@ flutter: - assets/images/ellet.png # Printing - assets/js/pdf/3.2.146/pdf.min.js + # Sounds + - assets/sounds/bonk.wav + - assets/sounds/boop.wav + - assets/sounds/badeep.wav + - assets/sounds/beepbadeep.wav # Fonts fonts: - family: Source Code Pro From 01c6490ec4ed5b8fc96e1e43b970c2621acaf943 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 27 Jul 2024 18:36:06 -0400 Subject: [PATCH 167/270] ui improvements for invitations --- assets/i18n/en.json | 11 +++- .../cubits/contact_invitation_list_cubit.dart | 27 +++++--- .../views/contact_invitation_display.dart | 48 ++++++++++---- .../views/create_invitation_dialog.dart | 13 ++-- .../views/invitation_dialog.dart | 44 +++++++++++-- .../views/notifications_preferences.dart | 65 +++++++++++++++---- packages/veilid_support/pubspec.lock | 14 ++-- packages/veilid_support/pubspec.yaml | 10 +-- pubspec.lock | 14 ++-- pubspec.yaml | 10 +-- 10 files changed, 179 insertions(+), 77 deletions(-) diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 0fad430..22feb44 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -122,7 +122,9 @@ }, "create_invitation_dialog": { "title": "Create Contact Invitation", - "connect_with_me": "Connect with me on VeilidChat!", + "me": "me", + "fingerprint": "Fingerprint:", + "connect_with_me": "Connect with {name} on VeilidChat!", "enter_message_hint": "Enter message for contact (optional)", "message_to_contact": "Message to send with invitation (not encrypted)", "generate": "Generate Invitation", @@ -148,6 +150,7 @@ "failed_to_reject": "Failed to reject contact invitation", "invalid_invitation": "Invalid invitation", "try_again_online": "Invitation could not be reached, try again when online", + "key_not_found": "Invitation could not be found, it may not be on the network yet", "protected_with_pin": "Contact invitation is protected with a PIN", "protected_with_password": "Contact invitation is protected with a password", "invalid_pin": "Invalid PIN", @@ -155,7 +158,7 @@ }, "waiting_invitation": { "accepted": "Contact invitation accepted from {name}", - "reject": "Contact invitation was rejected" + "rejected": "Contact invitation was rejected" }, "paste_invitation_dialog": { "title": "Paste Contact Invite", @@ -225,6 +228,10 @@ "in_app": "In-app", "push": "Push", "in_app_or_push": "In-app or Push", + "notifications": "Notifications", + "event": "Event", + "sound": "Sound", + "delivery": "Delivery", "enable_badge": "Enable icon 'badge' bubble", "enable_notifications": "Enable notifications", "message_notification_content": "Message notification content", diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index f3e9521..f5663af 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:async_tools/async_tools.dart'; import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fixnum/fixnum.dart'; @@ -214,9 +215,11 @@ class ContactInvitationListCubit } } - Future validateInvitation( - {required Uint8List inviteData, - required GetEncryptionKeyCallback getEncryptionKeyCallback}) async { + Future validateInvitation({ + required Uint8List inviteData, + required GetEncryptionKeyCallback getEncryptionKeyCallback, + required CancelRequest cancelRequest, + }) async { final pool = DHTRecordPool.instance; // Get contact request inbox from invitation @@ -245,15 +248,18 @@ class ContactInvitationListCubit contactRequestInboxKey) != -1; - await (await pool.openRecordRead(contactRequestInboxKey, - debugName: 'ContactInvitationListCubit::validateInvitation::' - 'ContactRequestInbox', - parent: pool.getParentRecordKey(contactRequestInboxKey) ?? - _accountInfo.accountRecordKey)) + await (await pool + .openRecordRead(contactRequestInboxKey, + debugName: 'ContactInvitationListCubit::validateInvitation::' + 'ContactRequestInbox', + parent: pool.getParentRecordKey(contactRequestInboxKey) ?? + _accountInfo.accountRecordKey) + .withCancel(cancelRequest)) .maybeDeleteScope(!isSelf, (contactRequestInbox) async { // final contactRequest = await contactRequestInbox - .getProtobuf(proto.ContactRequest.fromBuffer); + .getProtobuf(proto.ContactRequest.fromBuffer) + .withCancel(cancelRequest); final cs = await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind); @@ -281,7 +287,8 @@ class ContactInvitationListCubit // Fetch the account master final contactSuperIdentity = await SuperIdentity.open( - superRecordKey: contactSuperIdentityRecordKey); + superRecordKey: contactSuperIdentityRecordKey) + .withCancel(cancelRequest); // Verify final idcs = await contactSuperIdentity.currentInstance.cryptoSystem; diff --git a/lib/contact_invitation/views/contact_invitation_display.dart b/lib/contact_invitation/views/contact_invitation_display.dart index d816fc3..83b80d0 100644 --- a/lib/contact_invitation/views/contact_invitation_display.dart +++ b/lib/contact_invitation/views/contact_invitation_display.dart @@ -11,6 +11,7 @@ import 'package:provider/provider.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:veilid_support/veilid_support.dart'; +import '../../account_manager/account_manager.dart'; import '../../notifications/notifications.dart'; import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; @@ -20,17 +21,20 @@ class ContactInvitationDisplayDialog extends StatelessWidget { const ContactInvitationDisplayDialog._({ required this.locator, required this.message, + required this.fingerprint, }); final Locator locator; final String message; + final String fingerprint; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add(StringProperty('message', message)) - ..add(DiagnosticsProperty('locator', locator)); + ..add(DiagnosticsProperty('locator', locator)) + ..add(StringProperty('fingerprint', fingerprint)); } String makeTextInvite(String message, Uint8List data) { @@ -38,10 +42,12 @@ class ContactInvitationDisplayDialog extends StatelessWidget { base64UrlNoPadEncode(data), '\n', 40, repeat: true); final msg = message.isNotEmpty ? '$message\n' : ''; + return '$msg' '--- BEGIN VEILIDCHAT CONTACT INVITE ----\n' '$invite\n' - '---- END VEILIDCHAT CONTACT INVITE -----\n'; + '---- END VEILIDCHAT CONTACT INVITE -----\n' + 'Fingerprint:\n$fingerprint\n'; } @override @@ -97,18 +103,27 @@ class ContactInvitationDisplayDialog extends StatelessWidget { .copyWith(color: Colors.black))) .paddingAll(8), FittedBox( - child: QrImageView.withQr( - size: 300, - qr: QrCode.fromUint8List( - data: data.$1, - errorCorrectLevel: - QrErrorCorrectLevel.L))) - .expanded(), + child: QrImageView.withQr( + size: 300, + qr: QrCode.fromUint8List( + data: data.$1, + errorCorrectLevel: + QrErrorCorrectLevel.L)), + ).expanded(), Text(message, softWrap: true, style: textTheme.labelLarge! .copyWith(color: Colors.black)) .paddingAll(8), + Text( + '${translate('create_invitation_dialog.fingerprint')}\n' + '$fingerprint', + softWrap: true, + textAlign: TextAlign.center, + style: textTheme.labelSmall!.copyWith( + color: Colors.black, + fontFamily: 'Source Code Pro')) + .paddingAll(2), ElevatedButton.icon( icon: const Icon(Icons.copy), style: ElevatedButton.styleFrom( @@ -129,11 +144,15 @@ class ContactInvitationDisplayDialog extends StatelessWidget { error: errorPage))))); } - static Future show( - {required BuildContext context, - required Locator locator, - required InvitationGeneratorCubit Function(BuildContext) create, - required String message}) async { + static Future show({ + required BuildContext context, + required Locator locator, + required InvitationGeneratorCubit Function(BuildContext) create, + required String message, + }) async { + final fingerprint = + locator().state.identityPublicKey.toString(); + await showPopControlDialog( context: context, builder: (context) => BlocProvider( @@ -141,6 +160,7 @@ class ContactInvitationDisplayDialog extends StatelessWidget { child: ContactInvitationDisplayDialog._( locator: locator, message: message, + fingerprint: fingerprint, ))); } } diff --git a/lib/contact_invitation/views/create_invitation_dialog.dart b/lib/contact_invitation/views/create_invitation_dialog.dart index 9117d03..5711a32 100644 --- a/lib/contact_invitation/views/create_invitation_dialog.dart +++ b/lib/contact_invitation/views/create_invitation_dialog.dart @@ -37,8 +37,7 @@ class CreateInvitationDialog extends StatefulWidget { } class CreateInvitationDialogState extends State { - final _messageTextController = TextEditingController( - text: translate('create_invitation_dialog.connect_with_me')); + late final TextEditingController _messageTextController; EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none; String _encryptionKey = ''; @@ -46,6 +45,12 @@ class CreateInvitationDialogState extends State { @override void initState() { + final accountInfo = widget.locator().state; + final name = accountInfo.asData?.value.profile.name ?? + translate('create_invitation_dialog.me'); + _messageTextController = TextEditingController( + text: translate('create_invitation_dialog.connect_with_me', + args: {'name': name})); super.initState(); } @@ -152,13 +157,13 @@ class CreateInvitationDialogState extends State { message: _messageTextController.text, expiration: _expiration); + navigator.pop(); + await ContactInvitationDisplayDialog.show( context: context, locator: widget.locator, message: _messageTextController.text, create: (context) => InvitationGeneratorCubit(generator)); - - navigator.pop(); } @override diff --git a/lib/contact_invitation/views/invitation_dialog.dart b/lib/contact_invitation/views/invitation_dialog.dart index 5e1ece6..385cbcb 100644 --- a/lib/contact_invitation/views/invitation_dialog.dart +++ b/lib/contact_invitation/views/invitation_dialog.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:async_tools/async_tools.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -61,17 +62,19 @@ class InvitationDialog extends StatefulWidget { } class InvitationDialogState extends State { - ValidContactInvitation? _validInvitation; - bool _isValidating = false; - bool _isAccepting = false; - @override void initState() { super.initState(); } - bool get isValidating => _isValidating; - bool get isAccepting => _isAccepting; + Future _onCancel() async { + final navigator = Navigator.of(context); + _cancelRequest.cancel(); + setState(() { + _isAccepting = false; + }); + navigator.pop(); + } Future _onAccept() async { final navigator = Navigator.of(context); @@ -153,6 +156,7 @@ class InvitationDialogState extends State { final validatedContactInvitation = await contactInvitationListCubit.validateInvitation( inviteData: inviteData, + cancelRequest: _cancelRequest, getEncryptionKeyCallback: (cs, encryptionKeyType, encryptedSecret) async { String encryptionKey; @@ -234,6 +238,9 @@ class InvitationDialogState extends State { late final String errorText; if (e is VeilidAPIExceptionTryAgain) { errorText = translate('invitation_dialog.try_again_online'); + } + if (e is VeilidAPIExceptionKeyNotFound) { + errorText = translate('invitation_dialog.key_not_found'); } else { errorText = translate('invitation_dialog.invalid_invitation'); } @@ -245,6 +252,12 @@ class InvitationDialogState extends State { _validInvitation = null; widget.onValidationFailed(); }); + } on CancelException { + setState(() { + _isValidating = false; + _validInvitation = null; + widget.onValidationCancelled(); + }); } on Exception catch (e) { log.debug('exception: $e', e); setState(() { @@ -264,6 +277,11 @@ class InvitationDialogState extends State { Text(translate('invitation_dialog.validating')) .paddingLTRB(0, 0, 0, 16), buildProgressIndicator().paddingAll(16), + ElevatedButton.icon( + icon: const Icon(Icons.cancel), + label: Text(translate('button.cancel')), + onPressed: _onCancel, + ).paddingAll(16), ]).toCenter(), if (_validInvitation == null && !_isValidating && @@ -315,13 +333,25 @@ class InvitationDialogState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: _isAccepting - ? [buildProgressIndicator().paddingAll(16)] + ? [ + buildProgressIndicator().paddingAll(16), + ] : _buildPreAccept()), ), ); return PopControl(dismissible: dismissible, child: dialog); } + //////////////////////////////////////////////////////////////////////////// + + ValidContactInvitation? _validInvitation; + bool _isValidating = false; + bool _isAccepting = false; + final _cancelRequest = CancelRequest(); + + bool get isValidating => _isValidating; + bool get isAccepting => _isAccepting; + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); diff --git a/lib/notifications/views/notifications_preferences.dart b/lib/notifications/views/notifications_preferences.dart index 12f4667..5967522 100644 --- a/lib/notifications/views/notifications_preferences.dart +++ b/lib/notifications/views/notifications_preferences.dart @@ -52,7 +52,11 @@ Widget buildSettingsPageNotificationPreferences( out.add(DropdownMenuItem( value: x.$1, enabled: x.$2, - child: Text(x.$3, style: textTheme.labelSmall))); + child: Text( + x.$3, + style: textTheme.labelSmall, + textAlign: TextAlign.center, + ))); } return out; } @@ -71,7 +75,11 @@ Widget buildSettingsPageNotificationPreferences( out.add(DropdownMenuItem( value: x.$1, enabled: x.$2, - child: Text(x.$3, style: textTheme.labelSmall))); + child: Text( + x.$3, + style: textTheme.labelSmall, + textAlign: TextAlign.center, + ))); } return out; } @@ -100,17 +108,23 @@ Widget buildSettingsPageNotificationPreferences( out.add(DropdownMenuItem( value: x.$1, enabled: x.$2, - child: Text(x.$3, style: textTheme.labelSmall))); + child: Text( + x.$3, + style: textTheme.labelSmall, + textAlign: TextAlign.center, + ))); } return out; } - return DecoratedBox( - decoration: ShapeDecoration( - shape: RoundedRectangleBorder( - side: BorderSide(width: 2, color: scale.primaryScale.border), - borderRadius: - BorderRadius.circular(8 * scaleConfig.borderRadiusScale))), + return InputDecorator( + decoration: InputDecoration( + labelText: translate('settings_page.notifications'), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8 * scaleConfig.borderRadiusScale), + borderSide: BorderSide(width: 2, color: scale.primaryScale.border), + ), + ), child: Column(mainAxisSize: MainAxisSize.min, children: [ // Display Beta Warning FormBuilderCheckbox( @@ -175,12 +189,35 @@ Widget buildSettingsPageNotificationPreferences( await updatePreferences(newNotificationsPreference); }, items: messageNotificationContentItems(), - ).paddingAll(8), + ).paddingLTRB(0, 4, 0, 4), // Notifications Table( defaultVerticalAlignment: TableCellVerticalAlignment.middle, children: [ + TableRow(children: [ + Text(translate('settings_page.event'), + textAlign: TextAlign.center, + style: textTheme.titleMedium!.copyWith( + color: scale.primaryScale.border, + decorationColor: scale.primaryScale.border, + decoration: TextDecoration.underline)) + .paddingAll(8), + Text(translate('settings_page.delivery'), + textAlign: TextAlign.center, + style: textTheme.titleMedium!.copyWith( + color: scale.primaryScale.border, + decorationColor: scale.primaryScale.border, + decoration: TextDecoration.underline)) + .paddingAll(8), + Text(translate('settings_page.sound'), + textAlign: TextAlign.center, + style: textTheme.titleMedium!.copyWith( + color: scale.primaryScale.border, + decorationColor: scale.primaryScale.border, + decoration: TextDecoration.underline)) + .paddingAll(8), + ]), TableRow(children: [ // Invitation accepted Text( @@ -216,7 +253,7 @@ Widget buildSettingsPageNotificationPreferences( await updatePreferences(newNotificationsPreference); }, items: soundEffectItems(), - ).paddingAll(4) + ).paddingLTRB(4, 4, 0, 4) ]), // Message received TableRow(children: [ @@ -253,7 +290,7 @@ Widget buildSettingsPageNotificationPreferences( await updatePreferences(newNotificationsPreference); }, items: soundEffectItems(), - ).paddingAll(4) + ).paddingLTRB(4, 4, 0, 4) ]), // Message sent @@ -277,9 +314,9 @@ Widget buildSettingsPageNotificationPreferences( await updatePreferences(newNotificationsPreference); }, items: soundEffectItems(), - ).paddingAll(4) + ).paddingLTRB(4, 4, 0, 4) ]), - ]).paddingAll(8) + ]) ]).paddingAll(8), ); } diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index 7f08359..e68b6db 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -36,10 +36,9 @@ packages: async_tools: dependency: "direct main" description: - name: async_tools - sha256: "375da1e5b51974a9e84469b7630f36d1361fb53d3fcc818a5a0121ab51fe0343" - url: "https://pub.dev" - source: hosted + path: "../../../dart_async_tools" + relative: true + source: path version: "0.1.4" bloc: dependency: "direct main" @@ -52,10 +51,9 @@ packages: bloc_advanced_tools: dependency: "direct main" description: - name: bloc_advanced_tools - sha256: "0599d860eb096c5b12457c60bdf7f66bfcb7171bc94ccf2c7c752b6a716a79f9" - url: "https://pub.dev" - source: hosted + path: "../../../bloc_advanced_tools" + relative: true + source: path version: "0.1.4" boolean_selector: dependency: transitive diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index 90d7ad8..51c3e60 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -26,11 +26,11 @@ dependencies: # veilid: ^0.0.1 path: ../../../veilid/veilid-flutter -# dependency_overrides: -# async_tools: -# path: ../../../dart_async_tools -# bloc_advanced_tools: -# path: ../../../bloc_advanced_tools +dependency_overrides: + async_tools: + path: ../../../dart_async_tools + bloc_advanced_tools: + path: ../../../bloc_advanced_tools dev_dependencies: build_runner: ^2.4.10 diff --git a/pubspec.lock b/pubspec.lock index 2b3149b..6d78407 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -84,10 +84,9 @@ packages: async_tools: dependency: "direct main" description: - name: async_tools - sha256: "375da1e5b51974a9e84469b7630f36d1361fb53d3fcc818a5a0121ab51fe0343" - url: "https://pub.dev" - source: hosted + path: "../dart_async_tools" + relative: true + source: path version: "0.1.4" awesome_extensions: dependency: "direct main" @@ -140,10 +139,9 @@ packages: bloc_advanced_tools: dependency: "direct main" description: - name: bloc_advanced_tools - sha256: "0599d860eb096c5b12457c60bdf7f66bfcb7171bc94ccf2c7c752b6a716a79f9" - url: "https://pub.dev" - source: hosted + path: "../bloc_advanced_tools" + relative: true + source: path version: "0.1.4" blurry_modal_progress_hud: dependency: "direct main" diff --git a/pubspec.yaml b/pubspec.yaml index fea8acb..2c0b8eb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -107,11 +107,11 @@ dependencies: xterm: ^4.0.0 zxing2: ^0.2.3 -# dependency_overrides: -# async_tools: -# path: ../dart_async_tools -# bloc_advanced_tools: -# path: ../bloc_advanced_tools +dependency_overrides: + async_tools: + path: ../dart_async_tools + bloc_advanced_tools: + path: ../bloc_advanced_tools # flutter_chat_ui: # path: ../flutter_chat_ui From 5e4f47d5a181643535e45b40d9d53f806847784a Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 31 Jul 2024 12:04:43 -0500 Subject: [PATCH 168/270] account management update --- assets/i18n/en.json | 60 +++- .../cubits/account_record_cubit.dart | 37 ++- lib/account_manager/models/account_spec.dart | 55 ++++ lib/account_manager/models/models.dart | 2 +- .../models/new_profile_spec.dart | 5 - .../repository/account_repository.dart | 21 +- .../views/edit_account_page.dart | 52 +-- .../views/edit_profile_form.dart | 302 ++++++++++++++++++ .../views/new_account_page.dart | 21 +- .../views/profile_edit_form.dart | 118 ------- .../views/contact_invitation_list_widget.dart | 2 +- lib/contacts/cubits/contact_list_cubit.dart | 38 ++- lib/contacts/views/availability_widget.dart | 68 ++++ .../views/contact_details_widget.dart | 41 +++ lib/contacts/views/contact_item_widget.dart | 82 +++-- lib/contacts/views/contact_list_widget.dart | 86 ----- lib/contacts/views/contacts_browser.dart | 247 ++++++++++++++ lib/contacts/views/contacts_dialog.dart | 140 ++++++++ lib/contacts/views/edit_contact_form.dart | 174 ++++++++++ lib/contacts/views/no_contact_widget.dart | 41 +++ lib/contacts/views/views.dart | 7 +- lib/layout/home/drawer_menu/drawer_menu.dart | 79 +++-- lib/layout/home/home_account_ready.dart | 110 ++++--- lib/layout/home/home_screen.dart | 9 +- .../bottom_sheet_action_button.dart | 68 ---- lib/layout/home/main_pager/chats_page.dart | 28 -- lib/layout/home/main_pager/contacts_page.dart | 58 ---- lib/layout/home/main_pager/main_pager.dart | 242 -------------- lib/layout/layout.dart | 1 - lib/proto/veilidchat.pb.dart | 50 ++- lib/proto/veilidchat.pbjson.dart | 15 +- lib/proto/veilidchat.proto | 21 +- lib/router/cubits/router_cubit.dart | 2 +- lib/settings/settings_page.dart | 4 +- lib/theme/models/slider_tile.dart | 34 +- lib/theme/views/avatar_widget.dart | 77 +++++ lib/theme/views/styled_dialog.dart | 1 + lib/theme/views/styled_scaffold.dart | 18 +- lib/theme/views/views.dart | 1 + lib/theme/views/widget_helpers.dart | 55 ++++ pubspec.lock | 13 +- pubspec.yaml | 9 +- 42 files changed, 1663 insertions(+), 831 deletions(-) create mode 100644 lib/account_manager/models/account_spec.dart delete mode 100644 lib/account_manager/models/new_profile_spec.dart create mode 100644 lib/account_manager/views/edit_profile_form.dart delete mode 100644 lib/account_manager/views/profile_edit_form.dart create mode 100644 lib/contacts/views/availability_widget.dart create mode 100644 lib/contacts/views/contact_details_widget.dart delete mode 100644 lib/contacts/views/contact_list_widget.dart create mode 100644 lib/contacts/views/contacts_browser.dart create mode 100644 lib/contacts/views/contacts_dialog.dart create mode 100644 lib/contacts/views/edit_contact_form.dart create mode 100644 lib/contacts/views/no_contact_widget.dart delete mode 100644 lib/layout/home/main_pager/bottom_sheet_action_button.dart delete mode 100644 lib/layout/home/main_pager/chats_page.dart delete mode 100644 lib/layout/home/main_pager/contacts_page.dart delete mode 100644 lib/layout/home/main_pager/main_pager.dart create mode 100644 lib/theme/views/avatar_widget.dart diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 22feb44..ade2dcb 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -3,7 +3,9 @@ "title": "VeilidChat" }, "menu": { - "settings_tooltip": "Settings", + "accounts_menu_tooltip": "Accounts Menu", + "contacts_tooltip": "Contacts List", + "new_chat_tooltip": "Start New Chat", "add_account_tooltip": "Add Account", "accounts": "Accounts", "version": "Version" @@ -12,13 +14,23 @@ "beta_title": "VeilidChat is BETA SOFTWARE", "beta_text": "DO NOT USE THIS FOR ANYTHING IMPORTANT\n\nUntil 1.0 is released:\n\n• You should have no expectations of actual privacy, or guarantees of security.\n• You will likely lose accounts, contacts, and messages and need to recreate them.\n\nPlease read our BETA PARTICIPATION GUIDE located here:\n\n" }, - "pager": { - "chats": "Chats", - "contacts": "Contacts" - }, "account": { "form_name": "Name", - "form_pronouns": "Pronouns (optional)", + "empty_name": "Your name (required)", + "form_pronouns": "Pronouns", + "empty_pronouns": "(optional pronouns)", + "form_about": "About Me", + "empty_about": "Tell your contacts about yourself", + "form_free_message": "Free Message", + "empty_free_message": "Status when availability is 'Free'", + "form_away_message": "Away Message", + "empty_away_message": "Status when availability is 'Away'", + "form_busy_message": "Free Message", + "empty_busy_message": "Status when availability is 'Busy'", + "form_availability": "Availability", + "form_avatar": "Avatar", + "form_auto_away": "Automatic 'away' detection", + "form_auto_away_timeout": "Auto-away timeout (in minutes)", "form_lock_type": "Lock Type", "lock_type_none": "none", "lock_type_pin": "pin", @@ -101,11 +113,40 @@ "invalid_account_title": "Invalid Account", "invalid_account_text": "Account is invalid, removing from list" }, - "contacts_page": { + "contacts_dialog": { "contacts": "Contacts", + "edit_contact": "Edit Contact", "invitations": "Invitations", + "no_contact_selected": "No contact selected", + "new_chat": "New Chat" + }, + "contact_list": { + "contacts": "Contacts", + "invite_people": "Invite people to VeilidChat", + "search": "Search contacts", + "invitation": "Invitation", "loading_contacts": "Loading contacts..." }, + "contact_form": { + "form_name": "Name", + "form_pronouns": "Pronouns", + "form_about": "About", + "form_status": "Current Status", + "form_nickname": "Nickname", + "form_notes": "Notes", + "form_fingerprint": "Fingerprint", + "form_show_availability": "Show availability", + "save": "Save", + "save_disabled": "Save" + }, + "availability": { + "unspecified": "Unspecified", + "offline": "Offline", + "always_show_offline": "Always Show Offline", + "free": "Free", + "busy": "Busy", + "away": "Away" + }, "add_contact_sheet": { "new_contact": "New Contact", "create_invite": "Create Invitation", @@ -188,11 +229,6 @@ "reenter_password": "Re-Enter Password To Confirm", "password_does_not_match": "Password does not match" }, - "contact_list": { - "invite_people": "Invite people to VeilidChat", - "search": "Search contacts", - "invitation": "Invitation" - }, "chat_list": { "search": "Search chats", "start_a_conversation": "Start A Conversation", diff --git a/lib/account_manager/cubits/account_record_cubit.dart b/lib/account_manager/cubits/account_record_cubit.dart index 4028d65..0762926 100644 --- a/lib/account_manager/cubits/account_record_cubit.dart +++ b/lib/account_manager/cubits/account_record_cubit.dart @@ -40,12 +40,43 @@ class AccountRecordCubit extends DefaultDHTRecordCubit { //////////////////////////////////////////////////////////////////////////// // Public Interface - Future updateProfile(proto.Profile profile) async { + Future updateAccount( + AccountSpec accountSpec, + ) async { await record.eventualUpdateProtobuf(proto.Account.fromBuffer, (old) async { - if (old == null || old.profile == profile) { + if (old == null) { return null; } - return old.deepCopy()..profile = profile; + + final newAccount = old.deepCopy() + ..profile.name = accountSpec.name + ..profile.pronouns = accountSpec.pronouns + ..profile.about = accountSpec.about + ..profile.availability = accountSpec.availability + ..profile.status = accountSpec.status + //..profile.avatar = + ..profile.timestamp = Veilid.instance.now().toInt64() + ..invisible = accountSpec.invisible + ..autodetectAway = accountSpec.autoAway + ..autoAwayTimeoutMin = accountSpec.autoAwayTimeout + ..freeMessage = accountSpec.freeMessage + ..awayMessage = accountSpec.awayMessage + ..busyMessage = accountSpec.busyMessage; + + var changed = false; + if (newAccount.profile != old.profile || + newAccount.invisible != old.invisible || + newAccount.autodetectAway != old.autodetectAway || + newAccount.autoAwayTimeoutMin != old.autoAwayTimeoutMin || + newAccount.freeMessage != old.freeMessage || + newAccount.busyMessage != old.busyMessage || + newAccount.awayMessage != old.awayMessage) { + changed = true; + } + if (changed) { + return newAccount; + } + return null; }); } } diff --git a/lib/account_manager/models/account_spec.dart b/lib/account_manager/models/account_spec.dart new file mode 100644 index 0000000..539b8d0 --- /dev/null +++ b/lib/account_manager/models/account_spec.dart @@ -0,0 +1,55 @@ +import 'package:flutter/widgets.dart'; + +import '../../proto/proto.dart' as proto; + +/// Profile and Account configurable fields +/// Some are publicly visible via the proto.Profile +/// Some are privately held as proto.Account configurations +class AccountSpec { + AccountSpec( + {required this.name, + required this.pronouns, + required this.about, + required this.availability, + required this.invisible, + required this.freeMessage, + required this.awayMessage, + required this.busyMessage, + required this.avatar, + required this.autoAway, + required this.autoAwayTimeout}); + + String get status { + late final String status; + switch (availability) { + case proto.Availability.AVAILABILITY_AWAY: + status = awayMessage; + break; + case proto.Availability.AVAILABILITY_BUSY: + status = busyMessage; + break; + case proto.Availability.AVAILABILITY_FREE: + status = freeMessage; + break; + case proto.Availability.AVAILABILITY_UNSPECIFIED: + case proto.Availability.AVAILABILITY_OFFLINE: + status = ''; + break; + } + return status; + } + + //////////////////////////////////////////////////////////////////////////// + + String name; + String pronouns; + String about; + proto.Availability availability; + bool invisible; + String freeMessage; + String awayMessage; + String busyMessage; + ImageProvider? avatar; + bool autoAway; + int autoAwayTimeout; +} diff --git a/lib/account_manager/models/models.dart b/lib/account_manager/models/models.dart index 2860eec..8b785c6 100644 --- a/lib/account_manager/models/models.dart +++ b/lib/account_manager/models/models.dart @@ -1,6 +1,6 @@ export 'account_info.dart'; +export 'account_update_spec.dart'; export 'encryption_key_type.dart'; export 'local_account/local_account.dart'; -export 'new_profile_spec.dart'; export 'per_account_collection_state/per_account_collection_state.dart'; export 'user_login/user_login.dart'; diff --git a/lib/account_manager/models/new_profile_spec.dart b/lib/account_manager/models/new_profile_spec.dart deleted file mode 100644 index 173a382..0000000 --- a/lib/account_manager/models/new_profile_spec.dart +++ /dev/null @@ -1,5 +0,0 @@ -class NewProfileSpec { - NewProfileSpec({required this.name, required this.pronouns}); - String name; - String pronouns; -} diff --git a/lib/account_manager/repository/account_repository.dart b/lib/account_manager/repository/account_repository.dart index 9510997..13954ba 100644 --- a/lib/account_manager/repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository.dart @@ -133,14 +133,14 @@ class AccountRepository { /// with the identity instance, stores the account in the identity key and /// then logs into that account with no password set at this time Future createWithNewSuperIdentity( - proto.Profile newProfile) async { + AccountSpec accountSpec) async { log.debug('Creating super identity'); final wsi = await WritableSuperIdentity.create(); try { final localAccount = await _newLocalAccount( superIdentity: wsi.superIdentity, identitySecret: wsi.identitySecret, - newProfile: newProfile); + accountSpec: accountSpec); // Log in the new account by default with no pin final ok = await login( @@ -154,15 +154,13 @@ class AccountRepository { } } - Future editAccountProfile( - TypedKey superIdentityRecordKey, proto.Profile newProfile) async { - log.debug('Editing profile for $superIdentityRecordKey'); - + Future updateLocalAccount( + TypedKey superIdentityRecordKey, AccountSpec accountSpec) async { final localAccounts = await _localAccounts.get(); final newLocalAccounts = localAccounts.replaceFirstWhere( (x) => x.superIdentity.recordKey == superIdentityRecordKey, - (localAccount) => localAccount!.copyWith(name: newProfile.name)); + (localAccount) => localAccount!.copyWith(name: accountSpec.name)); await _localAccounts.set(newLocalAccounts); _streamController.add(AccountRepositoryChange.localAccounts); @@ -248,7 +246,7 @@ class AccountRepository { Future _newLocalAccount( {required SuperIdentity superIdentity, required SecretKey identitySecret, - required proto.Profile newProfile, + required AccountSpec accountSpec, EncryptionKeyType encryptionKeyType = EncryptionKeyType.none, String encryptionKey = ''}) async { log.debug('Creating new local account'); @@ -285,7 +283,10 @@ class AccountRepository { // Make account object final account = proto.Account() - ..profile = newProfile + ..profile.name = accountSpec.name + ..profile.pronouns = accountSpec.pronouns + ..profile.about = accountSpec.about + ..profile.status = accountSpec.status ..contactList = contactList.toProto() ..contactInvitationRecords = contactInvitationRecords.toProto() ..chatList = chatRecords.toProto(); @@ -309,7 +310,7 @@ class AccountRepository { encryptionKeyType: encryptionKeyType, biometricsEnabled: false, hiddenAccount: false, - name: newProfile.name, + name: accountSpec.name, ); // Add local account object to internal store diff --git a/lib/account_manager/views/edit_account_page.dart b/lib/account_manager/views/edit_account_page.dart index 0554def..4f19429 100644 --- a/lib/account_manager/views/edit_account_page.dart +++ b/lib/account_manager/views/edit_account_page.dart @@ -4,10 +4,8 @@ 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:go_router/go_router.dart'; -import 'package:protobuf/protobuf.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../layout/default_app_bar.dart'; @@ -17,12 +15,12 @@ import '../../theme/theme.dart'; import '../../tools/tools.dart'; import '../../veilid_processor/veilid_processor.dart'; import '../account_manager.dart'; -import 'profile_edit_form.dart'; +import 'edit_profile_form.dart'; class EditAccountPage extends StatefulWidget { const EditAccountPage( {required this.superIdentityRecordKey, - required this.existingProfile, + required this.existingAccount, required this.accountRecord, super.key}); @@ -30,7 +28,7 @@ class EditAccountPage extends StatefulWidget { State createState() => _EditAccountPageState(); final TypedKey superIdentityRecordKey; - final proto.Profile existingProfile; + final proto.Account existingAccount; final OwnedDHTRecordPointer accountRecord; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -38,8 +36,8 @@ class EditAccountPage extends StatefulWidget { properties ..add(DiagnosticsProperty( 'superIdentityRecordKey', superIdentityRecordKey)) - ..add(DiagnosticsProperty( - 'existingProfile', existingProfile)) + ..add(DiagnosticsProperty( + 'existingAccount', existingAccount)) ..add(DiagnosticsProperty( 'accountRecord', accountRecord)); } @@ -52,8 +50,7 @@ class _EditAccountPageState extends WindowSetupState { orientationCapability: OrientationCapability.portraitOnly); Widget _editAccountForm(BuildContext context, - {required Future Function(GlobalKey) - onSubmit}) => + {required Future Function(AccountSpec) onSubmit}) => EditProfileForm( header: translate('edit_account_page.header'), instructions: translate('edit_account_page.instructions'), @@ -61,8 +58,25 @@ class _EditAccountPageState extends WindowSetupState { submitDisabledText: translate('button.waiting_for_network'), onSubmit: onSubmit, initialValueCallback: (key) => switch (key) { - EditProfileForm.formFieldName => widget.existingProfile.name, - EditProfileForm.formFieldPronouns => widget.existingProfile.pronouns, + EditProfileForm.formFieldName => widget.existingAccount.profile.name, + EditProfileForm.formFieldPronouns => + widget.existingAccount.profile.pronouns, + EditProfileForm.formFieldAbout => + widget.existingAccount.profile.about, + EditProfileForm.formFieldAvailability => + widget.existingAccount.profile.availability, + EditProfileForm.formFieldFreeMessage => + widget.existingAccount.freeMessage, + EditProfileForm.formFieldAwayMessage => + widget.existingAccount.awayMessage, + EditProfileForm.formFieldBusyMessage => + widget.existingAccount.busyMessage, + EditProfileForm.formFieldAvatar => + widget.existingAccount.profile.avatar, + EditProfileForm.formFieldAutoAway => + widget.existingAccount.autodetectAway, + EditProfileForm.formFieldAutoAwayTimeout => + widget.existingAccount.autoAwayTimeoutMin, String() => throw UnimplementedError(), }, ); @@ -200,21 +214,11 @@ class _EditAccountPageState extends WindowSetupState { } } - Future _onSubmit(GlobalKey formKey) async { + Future _onSubmit(AccountSpec accountSpec) async { // dismiss the keyboard by unfocusing the textfield FocusScope.of(context).unfocus(); try { - final name = formKey - .currentState!.fields[EditProfileForm.formFieldName]!.value as String; - final pronouns = formKey.currentState! - .fields[EditProfileForm.formFieldPronouns]!.value as String? ?? - ''; - final newProfile = widget.existingProfile.deepCopy() - ..name = name - ..pronouns = pronouns - ..timestamp = Veilid.instance.now().toInt64(); - setState(() { _isInAsyncCall = true; }); @@ -231,11 +235,11 @@ class _EditAccountPageState extends WindowSetupState { // Update account profile DHT record // This triggers ConversationCubits to update - await accountRecordCubit.updateProfile(newProfile); + await accountRecordCubit.updateAccount(accountSpec); // Update local account profile await AccountRepository.instance - .editAccountProfile(widget.superIdentityRecordKey, newProfile); + .updateLocalAccount(widget.superIdentityRecordKey, accountSpec); if (mounted) { Navigator.canPop(context) diff --git a/lib/account_manager/views/edit_profile_form.dart b/lib/account_manager/views/edit_profile_form.dart new file mode 100644 index 0000000..c9a328e --- /dev/null +++ b/lib/account_manager/views/edit_profile_form.dart @@ -0,0 +1,302 @@ +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_translate/flutter_translate.dart'; +import 'package:form_builder_validators/form_builder_validators.dart'; + +import '../../contacts/contacts.dart'; +import '../../proto/proto.dart' as proto; +import '../../theme/theme.dart'; +import '../models/models.dart'; + +class EditProfileForm extends StatefulWidget { + const EditProfileForm({ + required this.header, + required this.instructions, + required this.submitText, + required this.submitDisabledText, + super.key, + this.onSubmit, + this.initialValueCallback, + }); + + @override + State createState() => _EditProfileFormState(); + + final String header; + final String instructions; + final Future Function(AccountSpec)? onSubmit; + final String submitText; + final String submitDisabledText; + final Object? Function(String key)? initialValueCallback; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(StringProperty('header', header)) + ..add(StringProperty('instructions', instructions)) + ..add(ObjectFlagProperty Function(AccountSpec)?>.has( + 'onSubmit', onSubmit)) + ..add(StringProperty('submitText', submitText)) + ..add(StringProperty('submitDisabledText', submitDisabledText)) + ..add(ObjectFlagProperty.has( + 'initialValueCallback', initialValueCallback)); + } + + static const String formFieldName = 'name'; + static const String formFieldPronouns = 'pronouns'; + static const String formFieldAbout = 'about'; + static const String formFieldAvailability = 'availability'; + static const String formFieldFreeMessage = 'free_message'; + static const String formFieldAwayMessage = 'away_message'; + static const String formFieldBusyMessage = 'busy_message'; + static const String formFieldAvatar = 'avatar'; + static const String formFieldAutoAway = 'auto_away'; + static const String formFieldAutoAwayTimeout = 'auto_away_timeout'; +} + +class _EditProfileFormState extends State { + final _formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + } + + FormBuilderDropdown _availabilityDropDown( + BuildContext context) { + final initialValueX = + widget.initialValueCallback?.call(EditProfileForm.formFieldAvailability) + as proto.Availability? ?? + proto.Availability.AVAILABILITY_FREE; + final initialValue = + initialValueX == proto.Availability.AVAILABILITY_UNSPECIFIED + ? proto.Availability.AVAILABILITY_FREE + : initialValueX; + + final availabilities = [ + proto.Availability.AVAILABILITY_FREE, + proto.Availability.AVAILABILITY_AWAY, + proto.Availability.AVAILABILITY_BUSY, + proto.Availability.AVAILABILITY_OFFLINE, + ]; + + return FormBuilderDropdown( + name: EditProfileForm.formFieldAvailability, + initialValue: initialValue, + items: availabilities + .map((x) => DropdownMenuItem( + value: x, + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Icon(AvailabilityWidget.availabilityIcon(x)), + Text(x == proto.Availability.AVAILABILITY_OFFLINE + ? translate('availability.always_show_offline') + : AvailabilityWidget.availabilityName(x)), + ]))) + .toList(), + ); + } + + AccountSpec _makeAccountSpec() { + final name = _formKey + .currentState!.fields[EditProfileForm.formFieldName]!.value as String; + final pronouns = _formKey.currentState! + .fields[EditProfileForm.formFieldPronouns]!.value as String? ?? + ''; + final about = _formKey.currentState!.fields[EditProfileForm.formFieldAbout]! + .value as String? ?? + ''; + final availability = _formKey + .currentState! + .fields[EditProfileForm.formFieldAvailability]! + .value as proto.Availability? ?? + proto.Availability.AVAILABILITY_FREE; + + final invisible = availability == proto.Availability.AVAILABILITY_OFFLINE; + + final freeMessage = _formKey.currentState! + .fields[EditProfileForm.formFieldFreeMessage]!.value as String? ?? + ''; + final awayMessage = _formKey.currentState! + .fields[EditProfileForm.formFieldAwayMessage]!.value as String? ?? + ''; + final busyMessage = _formKey.currentState! + .fields[EditProfileForm.formFieldBusyMessage]!.value as String? ?? + ''; + final autoAway = _formKey.currentState! + .fields[EditProfileForm.formFieldAutoAway]!.value as bool? ?? + false; + final autoAwayTimeout = _formKey.currentState! + .fields[EditProfileForm.formFieldAutoAwayTimeout]!.value as int? ?? + 30; + + return AccountSpec( + name: name, + pronouns: pronouns, + about: about, + availability: availability, + invisible: invisible, + freeMessage: freeMessage, + awayMessage: awayMessage, + busyMessage: busyMessage, + avatar: null, + autoAway: autoAway, + autoAwayTimeout: autoAwayTimeout); + } + + Widget _editProfileForm( + BuildContext context, + ) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + final textTheme = theme.textTheme; + + late final Color border; + if (scaleConfig.useVisualIndicators && !scaleConfig.preferBorders) { + border = scale.primaryScale.elementBackground; + } else { + border = scale.primaryScale.border; + } + + return FormBuilder( + key: _formKey, + child: Column( + children: [ + AvatarWidget( + name: _formKey.currentState?.value[EditProfileForm.formFieldName] + as String? ?? + '?', + size: 128, + borderColor: border, + foregroundColor: scale.primaryScale.primaryText, + backgroundColor: scale.primaryScale.primary, + scaleConfig: scaleConfig, + textStyle: theme.textTheme.titleLarge!.copyWith(fontSize: 64), + ).paddingLTRB(0, 0, 0, 16), + FormBuilderTextField( + autofocus: true, + name: EditProfileForm.formFieldName, + initialValue: widget.initialValueCallback + ?.call(EditProfileForm.formFieldName) as String?, + decoration: InputDecoration( + labelText: translate('account.form_name'), + hintText: translate('account.empty_name')), + maxLength: 64, + // The validator receives the text that the user has entered. + validator: FormBuilderValidators.compose([ + FormBuilderValidators.required(), + ]), + textInputAction: TextInputAction.next, + ), + FormBuilderTextField( + name: EditProfileForm.formFieldPronouns, + initialValue: widget.initialValueCallback + ?.call(EditProfileForm.formFieldPronouns) as String?, + maxLength: 64, + decoration: InputDecoration( + labelText: translate('account.form_pronouns'), + hintText: translate('account.empty_pronouns')), + textInputAction: TextInputAction.next, + ), + FormBuilderTextField( + name: EditProfileForm.formFieldAbout, + initialValue: widget.initialValueCallback + ?.call(EditProfileForm.formFieldAbout) as String?, + maxLength: 1024, + maxLines: 8, + minLines: 1, + decoration: InputDecoration( + labelText: translate('account.form_about'), + hintText: translate('account.empty_about')), + textInputAction: TextInputAction.newline, + ), + _availabilityDropDown(context), + FormBuilderTextField( + name: EditProfileForm.formFieldFreeMessage, + initialValue: widget.initialValueCallback + ?.call(EditProfileForm.formFieldFreeMessage) as String?, + maxLength: 128, + decoration: InputDecoration( + labelText: translate('account.form_free_message'), + hintText: translate('account.empty_free_message')), + textInputAction: TextInputAction.next, + ), + FormBuilderTextField( + name: EditProfileForm.formFieldAwayMessage, + initialValue: widget.initialValueCallback + ?.call(EditProfileForm.formFieldAwayMessage) as String?, + maxLength: 128, + decoration: InputDecoration( + labelText: translate('account.form_away_message'), + hintText: translate('account.empty_away_message')), + textInputAction: TextInputAction.next, + ), + FormBuilderTextField( + name: EditProfileForm.formFieldBusyMessage, + initialValue: widget.initialValueCallback + ?.call(EditProfileForm.formFieldBusyMessage) as String?, + maxLength: 128, + decoration: InputDecoration( + labelText: translate('account.form_busy_message'), + hintText: translate('account.empty_busy_message')), + textInputAction: TextInputAction.next, + ), + FormBuilderCheckbox( + name: EditProfileForm.formFieldAutoAway, + initialValue: widget.initialValueCallback + ?.call(EditProfileForm.formFieldAutoAway) as bool? ?? + false, + side: BorderSide(color: scale.primaryScale.border, width: 2), + title: Text(translate('account.form_auto_away'), + style: textTheme.labelMedium), + ), + FormBuilderTextField( + name: EditProfileForm.formFieldAutoAwayTimeout, + enabled: _formKey.currentState + ?.value[EditProfileForm.formFieldAutoAway] as bool? ?? + false, + initialValue: widget.initialValueCallback + ?.call(EditProfileForm.formFieldAutoAwayTimeout) + as String? ?? + '15', + decoration: InputDecoration( + labelText: translate('account.form_auto_away_timeout'), + ), + validator: FormBuilderValidators.positiveNumber(), + textInputAction: TextInputAction.next, + ), + Row(children: [ + const Spacer(), + Text(widget.instructions).toCenter().flexible(flex: 6), + const Spacer(), + ]).paddingSymmetric(vertical: 4), + ElevatedButton( + onPressed: widget.onSubmit == null + ? null + : () async { + if (_formKey.currentState?.saveAndValidate() ?? false) { + final aus = _makeAccountSpec(); + await widget.onSubmit!(aus); + } + }, + child: Row(mainAxisSize: MainAxisSize.min, children: [ + const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0), + Text((widget.onSubmit == null) + ? widget.submitDisabledText + : widget.submitText) + .paddingLTRB(0, 0, 4, 0) + ]), + ).paddingSymmetric(vertical: 4).alignAtCenterRight(), + ], + ), + ); + } + + @override + Widget build(BuildContext context) => _editProfileForm( + context, + ); +} diff --git a/lib/account_manager/views/new_account_page.dart b/lib/account_manager/views/new_account_page.dart index 943e79e..b107a7b 100644 --- a/lib/account_manager/views/new_account_page.dart +++ b/lib/account_manager/views/new_account_page.dart @@ -3,17 +3,15 @@ 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_form_builder/flutter_form_builder.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:go_router/go_router.dart'; import '../../layout/default_app_bar.dart'; -import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; import '../../tools/tools.dart'; import '../../veilid_processor/veilid_processor.dart'; import '../account_manager.dart'; -import 'profile_edit_form.dart'; +import 'edit_profile_form.dart'; class NewAccountPage extends StatefulWidget { const NewAccountPage({super.key}); @@ -29,7 +27,7 @@ class _NewAccountPageState extends WindowSetupState { orientationCapability: OrientationCapability.portraitOnly); Widget _newAccountForm(BuildContext context, - {required Future Function(GlobalKey) onSubmit}) { + {required Future Function(AccountSpec) onSubmit}) { final networkReady = context .watch() .state @@ -47,28 +45,19 @@ class _NewAccountPageState extends WindowSetupState { onSubmit: !canSubmit ? null : onSubmit); } - Future _onSubmit(GlobalKey formKey) async { + Future _onSubmit(AccountSpec accountSpec) async { // dismiss the keyboard by unfocusing the textfield FocusScope.of(context).unfocus(); try { - final name = formKey - .currentState!.fields[EditProfileForm.formFieldName]!.value as String; - final pronouns = formKey.currentState! - .fields[EditProfileForm.formFieldPronouns]!.value as String? ?? - ''; - final newProfile = proto.Profile() - ..name = name - ..pronouns = pronouns; - setState(() { _isInAsyncCall = true; }); try { final writableSuperIdentity = await AccountRepository.instance - .createWithNewSuperIdentity(newProfile); + .createWithNewSuperIdentity(accountSpec); GoRouterHelper(context).pushReplacement('/new_account/recovery_key', - extra: [writableSuperIdentity, newProfile.name]); + extra: [writableSuperIdentity, accountSpec.name]); } finally { if (mounted) { setState(() { diff --git a/lib/account_manager/views/profile_edit_form.dart b/lib/account_manager/views/profile_edit_form.dart deleted file mode 100644 index cc6e987..0000000 --- a/lib/account_manager/views/profile_edit_form.dart +++ /dev/null @@ -1,118 +0,0 @@ -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_translate/flutter_translate.dart'; -import 'package:form_builder_validators/form_builder_validators.dart'; - -class EditProfileForm extends StatefulWidget { - const EditProfileForm({ - required this.header, - required this.instructions, - required this.submitText, - required this.submitDisabledText, - super.key, - this.onSubmit, - this.initialValueCallback, - }); - - @override - State createState() => _EditProfileFormState(); - - final String header; - final String instructions; - final Future Function(GlobalKey)? onSubmit; - final String submitText; - final String submitDisabledText; - final Object? Function(String key)? initialValueCallback; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(StringProperty('header', header)) - ..add(StringProperty('instructions', instructions)) - ..add(ObjectFlagProperty< - Future Function( - GlobalKey p1)?>.has('onSubmit', onSubmit)) - ..add(StringProperty('submitText', submitText)) - ..add(StringProperty('submitDisabledText', submitDisabledText)) - ..add(ObjectFlagProperty.has( - 'initialValueCallback', initialValueCallback)); - } - - static const String formFieldName = 'name'; - static const String formFieldPronouns = 'pronouns'; -} - -class _EditProfileFormState extends State { - final _formKey = GlobalKey(); - - @override - void initState() { - super.initState(); - } - - Widget _editProfileForm( - BuildContext context, - ) => - FormBuilder( - key: _formKey, - child: Column( - children: [ - Text(widget.header) - .textStyle(context.headlineSmall) - .paddingSymmetric(vertical: 16), - FormBuilderTextField( - autofocus: true, - name: EditProfileForm.formFieldName, - initialValue: widget.initialValueCallback - ?.call(EditProfileForm.formFieldName) as String?, - decoration: - InputDecoration(labelText: translate('account.form_name')), - maxLength: 64, - // The validator receives the text that the user has entered. - validator: FormBuilderValidators.compose([ - FormBuilderValidators.required(), - ]), - textInputAction: TextInputAction.next, - ), - FormBuilderTextField( - name: EditProfileForm.formFieldPronouns, - initialValue: widget.initialValueCallback - ?.call(EditProfileForm.formFieldPronouns) as String?, - maxLength: 64, - decoration: InputDecoration( - labelText: translate('account.form_pronouns')), - textInputAction: TextInputAction.next, - ), - Row(children: [ - const Spacer(), - Text(widget.instructions).toCenter().flexible(flex: 6), - const Spacer(), - ]).paddingSymmetric(vertical: 4), - ElevatedButton( - onPressed: widget.onSubmit == null - ? null - : () async { - if (_formKey.currentState?.saveAndValidate() ?? false) { - await widget.onSubmit!(_formKey); - } - }, - child: Row(mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0), - Text((widget.onSubmit == null) - ? widget.submitDisabledText - : widget.submitText) - .paddingLTRB(0, 0, 4, 0) - ]), - ).paddingSymmetric(vertical: 4).alignAtCenterRight(), - ], - ), - ); - - @override - Widget build(BuildContext context) => _editProfileForm( - context, - ); -} diff --git a/lib/contact_invitation/views/contact_invitation_list_widget.dart b/lib/contact_invitation/views/contact_invitation_list_widget.dart index 56a6b9a..ff3bb8d 100644 --- a/lib/contact_invitation/views/contact_invitation_list_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_list_widget.dart @@ -61,7 +61,7 @@ class ContactInvitationListWidgetState }); _controller.animateTo(_expanded ? 1 : 0); }, - title: translate('contacts_page.invitations'), + title: translate('contacts_dialog.invitations'), sliver: SliverList.builder( itemCount: widget.contactInvitationRecordList.length, itemBuilder: (context, index) { diff --git a/lib/contacts/cubits/contact_list_cubit.dart b/lib/contacts/cubits/contact_list_cubit.dart index 76079b3..d3c6483 100644 --- a/lib/contacts/cubits/contact_list_cubit.dart +++ b/lib/contacts/cubits/contact_list_cubit.dart @@ -71,7 +71,43 @@ class ContactListCubit extends DHTShortArrayCubit { final updated = await writer.tryWriteItemProtobuf( proto.Contact.fromBuffer, pos, newContact); if (!updated) { - throw DHTExceptionOutdated(); + throw const DHTExceptionOutdated(); + } + break; + } + } + }); + } + + Future updateContactFields({ + required TypedKey localConversationRecordKey, + String? nickname, + String? notes, + bool? showAvailability, + }) async { + // Update contact's locally-modifiable fields + await operateWriteEventual((writer) async { + for (var pos = 0; pos < writer.length; pos++) { + final c = await writer.getProtobuf(proto.Contact.fromBuffer, pos); + if (c != null && + c.localConversationRecordKey.toVeilid() == + localConversationRecordKey) { + final newContact = c.deepCopy(); + + if (nickname != null) { + newContact.nickname = nickname; + } + if (notes != null) { + newContact.notes = notes; + } + if (showAvailability != null) { + newContact.showAvailability = showAvailability; + } + + final updated = await writer.tryWriteItemProtobuf( + proto.Contact.fromBuffer, pos, newContact); + if (!updated) { + throw const DHTExceptionOutdated(); } break; } diff --git a/lib/contacts/views/availability_widget.dart b/lib/contacts/views/availability_widget.dart new file mode 100644 index 0000000..8dc66d8 --- /dev/null +++ b/lib/contacts/views/availability_widget.dart @@ -0,0 +1,68 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_translate/flutter_translate.dart'; + +import '../../proto/proto.dart' as proto; + +class AvailabilityWidget extends StatelessWidget { + const AvailabilityWidget({required this.availability, super.key}); + + static IconData availabilityIcon(proto.Availability availability) { + late final IconData iconData; + switch (availability) { + case proto.Availability.AVAILABILITY_AWAY: + iconData = Icons.hot_tub; + case proto.Availability.AVAILABILITY_BUSY: + iconData = Icons.event_busy; + case proto.Availability.AVAILABILITY_FREE: + iconData = Icons.event_available; + case proto.Availability.AVAILABILITY_OFFLINE: + iconData = Icons.cloud_off; + case proto.Availability.AVAILABILITY_UNSPECIFIED: + iconData = Icons.question_mark; + } + return iconData; + } + + static String availabilityName(proto.Availability availability) { + late final String name; + switch (availability) { + case proto.Availability.AVAILABILITY_AWAY: + name = translate('availability.away'); + case proto.Availability.AVAILABILITY_BUSY: + name = translate('availability.busy'); + case proto.Availability.AVAILABILITY_FREE: + name = translate('availability.free'); + case proto.Availability.AVAILABILITY_OFFLINE: + name = translate('availability.offline'); + case proto.Availability.AVAILABILITY_UNSPECIFIED: + name = translate('availability.unspecified'); + } + return name; + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + // final scale = theme.extension()!; + // final scaleConfig = theme.extension()!; + + final name = availabilityName(availability); + final iconData = availabilityIcon(availability); + + return Row(mainAxisSize: MainAxisSize.min, children: [ + Icon(iconData, size: 32), + Text(name, style: textTheme.labelSmall) + ]); + } + + final proto.Availability availability; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add( + DiagnosticsProperty('availability', availability)); + } +} diff --git a/lib/contacts/views/contact_details_widget.dart b/lib/contacts/views/contact_details_widget.dart new file mode 100644 index 0000000..bd4376f --- /dev/null +++ b/lib/contacts/views/contact_details_widget.dart @@ -0,0 +1,41 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../proto/proto.dart' as proto; +import '../contacts.dart'; + +class ContactDetailsWidget extends StatefulWidget { + const ContactDetailsWidget({required this.contact, super.key}); + final proto.Contact contact; + + @override + State createState() => _ContactDetailsWidgetState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('contact', contact)); + } +} + +class _ContactDetailsWidgetState extends State + with SingleTickerProviderStateMixin { + @override + Widget build(BuildContext context) => SingleChildScrollView( + child: EditContactForm( + formKey: GlobalKey(), + contact: widget.contact, + onSubmit: (fbs) async { + final contactList = context.read(); + await contactList.updateContactFields( + localConversationRecordKey: + widget.contact.localConversationRecordKey.toVeilid(), + nickname: fbs.currentState + ?.value[EditContactForm.formFieldNickname] as String, + notes: fbs.currentState?.value[EditContactForm.formFieldNotes] + as String, + showAvailability: fbs.currentState + ?.value[EditContactForm.formFieldShowAvailability] as bool); + })); +} diff --git a/lib/contacts/views/contact_item_widget.dart b/lib/contacts/views/contact_item_widget.dart index a7441e9..7bf2fa4 100644 --- a/lib/contacts/views/contact_item_widget.dart +++ b/lib/contacts/views/contact_item_widget.dart @@ -1,34 +1,36 @@ +import 'package:async_tools/async_tools.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 '../../chat_list/chat_list.dart'; -import '../../layout/layout.dart'; import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; -import '../contacts.dart'; + +const _kOnTap = 'onTap'; +const _kOnDelete = 'onDelete'; class ContactItemWidget extends StatelessWidget { const ContactItemWidget( - {required proto.Contact contact, required bool disabled, super.key}) + {required proto.Contact contact, + required bool disabled, + required bool selected, + Future Function(proto.Contact)? onTap, + Future Function(proto.Contact)? onDoubleTap, + Future Function(proto.Contact)? onDelete, + super.key}) : _disabled = disabled, - _contact = contact; + _selected = selected, + _contact = contact, + _onTap = onTap, + _onDoubleTap = onDoubleTap, + _onDelete = onDelete; @override // ignore: prefer_expression_function_bodies Widget build( BuildContext context, ) { - final localConversationRecordKey = - _contact.localConversationRecordKey.toVeilid(); - - const selected = false; // xxx: eventually when we have selectable contacts: - // activeContactCubit.state == localConversationRecordKey; - - final tileDisabled = _disabled || context.watch().isBusy; - late final String title; late final String subtitle; + if (_contact.nickname.isNotEmpty) { title = _contact.nickname; if (_contact.profile.pronouns.isNotEmpty) { @@ -47,41 +49,33 @@ class ContactItemWidget extends StatelessWidget { return SliderTile( key: ObjectKey(_contact), - disabled: tileDisabled, - selected: selected, + disabled: _disabled, + selected: _selected, tileScale: ScaleKind.primary, title: title, subtitle: subtitle, icon: Icons.person, - onTap: () async { - // Start a chat - final chatListCubit = context.read(); - - await chatListCubit.getOrCreateChatSingleContact(contact: _contact); - // Click over to chats - if (context.mounted) { - await MainPager.of(context) - ?.pageController - .animateToPage(1, duration: 250.ms, curve: Curves.easeInOut); - } - }, + onDoubleTap: _onDoubleTap == null + ? null + : () => singleFuture((this, _kOnTap), () async { + await _onDoubleTap(_contact); + }), + onTap: _onTap == null + ? null + : () => singleFuture((this, _kOnTap), () async { + await _onTap(_contact); + }), endActions: [ - SliderTileAction( + if (_onDelete != null) + SliderTileAction( icon: Icons.delete, label: translate('button.delete'), actionScale: ScaleKind.tertiary, - onPressed: (context) async { - final contactListCubit = context.read(); - final chatListCubit = context.read(); - - // Delete the contact itself - await contactListCubit.deleteContact( - localConversationRecordKey: localConversationRecordKey); - - // Remove any chats for this contact - await chatListCubit.deleteChat( - localConversationRecordKey: localConversationRecordKey); - }) + onPressed: (_context) => + singleFuture((this, _kOnDelete), () async { + await _onDelete(_contact); + }), + ), ], ); } @@ -90,4 +84,8 @@ class ContactItemWidget extends StatelessWidget { final proto.Contact _contact; final bool _disabled; + final bool _selected; + final Future Function(proto.Contact contact)? _onTap; + final Future Function(proto.Contact contact)? _onDoubleTap; + final Future Function(proto.Contact contact)? _onDelete; } diff --git a/lib/contacts/views/contact_list_widget.dart b/lib/contacts/views/contact_list_widget.dart deleted file mode 100644 index f818892..0000000 --- a/lib/contacts/views/contact_list_widget.dart +++ /dev/null @@ -1,86 +0,0 @@ -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_translate/flutter_translate.dart'; -import 'package:searchable_listview/searchable_listview.dart'; - -import '../../proto/proto.dart' as proto; -import '../../theme/theme.dart'; -import 'contact_item_widget.dart'; -import 'empty_contact_list_widget.dart'; - -class ContactListWidget extends StatefulWidget { - const ContactListWidget( - {required this.contactList, required this.disabled, super.key}); - final IList? contactList; - final bool disabled; - - @override - State createState() => _ContactListWidgetState(); - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(IterableProperty('contactList', contactList)) - ..add(DiagnosticsProperty('disabled', disabled)); - } -} - -class _ContactListWidgetState extends State - with SingleTickerProviderStateMixin { - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - //final textTheme = theme.textTheme; - final scale = theme.extension()!; - final scaleConfig = theme.extension()!; - - return SliverLayoutBuilder( - builder: (context, constraints) => styledHeaderSliver( - context: context, - backgroundColor: scaleConfig.preferBorders - ? scale.primaryScale.subtleBackground - : scale.primaryScale.subtleBorder, - title: translate('contacts_page.contacts'), - sliver: SliverFillRemaining( - child: SearchableList.sliver( - initialList: widget.contactList == null - ? [] - : widget.contactList!.toList(), - itemBuilder: (c) => - ContactItemWidget(contact: c, disabled: widget.disabled) - .paddingLTRB(0, 4, 0, 0), - filter: (value) { - final lowerValue = value.toLowerCase(); - if (widget.contactList == null) { - return []; - } - return widget.contactList! - .where((element) => - element.nickname.toLowerCase().contains(lowerValue) || - element.profile.name - .toLowerCase() - .contains(lowerValue) || - element.profile.pronouns - .toLowerCase() - .contains(lowerValue)) - .toList(); - }, - searchFieldHeight: 40, - spaceBetweenSearchAndList: 4, - emptyWidget: widget.contactList == null - ? waitingPage( - text: translate('contacts_page.loading_contacts')) - : const EmptyContactListWidget(), - defaultSuffixIconColor: scale.primaryScale.border, - closeKeyboardWhenScrolling: true, - searchFieldEnabled: widget.contactList != null, - inputDecoration: InputDecoration( - labelText: translate('contact_list.search'), - ), - ), - ))); - } -} diff --git a/lib/contacts/views/contacts_browser.dart b/lib/contacts/views/contacts_browser.dart new file mode 100644 index 0000000..f3b03c9 --- /dev/null +++ b/lib/contacts/views/contacts_browser.dart @@ -0,0 +1,247 @@ +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:searchable_listview/searchable_listview.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../chat_list/chat_list.dart'; +import '../../contact_invitation/contact_invitation.dart'; +import '../../proto/proto.dart' as proto; +import '../../theme/theme.dart'; +import '../cubits/cubits.dart'; +import 'contact_item_widget.dart'; +import 'empty_contact_list_widget.dart'; + +enum ContactsBrowserElementKind { + invitation, + contact, +} + +class ContactsBrowserElement { + ContactsBrowserElement.invitation(proto.ContactInvitationRecord i) + : kind = ContactsBrowserElementKind.invitation, + contact = null, + invitation = i; + ContactsBrowserElement.contact(proto.Contact c) + : kind = ContactsBrowserElementKind.contact, + invitation = null, + contact = c; + + final ContactsBrowserElementKind kind; + final proto.ContactInvitationRecord? invitation; + final proto.Contact? contact; +} + +class ContactsBrowser extends StatefulWidget { + const ContactsBrowser( + {required this.onContactSelected, + required this.onChatStarted, + this.selectedContactRecordKey, + super.key}); + @override + State createState() => _ContactsBrowserState(); + + final Future Function(proto.Contact? contact) onContactSelected; + final Future Function(proto.Contact contact) onChatStarted; + final TypedKey? selectedContactRecordKey; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty( + 'selectedContactRecordKey', selectedContactRecordKey)) + ..add( + ObjectFlagProperty Function(proto.Contact? contact)>.has( + 'onContactSelected', onContactSelected)) + ..add( + ObjectFlagProperty Function(proto.Contact contact)>.has( + 'onChatStarted', onChatStarted)); + } +} + +class _ContactsBrowserState extends State + with SingleTickerProviderStateMixin { + Widget buildInvitationBar(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + + return Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ + Column(mainAxisSize: MainAxisSize.min, children: [ + IconButton( + onPressed: () async { + await CreateInvitationDialog.show(context); + }, + iconSize: 32, + icon: const Icon(Icons.contact_page), + color: scale.primaryScale.hoverBorder, + tooltip: translate('add_contact_sheet.create_invite'), + ) + ]), + Column(mainAxisSize: MainAxisSize.min, children: [ + IconButton( + onPressed: () async { + await ScanInvitationDialog.show(context); + }, + iconSize: 32, + icon: const Icon(Icons.qr_code_scanner), + color: scale.primaryScale.hoverBorder, + tooltip: translate('add_contact_sheet.scan_invite')), + ]), + Column(mainAxisSize: MainAxisSize.min, children: [ + IconButton( + onPressed: () async { + await PasteInvitationDialog.show(context); + }, + iconSize: 32, + icon: const Icon(Icons.paste), + color: scale.primaryScale.hoverBorder, + tooltip: translate('add_contact_sheet.paste_invite'), + ), + ]) + ]).paddingAll(16); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + + final cilState = context.watch().state; + final cilBusy = cilState.busy; + final contactInvitationRecordList = + cilState.state.asData?.value.map((x) => x.value).toIList() ?? + const IListConst([]); + + final ciState = context.watch().state; + final ciBusy = ciState.busy; + final contactList = + ciState.state.asData?.value.map((x) => x.value).toIList(); + + final expansionListData = + >{}; + if (contactInvitationRecordList.isNotEmpty) { + expansionListData[ContactsBrowserElementKind.invitation] = + contactInvitationRecordList + .toList() + .map(ContactsBrowserElement.invitation) + .toList(); + } + if (contactList != null) { + expansionListData[ContactsBrowserElementKind.contact] = + contactList.toList().map(ContactsBrowserElement.contact).toList(); + } + + return Column(children: [ + buildInvitationBar(context), + SearchableList.expansion( + expansionListData: expansionListData, + expansionTitleBuilder: (k) { + final kind = k as ContactsBrowserElementKind; + late final String title; + switch (kind) { + case ContactsBrowserElementKind.contact: + title = translate('contacts_dialog.contacts'); + case ContactsBrowserElementKind.invitation: + title = translate('contacts_dialog.invitations'); + } + + return Center( + child: Text(title, style: textTheme.titleSmall), + ); + }, + expansionInitiallyExpanded: (k) => true, + expansionListBuilder: (_index, element) { + switch (element.kind) { + case ContactsBrowserElementKind.contact: + final contact = element.contact!; + return ContactItemWidget( + contact: contact, + selected: widget.selectedContactRecordKey == + contact.localConversationRecordKey.toVeilid(), + disabled: ciBusy, + onTap: _onTapContact, + onDoubleTap: _onStartChat, + onDelete: _onDeleteContact) + .paddingLTRB(0, 4, 0, 0); + case ContactsBrowserElementKind.invitation: + final invitation = element.invitation!; + return ContactInvitationItemWidget( + contactInvitationRecord: invitation, disabled: cilBusy) + .paddingLTRB(0, 4, 0, 0); + } + }, + filterExpansionData: (value) { + final lowerValue = value.toLowerCase(); + final filteredMap = { + for (final entry in expansionListData.entries) + entry.key: (expansionListData[entry.key] ?? []).where((element) { + switch (element.kind) { + case ContactsBrowserElementKind.contact: + final contact = element.contact!; + return contact.nickname + .toLowerCase() + .contains(lowerValue) || + contact.profile.name + .toLowerCase() + .contains(lowerValue) || + contact.profile.pronouns + .toLowerCase() + .contains(lowerValue); + case ContactsBrowserElementKind.invitation: + final invitation = element.invitation!; + return invitation.message + .toLowerCase() + .contains(lowerValue); + } + }).toList() + }; + return filteredMap; + }, + hideEmptyExpansionItems: true, + searchFieldHeight: 40, + listViewPadding: const EdgeInsets.all(4), + spaceBetweenSearchAndList: 4, + emptyWidget: contactList == null + ? waitingPage(text: translate('contact_list.loading_contacts')) + : const EmptyContactListWidget(), + defaultSuffixIconColor: scale.primaryScale.border, + closeKeyboardWhenScrolling: true, + searchFieldEnabled: contactList != null, + inputDecoration: + InputDecoration(labelText: translate('contact_list.search')), + ).expanded() + ]); + } + + Future _onTapContact(proto.Contact contact) async { + await widget.onContactSelected(contact); + } + + Future _onStartChat(proto.Contact contact) async { + await widget.onChatStarted(contact); + } + + Future _onDeleteContact(proto.Contact contact) async { + final localConversationRecordKey = + contact.localConversationRecordKey.toVeilid(); + + final contactListCubit = context.read(); + final chatListCubit = context.read(); + + // Delete the contact itself + await contactListCubit.deleteContact( + localConversationRecordKey: localConversationRecordKey); + + // Remove any chats for this contact + await chatListCubit.deleteChat( + localConversationRecordKey: localConversationRecordKey); + } +} diff --git a/lib/contacts/views/contacts_dialog.dart b/lib/contacts/views/contacts_dialog.dart new file mode 100644 index 0000000..d0c1c82 --- /dev/null +++ b/lib/contacts/views/contacts_dialog.dart @@ -0,0 +1,140 @@ +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_translate/flutter_translate.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import '../../chat/chat.dart'; +import '../../chat_list/chat_list.dart'; +import '../../proto/proto.dart' as proto; +import '../../contact_invitation/contact_invitation.dart'; +import '../../layout/layout.dart'; +import '../../theme/theme.dart'; +import '../../veilid_processor/veilid_processor.dart'; +import '../contacts.dart'; + +class ContactsDialog extends StatefulWidget { + const ContactsDialog._({required this.modalContext}); + + @override + State createState() => _ContactsDialogState(); + + static Future show(BuildContext modalContext) async { + await showDialog( + context: modalContext, + barrierDismissible: false, + useRootNavigator: false, + builder: (context) => ContactsDialog._(modalContext: modalContext)); + } + + final BuildContext modalContext; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + .add(DiagnosticsProperty('modalContext', modalContext)); + } +} + +class _ContactsDialogState extends State { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + + final enableSplit = !isMobileWidth(context); + final enableLeft = enableSplit || _selectedContact == null; + final enableRight = enableSplit || _selectedContact != null; + + return SizedBox( + width: MediaQuery.of(context).size.width, + child: StyledScaffold( + appBar: DefaultAppBar( + title: Text(!enableSplit && enableRight + ? translate('contacts_dialog.edit_contact') + : translate('contacts_dialog.contacts')), + leading: Navigator.canPop(context) + ? IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + if (!enableSplit && enableRight) { + setState(() { + _selectedContact = null; + }); + } else { + Navigator.pop(context); + } + }, + ) + : null, + actions: [ + if (_selectedContact != null) + IconButton( + icon: const Icon(Icons.chat_bubble), + tooltip: translate('contacts_dialog.new_chat'), + onPressed: () async { + await onChatStarted(_selectedContact!); + }) + ]), + body: LayoutBuilder(builder: (context, constraint) { + final maxWidth = constraint.maxWidth; + + return Row(children: [ + Offstage( + offstage: !enableLeft, + child: SizedBox( + width: enableLeft && !enableRight + ? maxWidth + : (maxWidth / 3).clamp(200, 500), + child: DecoratedBox( + decoration: BoxDecoration( + color: scale.primaryScale.subtleBackground), + child: ContactsBrowser( + selectedContactRecordKey: _selectedContact + ?.localConversationRecordKey + .toVeilid(), + onContactSelected: onContactSelected, + onChatStarted: onChatStarted, + ).paddingAll(8)))), + if (enableRight) + if (_selectedContact == null) + const NoContactWidget().expanded() + else + ContactDetailsWidget(contact: _selectedContact!) + .paddingAll(8) + .expanded(), + ]); + }))); + } + + Future onContactSelected(proto.Contact? contact) async { + setState(() { + _selectedContact = contact; + }); + } + + Future onChatStarted(proto.Contact contact) async { + final chatListCubit = context.read(); + await chatListCubit.getOrCreateChatSingleContact(contact: contact); + + if (mounted) { + context + .read() + .setActiveChat(contact.localConversationRecordKey.toVeilid()); + + Navigator.pop(context); + } + } + + proto.Contact? _selectedContact; +} diff --git a/lib/contacts/views/edit_contact_form.dart b/lib/contacts/views/edit_contact_form.dart new file mode 100644 index 0000000..0e8acc1 --- /dev/null +++ b/lib/contacts/views/edit_contact_form.dart @@ -0,0 +1,174 @@ +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_translate/flutter_translate.dart'; + +import '../../proto/proto.dart' as proto; +import '../../theme/theme.dart'; +import 'availability_widget.dart'; + +class EditContactForm extends StatefulWidget { + const EditContactForm({ + required this.formKey, + required this.contact, + this.onSubmit, + super.key, + }); + + @override + State createState() => _EditContactFormState(); + + final proto.Contact contact; + final Future Function(GlobalKey)? onSubmit; + final GlobalKey formKey; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ObjectFlagProperty< + Future Function( + GlobalKey p1)?>.has('onSubmit', onSubmit)) + ..add(DiagnosticsProperty('contact', contact)) + ..add( + DiagnosticsProperty>('formKey', formKey)); + } + + static const String formFieldNickname = 'nickname'; + static const String formFieldNotes = 'notes'; + static const String formFieldShowAvailability = 'show_availability'; +} + +class _EditContactFormState extends State { + @override + void initState() { + super.initState(); + } + + Widget _availabilityWidget( + BuildContext context, proto.Availability availability) => + AvailabilityWidget(availability: availability); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + final textTheme = theme.textTheme; + + late final Color border; + if (scaleConfig.useVisualIndicators && !scaleConfig.preferBorders) { + border = scale.primaryScale.elementBackground; + } else { + border = scale.primaryScale.border; + } + + return FormBuilder( + key: widget.formKey, + child: Column( + children: [ + AvatarWidget( + name: widget.contact.profile.name, + size: 128, + borderColor: border, + foregroundColor: scale.primaryScale.primaryText, + backgroundColor: scale.primaryScale.primary, + scaleConfig: scaleConfig, + textStyle: theme.textTheme.titleLarge!.copyWith(fontSize: 64), + ).paddingLTRB(0, 0, 0, 16), + SelectableText(widget.contact.profile.name, + style: textTheme.headlineMedium) + .decoratorLabel( + context, + translate('contact_form.form_name'), + scale: scale.secondaryScale, + ) + .paddingSymmetric(vertical: 8), + SelectableText(widget.contact.profile.pronouns, + style: textTheme.headlineSmall) + .decoratorLabel( + context, + translate('contact_form.form_pronouns'), + scale: scale.secondaryScale, + ) + .paddingSymmetric(vertical: 8), + Row(children: [ + _availabilityWidget(context, widget.contact.profile.availability), + SelectableText(widget.contact.profile.status, + style: textTheme.bodyMedium) + .paddingSymmetric(horizontal: 8) + ]) + .decoratorLabel( + context, + translate('contact_form.form_status'), + scale: scale.secondaryScale, + ) + .paddingSymmetric(vertical: 8), + SelectableText(widget.contact.profile.about, + minLines: 1, maxLines: 8, style: textTheme.bodyMedium) + .decoratorLabel( + context, + translate('contact_form.form_about'), + scale: scale.secondaryScale, + ) + .paddingSymmetric(vertical: 8), + SelectableText( + widget.contact.identityPublicKey.value.toVeilid().toString(), + style: textTheme.labelMedium! + .copyWith(fontFamily: 'Source Code Pro')) + .decoratorLabel( + context, + translate('contact_form.form_fingerprint'), + scale: scale.secondaryScale, + ) + .paddingSymmetric(vertical: 8), + Divider(color: border).paddingLTRB(8, 0, 8, 8), + FormBuilderTextField( + autofocus: true, + name: EditContactForm.formFieldNickname, + initialValue: widget.contact.nickname, + decoration: InputDecoration( + labelText: translate('contact_form.form_nickname')), + maxLength: 64, + textInputAction: TextInputAction.next, + ), + FormBuilderCheckbox( + name: EditContactForm.formFieldShowAvailability, + initialValue: widget.contact.showAvailability, + side: BorderSide(color: scale.primaryScale.border, width: 2), + title: Text(translate('contact_form.form_show_availability'), + style: textTheme.labelMedium), + ), + FormBuilderTextField( + name: EditContactForm.formFieldNotes, + initialValue: widget.contact.notes, + minLines: 1, + maxLines: 8, + maxLength: 1024, + decoration: InputDecoration( + labelText: translate('contact_form.form_notes')), + textInputAction: TextInputAction.newline, + ), + ElevatedButton( + onPressed: widget.onSubmit == null + ? null + : () async { + if (widget.formKey.currentState?.saveAndValidate() ?? + false) { + await widget.onSubmit!(widget.formKey); + } + }, + child: Row(mainAxisSize: MainAxisSize.min, children: [ + const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0), + Text((widget.onSubmit == null) + ? translate('contact_form.save') + : translate('contact_form.save')) + .paddingLTRB(0, 0, 4, 0) + ]), + ).paddingSymmetric(vertical: 4).alignAtCenterRight(), + ], + ), + ); + } +} diff --git a/lib/contacts/views/no_contact_widget.dart b/lib/contacts/views/no_contact_widget.dart new file mode 100644 index 0000000..c559d8b --- /dev/null +++ b/lib/contacts/views/no_contact_widget.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_translate/flutter_translate.dart'; + +import '../../theme/models/scale_scheme.dart'; + +class NoContactWidget extends StatelessWidget { + const NoContactWidget({super.key}); + + @override + // ignore: prefer_expression_function_bodies + Widget build( + BuildContext context, + ) { + final theme = Theme.of(context); + final scale = theme.extension()!; + + return DecoratedBox( + decoration: BoxDecoration( + color: scale.primaryScale.appBackground, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.person, + color: scale.primaryScale.subtleBorder, + size: 48, + ), + Text( + textAlign: TextAlign.center, + translate('contacts_dialog.no_contact_selected'), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: scale.primaryScale.subtleBorder, + ), + ), + ], + ), + ); + } +} diff --git a/lib/contacts/views/views.dart b/lib/contacts/views/views.dart index 8c98b0f..55b96d0 100644 --- a/lib/contacts/views/views.dart +++ b/lib/contacts/views/views.dart @@ -1,3 +1,8 @@ +export 'availability_widget.dart'; +export 'contact_details_widget.dart'; export 'contact_item_widget.dart'; -export 'contact_list_widget.dart'; +export 'contacts_browser.dart'; +export 'contacts_dialog.dart'; +export 'edit_contact_form.dart'; export 'empty_contact_list_widget.dart'; +export 'no_contact_widget.dart'; diff --git a/lib/layout/home/drawer_menu/drawer_menu.dart b/lib/layout/home/drawer_menu/drawer_menu.dart index 6d7209c..0821bbb 100644 --- a/lib/layout/home/drawer_menu/drawer_menu.dart +++ b/lib/layout/home/drawer_menu/drawer_menu.dart @@ -40,10 +40,10 @@ class _DrawerMenuState extends State { } void _doEditClick(TypedKey superIdentityRecordKey, - proto.Profile existingProfile, OwnedDHTRecordPointer accountRecord) { + proto.Account existingAccount, OwnedDHTRecordPointer accountRecord) { singleFuture(this, () async { await GoRouterHelper(context).push('/edit_account', - extra: [superIdentityRecordKey, existingProfile, accountRecord]); + extra: [superIdentityRecordKey, existingAccount, accountRecord]); }); } @@ -58,6 +58,45 @@ class _DrawerMenuState extends State { borderRadius: BorderRadius.circular(borderRadius))), child: child); + Widget _makeAvatarWidget({ + required String name, + required double size, + required Color borderColor, + required Color foregroundColor, + required Color backgroundColor, + required ScaleConfig scaleConfig, + required TextStyle textStyle, + ImageProvider? imageProvider, + }) { + final abbrev = name.split(' ').map((s) => s.isEmpty ? '' : s[0]).join(); + late final String shortname; + if (abbrev.length >= 3) { + shortname = abbrev[0] + abbrev[1] + abbrev[abbrev.length - 1]; + } else { + shortname = abbrev; + } + + return Container( + height: size, + width: size, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: scaleConfig.preferBorders + ? Border.all( + color: borderColor, + width: 2 * (size ~/ 32 + 1), + strokeAlign: BorderSide.strokeAlignOutside) + : null, + color: Colors.blue, + ), + child: AvatarImage( + //size: 32, + backgroundImage: imageProvider, + backgroundColor: backgroundColor, + foregroundColor: foregroundColor, + child: Text(shortname, style: textStyle))); + } + Widget _makeAccountWidget( {required String name, required bool selected, @@ -67,13 +106,6 @@ class _DrawerMenuState extends State { required void Function()? callback, required void Function()? footerCallback}) { final theme = Theme.of(context); - final abbrev = name.split(' ').map((s) => s.isEmpty ? '' : s[0]).join(); - late final String shortname; - if (abbrev.length >= 3) { - shortname = abbrev[0] + abbrev[1] + abbrev[abbrev.length - 1]; - } else { - shortname = abbrev; - } late final Color background; late final Color hoverBackground; @@ -99,24 +131,15 @@ class _DrawerMenuState extends State { activeBorder = scale.primary; } - final avatar = Container( - height: 34, - width: 34, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: scaleConfig.preferBorders - ? Border.all( - color: border, - width: 2, - strokeAlign: BorderSide.strokeAlignOutside) - : null, - color: Colors.blue, - ), - child: AvatarImage( - //size: 32, - backgroundColor: loggedIn ? scale.primary : scale.elementBackground, - foregroundColor: loggedIn ? scale.primaryText : scale.subtleText, - child: Text(shortname, style: theme.textTheme.titleLarge))); + final avatar = AvatarWidget( + name: name, + size: 34, + borderColor: border, + foregroundColor: loggedIn ? scale.primaryText : scale.subtleText, + backgroundColor: loggedIn ? scale.primary : scale.elementBackground, + scaleConfig: scaleConfig, + textStyle: theme.textTheme.titleLarge!, + ); return AnimatedPadding( padding: EdgeInsets.fromLTRB(selected ? 0 : 8, selected ? 0 : 2, @@ -190,7 +213,7 @@ class _DrawerMenuState extends State { footerCallback: () { _doEditClick( superIdentityRecordKey, - value.profile, + value, perAccountState.accountInfo.userLogin!.accountRecordInfo .accountRecord); }), diff --git a/lib/layout/home/home_account_ready.dart b/lib/layout/home/home_account_ready.dart index a5dba61..5a6ab2b 100644 --- a/lib/layout/home/home_account_ready.dart +++ b/lib/layout/home/home_account_ready.dart @@ -6,9 +6,10 @@ import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart'; import '../../account_manager/account_manager.dart'; import '../../chat/chat.dart'; +import '../../chat_list/chat_list.dart'; +import '../../contacts/contacts.dart'; import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; -import 'main_pager/main_pager.dart'; class HomeAccountReady extends StatefulWidget { const HomeAccountReady({super.key}); @@ -23,6 +24,75 @@ class _HomeAccountReadyState extends State { super.initState(); } + Widget buildMenuButton() => Builder(builder: (context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + return IconButton( + icon: const Icon(Icons.menu), + color: scaleConfig.preferBorders + ? scale.primaryScale.border + : scale.primaryScale.borderText, + constraints: const BoxConstraints.expand(height: 48, width: 48), + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + scaleConfig.preferBorders + ? scale.primaryScale.hoverElementBackground + : scale.primaryScale.hoverBorder), + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + side: !scaleConfig.useVisualIndicators + ? BorderSide.none + : BorderSide( + strokeAlign: BorderSide.strokeAlignCenter, + color: scaleConfig.preferBorders + ? scale.primaryScale.border + : scale.primaryScale.borderText, + width: 2), + borderRadius: BorderRadius.all( + Radius.circular(12 * scaleConfig.borderRadiusScale))), + )), + tooltip: translate('menu.accounts_menu_tooltip'), + onPressed: () async { + final ctrl = context.read(); + await ctrl.toggle?.call(); + }); + }); + + Widget buildContactsButton() => Builder(builder: (context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + return IconButton( + icon: const Icon(Icons.contacts), + color: scaleConfig.preferBorders + ? scale.primaryScale.border + : scale.primaryScale.borderText, + constraints: const BoxConstraints.expand(height: 48, width: 48), + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + scaleConfig.preferBorders + ? scale.primaryScale.hoverElementBackground + : scale.primaryScale.hoverBorder), + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + side: !scaleConfig.useVisualIndicators + ? BorderSide.none + : BorderSide( + strokeAlign: BorderSide.strokeAlignCenter, + color: scaleConfig.preferBorders + ? scale.primaryScale.border + : scale.primaryScale.borderText, + width: 2), + borderRadius: BorderRadius.all( + Radius.circular(12 * scaleConfig.borderRadiusScale))), + )), + tooltip: translate('menu.contacts_tooltip'), + onPressed: () async { + await ContactsDialog.show(context); + }); + }); + Widget buildUserPanel() => Builder(builder: (context) { final profile = context.select( (c) => c.state.asData!.value.profile); @@ -36,43 +106,14 @@ class _HomeAccountReadyState extends State { : scale.primaryScale.subtleBorder, child: Column(children: [ Row(children: [ - IconButton( - icon: const Icon(Icons.menu), - color: scaleConfig.preferBorders - ? scale.primaryScale.border - : scale.primaryScale.borderText, - constraints: - const BoxConstraints.expand(height: 48, width: 48), - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - scaleConfig.preferBorders - ? scale.primaryScale.hoverElementBackground - : scale.primaryScale.hoverBorder), - shape: WidgetStateProperty.all( - RoundedRectangleBorder( - side: !scaleConfig.useVisualIndicators - ? BorderSide.none - : BorderSide( - strokeAlign: BorderSide.strokeAlignCenter, - color: scaleConfig.preferBorders - ? scale.primaryScale.border - : scale.primaryScale.borderText, - width: 2), - borderRadius: BorderRadius.all(Radius.circular( - 12 * scaleConfig.borderRadiusScale))), - )), - tooltip: translate('menu.settings_tooltip'), - onPressed: () async { - final ctrl = context.read(); - await ctrl.toggle?.call(); - //await GoRouterHelper(context).push('/settings'); - }).paddingLTRB(0, 0, 8, 0), + buildMenuButton().paddingLTRB(0, 0, 8, 0), ProfileWidget( profile: profile, showPronouns: false, ).expanded(), + buildContactsButton().paddingLTRB(8, 0, 0, 0), ]).paddingAll(8), - MainPager(key: _mainPagerKey).expanded() + const ChatListWidget().expanded() ])); }); @@ -156,7 +197,4 @@ class _HomeAccountReadyState extends State { ]); }); } - - //////////////////////////////////////////////////////////////////////////// - final _mainPagerKey = GlobalKey(debugLabel: '_mainPagerKey'); } diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index a941220..5fd463a 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -132,7 +132,14 @@ class HomeScreenState extends State // Re-export all ready blocs to the account display subtree return perAccountCollectionState.provide( - child: const HomeAccountReady()); + child: Navigator( + onPopPage: (route, result) { + if (!route.didPop(result)) { + return false; + } + return true; + }, + pages: const [MaterialPage(child: HomeAccountReady())])); } } diff --git a/lib/layout/home/main_pager/bottom_sheet_action_button.dart b/lib/layout/home/main_pager/bottom_sheet_action_button.dart deleted file mode 100644 index 494cdb4..0000000 --- a/lib/layout/home/main_pager/bottom_sheet_action_button.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -class BottomSheetActionButton extends StatefulWidget { - const BottomSheetActionButton( - {required this.bottomSheetBuilder, - required this.builder, - this.foregroundColor, - this.backgroundColor, - this.shape, - super.key}); - final Color? foregroundColor; - final Color? backgroundColor; - final ShapeBorder? shape; - final Widget Function(BuildContext) builder; - final Widget Function(BuildContext) bottomSheetBuilder; - - @override - BottomSheetActionButtonState createState() => BottomSheetActionButtonState(); - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(ObjectFlagProperty.has( - 'bottomSheetBuilder', bottomSheetBuilder)) - ..add(ColorProperty('foregroundColor', foregroundColor)) - ..add(ColorProperty('backgroundColor', backgroundColor)) - ..add(DiagnosticsProperty('shape', shape)) - ..add(ObjectFlagProperty.has( - 'builder', builder)); - } -} - -class BottomSheetActionButtonState extends State { - bool _showFab = true; - - @override - void initState() { - super.initState(); - } - - @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - // - return _showFab - ? FloatingActionButton( - elevation: 0, - heroTag: this, - hoverElevation: 0, - shape: widget.shape, - foregroundColor: widget.foregroundColor, - backgroundColor: widget.backgroundColor, - child: widget.builder(context), - onPressed: () async { - await showModalBottomSheet( - context: context, builder: widget.bottomSheetBuilder); - }, - ) - : Container(); - } - - void showFloatingActionButton(bool value) { - setState(() { - _showFab = value; - }); - } -} diff --git a/lib/layout/home/main_pager/chats_page.dart b/lib/layout/home/main_pager/chats_page.dart deleted file mode 100644 index 2146b3d..0000000 --- a/lib/layout/home/main_pager/chats_page.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../../chat_list/chat_list.dart'; - -class ChatsPage extends StatefulWidget { - const ChatsPage({super.key}); - - @override - ChatsPageState createState() => ChatsPageState(); -} - -class ChatsPageState extends State { - @override - void initState() { - super.initState(); - } - - @override - void dispose() { - super.dispose(); - } - - @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return const ChatListWidget(); - } -} diff --git a/lib/layout/home/main_pager/contacts_page.dart b/lib/layout/home/main_pager/contacts_page.dart deleted file mode 100644 index c3699f2..0000000 --- a/lib/layout/home/main_pager/contacts_page.dart +++ /dev/null @@ -1,58 +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_bloc/flutter_bloc.dart'; - -import '../../../contact_invitation/contact_invitation.dart'; -import '../../../contacts/contacts.dart'; - -class ContactsPage extends StatefulWidget { - const ContactsPage({ - super.key, - }); - - @override - ContactsPageState createState() => ContactsPageState(); -} - -class ContactsPageState extends State { - @override - void initState() { - super.initState(); - } - - @override - void dispose() { - super.dispose(); - } - - @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - // final theme = Theme.of(context); - // final textTheme = theme.textTheme; - // final scale = theme.extension()!; - // final scaleConfig = theme.extension()!; - - final cilState = context.watch().state; - final cilBusy = cilState.busy; - final contactInvitationRecordList = - cilState.state.asData?.value.map((x) => x.value).toIList() ?? - const IListConst([]); - - final ciState = context.watch().state; - final ciBusy = ciState.busy; - final contactList = - ciState.state.asData?.value.map((x) => x.value).toIList(); - - return CustomScrollView(slivers: [ - if (contactInvitationRecordList.isNotEmpty) - SliverPadding( - padding: const EdgeInsets.only(bottom: 8), - sliver: ContactInvitationListWidget( - contactInvitationRecordList: contactInvitationRecordList, - disabled: cilBusy)), - ContactListWidget(contactList: contactList, disabled: ciBusy) - ]).paddingLTRB(8, 0, 8, 8); - } -} diff --git a/lib/layout/home/main_pager/main_pager.dart b/lib/layout/home/main_pager/main_pager.dart deleted file mode 100644 index 68ef39b..0000000 --- a/lib/layout/home/main_pager/main_pager.dart +++ /dev/null @@ -1,242 +0,0 @@ -import 'dart:async'; - -import 'package:animated_bottom_navigation_bar/' - 'animated_bottom_navigation_bar.dart'; -import 'package:awesome_extensions/awesome_extensions.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter_animate/flutter_animate.dart'; -import 'package:flutter_translate/flutter_translate.dart'; -import 'package:preload_page_view/preload_page_view.dart'; -import 'package:provider/provider.dart'; - -import '../../../chat/chat.dart'; -import '../../../contact_invitation/contact_invitation.dart'; -import '../../../theme/theme.dart'; -import 'bottom_sheet_action_button.dart'; -import 'chats_page.dart'; -import 'contacts_page.dart'; - -class MainPager extends StatefulWidget { - const MainPager({super.key}); - - @override - MainPagerState createState() => MainPagerState(); - - static MainPagerState? of(BuildContext context) => - context.findAncestorStateOfType(); -} - -class MainPagerState extends State with TickerProviderStateMixin { - ////////////////////////////////////////////////////////////////// - - @override - void initState() { - super.initState(); - } - - @override - void dispose() { - pageController.dispose(); - super.dispose(); - } - - Future scanContactInvitationDialog(BuildContext context) async { - await showDialog( - context: context, - // ignore: prefer_expression_function_bodies - builder: (context) { - return AlertDialog( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(20)), - ), - contentPadding: const EdgeInsets.only( - top: 10, - ), - title: const Text( - 'Scan Contact Invite', - style: TextStyle(fontSize: 24), - ), - content: ScanInvitationDialog( - locator: context.read, - )); - }); - } - - Widget _buildBottomBarItem(int index, bool isActive) { - final theme = Theme.of(context); - final scale = theme.extension()!; - final scaleConfig = theme.extension()!; - - final color = scaleConfig.useVisualIndicators - ? (scaleConfig.preferBorders - ? scale.primaryScale.border - : scale.primaryScale.borderText) - : (isActive - ? (scaleConfig.preferBorders - ? scale.primaryScale.border - : scale.primaryScale.borderText) - : (scaleConfig.preferBorders - ? scale.primaryScale.subtleBorder - : scale.primaryScale.borderText.withAlpha(0x80))); - - final item = Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - _selectedIconList[index], - size: 24, - color: color, - ), - const SizedBox(height: 4), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Text( - _bottomLabelList[index], - style: theme.textTheme.labelMedium!.copyWith( - fontWeight: isActive ? FontWeight.bold : FontWeight.normal, - color: color), - ), - ) - ], - ); - - if (scaleConfig.useVisualIndicators && isActive) { - return DecoratedBox( - decoration: ShapeDecoration( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 14 * scaleConfig.borderRadiusScale), - side: BorderSide( - width: 2, - color: scaleConfig.preferBorders - ? scale.primaryScale.border - : scale.primaryScale.borderText))), - child: item) - .paddingLTRB(8, 0, 8, 6); - } - - return item; - } - - Widget _bottomSheetBuilder(BuildContext sheetContext, BuildContext context) { - if (currentPage == 0) { - // New contact invitation - return newContactBottomSheetBuilder(sheetContext, context); - } else if (currentPage == 1) { - // New chat - return newChatBottomSheetBuilder(sheetContext, context); - } else { - // Unknown error - return debugPage('unknown page'); - } - } - - @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - final theme = Theme.of(context); - final scale = theme.extension()!; - final scaleConfig = theme.extension()!; - - return Scaffold( - //extendBody: true, - backgroundColor: Colors.transparent, - body: PreloadPageView( - key: _pageViewKey, - controller: pageController, - preloadPagesCount: 2, - onPageChanged: (index) { - setState(() { - currentPage = index; - }); - }, - children: const [ - ContactsPage(), - ChatsPage(), - ]), - // appBar: AppBar( - // toolbarHeight: 24, - // title: Text( - // 'C', - // style: Theme.of(context).textTheme.headlineSmall, - // ), - // ), - bottomNavigationBar: AnimatedBottomNavigationBar.builder( - itemCount: 2, - height: 64, - tabBuilder: _buildBottomBarItem, - activeIndex: currentPage, - gapLocation: GapLocation.end, - gapWidth: 90, - notchSmoothness: NotchSmoothness.defaultEdge, - notchMargin: 4, - backgroundColor: scaleConfig.preferBorders - ? scale.primaryScale.hoverElementBackground - : scale.primaryScale.hoverBorder, - elevation: 0, - onTap: (index) async { - await pageController.animateToPage(index, - duration: 250.ms, curve: Curves.easeInOut); - }, - ), - floatingActionButton: BottomSheetActionButton( - shape: CircleBorder( - side: !scaleConfig.useVisualIndicators - ? BorderSide.none - : BorderSide( - strokeAlign: BorderSide.strokeAlignCenter, - color: scaleConfig.preferBorders - ? scale.secondaryScale.border - : scale.secondaryScale.borderText, - width: 2), - ), - foregroundColor: scaleConfig.preferBorders - ? scale.secondaryScale.border - : scale.secondaryScale.borderText, - backgroundColor: scaleConfig.preferBorders - ? scale.secondaryScale.hoverElementBackground - : scale.secondaryScale.hoverBorder, - builder: (context) => Icon( - _fabIconList[currentPage], - color: scaleConfig.preferBorders - ? scale.secondaryScale.border - : scale.secondaryScale.borderText, - ), - bottomSheetBuilder: (sheetContext) => - _bottomSheetBuilder(sheetContext, context)), - floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, - ); - } - - ////////////////////////////////////////////////////////////////// - - final _selectedIconList = [Icons.person, Icons.chat]; - // final _unselectedIconList = [ - // Icons.chat_outlined, - // Icons.person_outlined - // ]; - final _fabIconList = [ - Icons.person_add_sharp, - Icons.chat, - ]; - final _bottomLabelList = [ - translate('pager.contacts'), - translate('pager.chats'), - ]; - final _pageViewKey = GlobalKey(debugLabel: '_pageViewKey'); - - // key-accessible controller - int currentPage = 0; - final pageController = PreloadPageController(); - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(IntProperty('currentPage', currentPage)) - ..add(DiagnosticsProperty( - 'pageController', pageController)); - } -} diff --git a/lib/layout/layout.dart b/lib/layout/layout.dart index 27975d5..a744264 100644 --- a/lib/layout/layout.dart +++ b/lib/layout/layout.dart @@ -1,4 +1,3 @@ export 'default_app_bar.dart'; export 'home/home.dart'; -export 'home/main_pager/main_pager.dart'; export 'splash.dart'; diff --git a/lib/proto/veilidchat.pb.dart b/lib/proto/veilidchat.pb.dart index 63bd910..5152594 100644 --- a/lib/proto/veilidchat.pb.dart +++ b/lib/proto/veilidchat.pb.dart @@ -1647,11 +1647,15 @@ class Account extends $pb.GeneratedMessage { 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) + ..a<$core.int>(3, _omitFieldNames ? '' : 'autoAwayTimeoutMin', $pb.PbFieldType.OU3) ..aOM<$1.OwnedDHTRecordPointer>(4, _omitFieldNames ? '' : 'contactList', subBuilder: $1.OwnedDHTRecordPointer.create) ..aOM<$1.OwnedDHTRecordPointer>(5, _omitFieldNames ? '' : 'contactInvitationRecords', subBuilder: $1.OwnedDHTRecordPointer.create) ..aOM<$1.OwnedDHTRecordPointer>(6, _omitFieldNames ? '' : 'chatList', subBuilder: $1.OwnedDHTRecordPointer.create) ..aOM<$1.OwnedDHTRecordPointer>(7, _omitFieldNames ? '' : 'groupChatList', subBuilder: $1.OwnedDHTRecordPointer.create) + ..aOS(8, _omitFieldNames ? '' : 'freeMessage') + ..aOS(9, _omitFieldNames ? '' : 'busyMessage') + ..aOS(10, _omitFieldNames ? '' : 'awayMessage') + ..aOB(11, _omitFieldNames ? '' : 'autodetectAway') ..hasRequiredFields = false ; @@ -1697,13 +1701,13 @@ class Account extends $pb.GeneratedMessage { void clearInvisible() => clearField(2); @$pb.TagNumber(3) - $core.int get autoAwayTimeoutSec => $_getIZ(2); + $core.int get autoAwayTimeoutMin => $_getIZ(2); @$pb.TagNumber(3) - set autoAwayTimeoutSec($core.int v) { $_setUnsignedInt32(2, v); } + set autoAwayTimeoutMin($core.int v) { $_setUnsignedInt32(2, v); } @$pb.TagNumber(3) - $core.bool hasAutoAwayTimeoutSec() => $_has(2); + $core.bool hasAutoAwayTimeoutMin() => $_has(2); @$pb.TagNumber(3) - void clearAutoAwayTimeoutSec() => clearField(3); + void clearAutoAwayTimeoutMin() => clearField(3); @$pb.TagNumber(4) $1.OwnedDHTRecordPointer get contactList => $_getN(3); @@ -1748,6 +1752,42 @@ class Account extends $pb.GeneratedMessage { void clearGroupChatList() => clearField(7); @$pb.TagNumber(7) $1.OwnedDHTRecordPointer ensureGroupChatList() => $_ensure(6); + + @$pb.TagNumber(8) + $core.String get freeMessage => $_getSZ(7); + @$pb.TagNumber(8) + set freeMessage($core.String v) { $_setString(7, v); } + @$pb.TagNumber(8) + $core.bool hasFreeMessage() => $_has(7); + @$pb.TagNumber(8) + void clearFreeMessage() => clearField(8); + + @$pb.TagNumber(9) + $core.String get busyMessage => $_getSZ(8); + @$pb.TagNumber(9) + set busyMessage($core.String v) { $_setString(8, v); } + @$pb.TagNumber(9) + $core.bool hasBusyMessage() => $_has(8); + @$pb.TagNumber(9) + void clearBusyMessage() => clearField(9); + + @$pb.TagNumber(10) + $core.String get awayMessage => $_getSZ(9); + @$pb.TagNumber(10) + set awayMessage($core.String v) { $_setString(9, v); } + @$pb.TagNumber(10) + $core.bool hasAwayMessage() => $_has(9); + @$pb.TagNumber(10) + void clearAwayMessage() => clearField(10); + + @$pb.TagNumber(11) + $core.bool get autodetectAway => $_getBF(10); + @$pb.TagNumber(11) + set autodetectAway($core.bool v) { $_setBool(10, v); } + @$pb.TagNumber(11) + $core.bool hasAutodetectAway() => $_has(10); + @$pb.TagNumber(11) + void clearAutodetectAway() => clearField(11); } class Contact extends $pb.GeneratedMessage { diff --git a/lib/proto/veilidchat.pbjson.dart b/lib/proto/veilidchat.pbjson.dart index fe6cac3..ec327f4 100644 --- a/lib/proto/veilidchat.pbjson.dart +++ b/lib/proto/veilidchat.pbjson.dart @@ -467,24 +467,31 @@ const Account$json = { '2': [ {'1': 'profile', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'profile'}, {'1': 'invisible', '3': 2, '4': 1, '5': 8, '10': 'invisible'}, - {'1': 'auto_away_timeout_sec', '3': 3, '4': 1, '5': 13, '10': 'autoAwayTimeoutSec'}, + {'1': 'auto_away_timeout_min', '3': 3, '4': 1, '5': 13, '10': 'autoAwayTimeoutMin'}, {'1': 'contact_list', '3': 4, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'contactList'}, {'1': 'contact_invitation_records', '3': 5, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'contactInvitationRecords'}, {'1': 'chat_list', '3': 6, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'chatList'}, {'1': 'group_chat_list', '3': 7, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'groupChatList'}, + {'1': 'free_message', '3': 8, '4': 1, '5': 9, '10': 'freeMessage'}, + {'1': 'busy_message', '3': 9, '4': 1, '5': 9, '10': 'busyMessage'}, + {'1': 'away_message', '3': 10, '4': 1, '5': 9, '10': 'awayMessage'}, + {'1': 'autodetect_away', '3': 11, '4': 1, '5': 8, '10': 'autodetectAway'}, ], }; /// Descriptor for `Account`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List accountDescriptor = $convert.base64Decode( 'CgdBY2NvdW50Ei0KB3Byb2ZpbGUYASABKAsyEy52ZWlsaWRjaGF0LlByb2ZpbGVSB3Byb2ZpbG' - 'USHAoJaW52aXNpYmxlGAIgASgIUglpbnZpc2libGUSMQoVYXV0b19hd2F5X3RpbWVvdXRfc2Vj' - 'GAMgASgNUhJhdXRvQXdheVRpbWVvdXRTZWMSPQoMY29udGFjdF9saXN0GAQgASgLMhouZGh0Lk' + 'USHAoJaW52aXNpYmxlGAIgASgIUglpbnZpc2libGUSMQoVYXV0b19hd2F5X3RpbWVvdXRfbWlu' + 'GAMgASgNUhJhdXRvQXdheVRpbWVvdXRNaW4SPQoMY29udGFjdF9saXN0GAQgASgLMhouZGh0Lk' '93bmVkREhUUmVjb3JkUG9pbnRlclILY29udGFjdExpc3QSWAoaY29udGFjdF9pbnZpdGF0aW9u' 'X3JlY29yZHMYBSABKAsyGi5kaHQuT3duZWRESFRSZWNvcmRQb2ludGVyUhhjb250YWN0SW52aX' 'RhdGlvblJlY29yZHMSNwoJY2hhdF9saXN0GAYgASgLMhouZGh0Lk93bmVkREhUUmVjb3JkUG9p' 'bnRlclIIY2hhdExpc3QSQgoPZ3JvdXBfY2hhdF9saXN0GAcgASgLMhouZGh0Lk93bmVkREhUUm' - 'Vjb3JkUG9pbnRlclINZ3JvdXBDaGF0TGlzdA=='); + 'Vjb3JkUG9pbnRlclINZ3JvdXBDaGF0TGlzdBIhCgxmcmVlX21lc3NhZ2UYCCABKAlSC2ZyZWVN' + 'ZXNzYWdlEiEKDGJ1c3lfbWVzc2FnZRgJIAEoCVILYnVzeU1lc3NhZ2USIQoMYXdheV9tZXNzYW' + 'dlGAogASgJUgthd2F5TWVzc2FnZRInCg9hdXRvZGV0ZWN0X2F3YXkYCyABKAhSDmF1dG9kZXRl' + 'Y3RBd2F5'); @$core.Deprecated('Use contactDescriptor instead') const Contact$json = { diff --git a/lib/proto/veilidchat.proto b/lib/proto/veilidchat.proto index 794cef8..0d4ca0a 100644 --- a/lib/proto/veilidchat.proto +++ b/lib/proto/veilidchat.proto @@ -319,13 +319,13 @@ message Chat { // Pronouns - Pronouns of user // Icon - Little picture to represent user in contact list message Profile { - // Friendy name + // Friendy name (max length 64) string name = 1; - // Pronouns of user + // Pronouns of user (max length 64) string pronouns = 2; - // Description of the user + // Description of the user (max length 1024) string about = 3; - // Status/away message + // Status/away message (max length 128) string status = 4; // Availability Availability availability = 5; @@ -345,8 +345,8 @@ message Account { Profile profile = 1; // Invisibility makes you always look 'Offline' bool invisible = 2; - // Auto-away sets 'away' mode after an inactivity time - uint32 auto_away_timeout_sec = 3; + // Auto-away sets 'away' mode after an inactivity time (only if autodetect_away is set) + uint32 auto_away_timeout_min = 3; // The contacts DHTList for this account // DHT Private dht.OwnedDHTRecordPointer contact_list = 4; @@ -359,6 +359,15 @@ message Account { // The GroupChats DHTList for this account // DHT Private dht.OwnedDHTRecordPointer group_chat_list = 7; + // Free message (max length 128) + string free_message = 8; + // Busy message (max length 128) + string busy_message = 9; + // Away message (max length 128) + string away_message = 10; + // Auto-detect away + bool autodetect_away = 11; + } // A record of a contact that has accepted a contact invitation diff --git a/lib/router/cubits/router_cubit.dart b/lib/router/cubits/router_cubit.dart index 95f2bf7..d442485 100644 --- a/lib/router/cubits/router_cubit.dart +++ b/lib/router/cubits/router_cubit.dart @@ -72,7 +72,7 @@ class RouterCubit extends Cubit { final extra = state.extra! as List; return EditAccountPage( superIdentityRecordKey: extra[0]! as TypedKey, - existingProfile: extra[1]! as proto.Profile, + existingAccount: extra[1]! as proto.Account, accountRecord: extra[2]! as OwnedDHTRecordPointer, ); }, diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart index ac21fc4..94606aa 100644 --- a/lib/settings/settings_page.dart +++ b/lib/settings/settings_page.dart @@ -47,7 +47,9 @@ class SettingsPageState extends State { child: ListView( children: [ buildSettingsPageColorPreferences( - context: context, onChanged: () => setState(() {})), + context: context, + onChanged: () => setState(() {})) + .paddingLTRB(0, 8, 0, 0), buildSettingsPageBrightnessPreferences( context: context, onChanged: () => setState(() {})), buildSettingsPageNotificationPreferences( diff --git a/lib/theme/models/slider_tile.dart b/lib/theme/models/slider_tile.dart index e6c4711..7631303 100644 --- a/lib/theme/models/slider_tile.dart +++ b/lib/theme/models/slider_tile.dart @@ -30,6 +30,7 @@ class SliderTile extends StatelessWidget { this.endActions = const [], this.startActions = const [], this.onTap, + this.onDoubleTap, this.icon, super.key}); @@ -39,6 +40,7 @@ class SliderTile extends StatelessWidget { final List endActions; final List startActions; final GestureTapCallback? onTap; + final GestureTapCallback? onDoubleTap; final IconData? icon; final String title; final String subtitle; @@ -55,7 +57,9 @@ class SliderTile extends StatelessWidget { ..add(ObjectFlagProperty.has('onTap', onTap)) ..add(DiagnosticsProperty('icon', icon)) ..add(StringProperty('title', title)) - ..add(StringProperty('subtitle', subtitle)); + ..add(StringProperty('subtitle', subtitle)) + ..add(ObjectFlagProperty.has( + 'onDoubleTap', onDoubleTap)); } @override @@ -138,18 +142,20 @@ class SliderTile extends StatelessWidget { padding: scaleConfig.useVisualIndicators ? EdgeInsets.zero : const EdgeInsets.fromLTRB(0, 2, 0, 2), - child: ListTile( - onTap: onTap, - dense: true, - visualDensity: const VisualDensity(vertical: -4), - title: Text( - title, - overflow: TextOverflow.fade, - softWrap: false, - ), - subtitle: subtitle.isNotEmpty ? Text(subtitle) : null, - iconColor: textColor, - textColor: textColor, - leading: icon == null ? null : Icon(icon))))); + child: GestureDetector( + onDoubleTap: onDoubleTap, + child: ListTile( + onTap: onTap, + dense: true, + visualDensity: const VisualDensity(vertical: -4), + title: Text( + title, + overflow: TextOverflow.fade, + softWrap: false, + ), + subtitle: subtitle.isNotEmpty ? Text(subtitle) : null, + iconColor: textColor, + textColor: textColor, + leading: icon == null ? null : Icon(icon)))))); } } diff --git a/lib/theme/views/avatar_widget.dart b/lib/theme/views/avatar_widget.dart new file mode 100644 index 0000000..43d351b --- /dev/null +++ b/lib/theme/views/avatar_widget.dart @@ -0,0 +1,77 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +import '../theme.dart'; + +class AvatarWidget extends StatelessWidget { + AvatarWidget({ + required String name, + required double size, + required Color borderColor, + required Color foregroundColor, + required Color backgroundColor, + required ScaleConfig scaleConfig, + required TextStyle textStyle, + super.key, + ImageProvider? imageProvider, + }) : _name = name, + _size = size, + _borderColor = borderColor, + _foregroundColor = foregroundColor, + _backgroundColor = backgroundColor, + _scaleConfig = scaleConfig, + _textStyle = textStyle, + _imageProvider = imageProvider; + + @override + Widget build(BuildContext context) { + final abbrev = _name.split(' ').map((s) => s.isEmpty ? '' : s[0]).join(); + late final String shortname; + if (abbrev.length >= 3) { + shortname = abbrev[0] + abbrev[1] + abbrev[abbrev.length - 1]; + } else { + shortname = abbrev; + } + + return Container( + height: _size, + width: _size, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: _scaleConfig.preferBorders + ? Border.all( + color: _borderColor, + width: 1 * (_size ~/ 32 + 1), + strokeAlign: BorderSide.strokeAlignOutside) + : null, + color: _borderColor, + ), + child: AvatarImage( + //size: 32, + backgroundImage: _imageProvider, + backgroundColor: + _scaleConfig.useVisualIndicators && !_scaleConfig.preferBorders + ? _foregroundColor + : _backgroundColor, + child: Text( + shortname, + style: _textStyle.copyWith( + color: _scaleConfig.useVisualIndicators && + !_scaleConfig.preferBorders + ? _backgroundColor + : _foregroundColor, + ), + ))); + } + + //////////////////////////////////////////////////////////////////////////// + final String _name; + final double _size; + final Color _borderColor; + final Color _foregroundColor; + final Color _backgroundColor; + final ScaleConfig _scaleConfig; + final TextStyle _textStyle; + final ImageProvider? _imageProvider; +} diff --git a/lib/theme/views/styled_dialog.dart b/lib/theme/views/styled_dialog.dart index b48a8fb..4e4bd50 100644 --- a/lib/theme/views/styled_dialog.dart +++ b/lib/theme/views/styled_dialog.dart @@ -50,6 +50,7 @@ class StyledDialog extends StatelessWidget { required Widget child}) async => showDialog( context: context, + useRootNavigator: false, builder: (context) => StyledDialog(title: title, child: child)); final String title; diff --git a/lib/theme/views/styled_scaffold.dart b/lib/theme/views/styled_scaffold.dart index af9e4eb..61e32aa 100644 --- a/lib/theme/views/styled_scaffold.dart +++ b/lib/theme/views/styled_scaffold.dart @@ -12,15 +12,15 @@ class StyledScaffold extends StatelessWidget { final scale = theme.extension()!; final scaleConfig = theme.extension()!; - final scaffold = isDesktop - ? clipBorder( - clipEnabled: true, - borderEnabled: scaleConfig.useVisualIndicators, - borderRadius: 16 * scaleConfig.borderRadiusScale, - borderColor: scale.primaryScale.border, - child: Scaffold(appBar: appBar, body: body, key: key)) - .paddingAll(32) - : Scaffold(appBar: appBar, body: body, key: key); + final enableBorder = !isMobileWidth(context); + + final scaffold = clipBorder( + clipEnabled: enableBorder, + borderEnabled: scaleConfig.useVisualIndicators, + borderRadius: 16 * scaleConfig.borderRadiusScale, + borderColor: scale.primaryScale.border, + child: Scaffold(appBar: appBar, body: body, key: key)) + .paddingAll(enableBorder ? 32 : 0); return GestureDetector( onTap: () => FocusManager.instance.primaryFocus?.unfocus(), diff --git a/lib/theme/views/views.dart b/lib/theme/views/views.dart index 642255e..b81f184 100644 --- a/lib/theme/views/views.dart +++ b/lib/theme/views/views.dart @@ -1,3 +1,4 @@ +export 'avatar_widget.dart'; export 'brightness_preferences.dart'; export 'color_preferences.dart'; export 'enter_password.dart'; diff --git a/lib/theme/views/widget_helpers.dart b/lib/theme/views/widget_helpers.dart index 4beb48b..158cc6c 100644 --- a/lib/theme/views/widget_helpers.dart +++ b/lib/theme/views/widget_helpers.dart @@ -41,6 +41,44 @@ extension ModalProgressExt on Widget { } } +extension LabelExt on Widget { + Widget decoratorLabel(BuildContext context, String label, + {ScaleColor? scale}) { + final theme = Theme.of(context); + final scaleScheme = theme.extension()!; + final scaleConfig = theme.extension()!; + scale = scale ?? scaleScheme.primaryScale; + + final border = scale.border; + final disabledBorder = scaleScheme.grayScale.border; + final hoverBorder = scale.hoverBorder; + final focusedErrorBorder = scaleScheme.errorScale.border; + final errorBorder = scaleScheme.errorScale.primary; + OutlineInputBorder makeBorder(Color color) => OutlineInputBorder( + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale), + borderSide: BorderSide(color: color), + ); + OutlineInputBorder makeFocusedBorder(Color color) => OutlineInputBorder( + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale), + borderSide: BorderSide(width: 2, color: color), + ); + return InputDecorator( + decoration: InputDecoration( + labelText: label, + floatingLabelStyle: TextStyle(color: hoverBorder), + border: makeBorder(border), + enabledBorder: makeBorder(border), + disabledBorder: makeBorder(disabledBorder), + focusedBorder: makeFocusedBorder(hoverBorder), + errorBorder: makeBorder(errorBorder), + focusedErrorBorder: makeFocusedBorder(focusedErrorBorder), + ), + child: this); + } +} + Widget buildProgressIndicator() => Builder(builder: (context) { final theme = Theme.of(context); final scale = theme.extension()!; @@ -292,6 +330,23 @@ Widget styledExpandingSliver( )); } +Widget styledHeader({required BuildContext context, required Widget child}) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + // final textTheme = theme.textTheme; + + return DecoratedBox( + decoration: ShapeDecoration( + color: scale.primaryScale.border, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12 * scaleConfig.borderRadiusScale), + topRight: + Radius.circular(12 * scaleConfig.borderRadiusScale)))), + child: child); +} + Widget styledTitleContainer({ required BuildContext context, required String title, diff --git a/pubspec.lock b/pubspec.lock index 6d78407..a42b9b0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -675,10 +675,10 @@ packages: dependency: "direct main" description: name: form_builder_validators - sha256: "475853a177bfc832ec12551f752fd0001278358a6d42d2364681ff15f48f67cf" + sha256: c61ed7b1deecf0e1ebe49e2fa79e3283937c5a21c7e48e3ed9856a4a14e1191a url: "https://pub.dev" source: hosted - version: "10.0.1" + version: "11.0.0" freezed: dependency: "direct dev" description: @@ -1258,11 +1258,10 @@ packages: searchable_listview: dependency: "direct main" description: - name: searchable_listview - sha256: "5e547f2a3f88f99a798cfe64882cfce51496d8f3177bea4829dd7bcf3fa8910d" - url: "https://pub.dev" - source: hosted - version: "2.14.0" + path: "../Searchable-Listview" + relative: true + source: path + version: "2.14.1" share_plus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 2c0b8eb..2226bc9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,7 +52,7 @@ dependencies: flutter_svg: ^2.0.10+1 flutter_translate: ^4.1.0 flutter_zoom_drawer: ^3.2.0 - form_builder_validators: ^10.0.1 + form_builder_validators: ^11.0.0 freezed_annotation: ^2.4.1 go_router: ^14.1.4 hydrated_bloc: ^9.1.5 @@ -81,7 +81,10 @@ dependencies: reorderable_grid: ^1.0.10 screenshot: ^3.0.0 scroll_to_index: ^3.0.1 - searchable_listview: ^2.14.0 + searchable_listview: + git: + url: https://gitlab.com/veilid/Searchable-Listview.git + ref: main share_plus: ^9.0.0 shared_preferences: ^2.2.3 signal_strength_indicator: ^0.4.1 @@ -112,6 +115,8 @@ dependency_overrides: path: ../dart_async_tools bloc_advanced_tools: path: ../bloc_advanced_tools + searchable_listview: + path: ../Searchable-Listview # flutter_chat_ui: # path: ../flutter_chat_ui From b6a812af87467cd9b79eca66e7ba5e53ab992747 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 31 Jul 2024 12:14:06 -0500 Subject: [PATCH 169/270] oops --- lib/account_manager/models/models.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/account_manager/models/models.dart b/lib/account_manager/models/models.dart index 8b785c6..1a0c809 100644 --- a/lib/account_manager/models/models.dart +++ b/lib/account_manager/models/models.dart @@ -1,5 +1,5 @@ export 'account_info.dart'; -export 'account_update_spec.dart'; +export 'account_spec.dart'; export 'encryption_key_type.dart'; export 'local_account/local_account.dart'; export 'per_account_collection_state/per_account_collection_state.dart'; From 030f9d9651768a0f50664567d5984ad3a88d8fab Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 1 Aug 2024 14:30:06 -0500 Subject: [PATCH 170/270] profile edit happens without requiring save button --- assets/i18n/en.json | 10 +- assets/images/handshake.png | Bin 0 -> 43924 bytes assets/images/toilet.png | Bin 0 -> 28325 bytes .../cubits/account_record_cubit.dart | 26 ++- .../views/edit_account_page.dart | 69 ++---- .../views/edit_profile_form.dart | 208 +++++++++++------- .../views/new_account_page.dart | 72 ++++-- .../chat_single_contact_item_widget.dart | 43 ++-- .../views/contact_invitation_item_widget.dart | 2 +- .../views/create_invitation_dialog.dart | 45 ++-- lib/contacts/views/availability_widget.dart | 52 +++-- lib/contacts/views/contact_item_widget.dart | 40 ++-- lib/contacts/views/contacts_browser.dart | 127 +++++++++-- lib/contacts/views/contacts_dialog.dart | 12 +- lib/proto/extensions.dart | 1 + lib/theme/models/slider_tile.dart | 14 +- lib/theme/views/widget_helpers.dart | 33 +++ pubspec.lock | 8 + pubspec.yaml | 3 + 19 files changed, 499 insertions(+), 266 deletions(-) create mode 100644 assets/images/handshake.png create mode 100644 assets/images/toilet.png diff --git a/assets/i18n/en.json b/assets/i18n/en.json index ade2dcb..b2e6907 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -25,7 +25,7 @@ "empty_free_message": "Status when availability is 'Free'", "form_away_message": "Away Message", "empty_away_message": "Status when availability is 'Away'", - "form_busy_message": "Free Message", + "form_busy_message": "Busy Message", "empty_busy_message": "Status when availability is 'Busy'", "form_availability": "Availability", "form_avatar": "Avatar", @@ -42,6 +42,7 @@ "create": "Create", "instructions": "This information will be shared with the people you invite to connect with you on VeilidChat.", "error": "Account creation error", + "network_is_offline": "Network is offline, try again when you're connected", "name": "Name", "pronouns": "Pronouns" }, @@ -149,9 +150,10 @@ }, "add_contact_sheet": { "new_contact": "New Contact", - "create_invite": "Create Invitation", - "scan_invite": "Scan Invitation", - "paste_invite": "Paste Invitation" + "create_invite": "Create\nInvitation", + "receive_invite": "Receive\nInvitation", + "scan_invite": "Scan\nInvitation", + "paste_invite": "Paste\nInvitation" }, "add_chat_sheet": { "new_chat": "New Chat" diff --git a/assets/images/handshake.png b/assets/images/handshake.png new file mode 100644 index 0000000000000000000000000000000000000000..d40fad1c889f016d4a61f59f17350a99b9ef0e31 GIT binary patch literal 43924 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 literal 0 HcmV?d00001 diff --git a/assets/images/toilet.png b/assets/images/toilet.png new file mode 100644 index 0000000000000000000000000000000000000000..6c0efa2a51a8399cf5af75529780da71f17a58d7 GIT binary patch literal 28325 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 literal 0 HcmV?d00001 diff --git a/lib/account_manager/cubits/account_record_cubit.dart b/lib/account_manager/cubits/account_record_cubit.dart index 0762926..2a3d9e2 100644 --- a/lib/account_manager/cubits/account_record_cubit.dart +++ b/lib/account_manager/cubits/account_record_cubit.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:async_tools/async_tools.dart'; import 'package:protobuf/protobuf.dart'; import 'package:veilid_support/veilid_support.dart'; @@ -7,6 +8,10 @@ import '../../proto/proto.dart' as proto; import '../account_manager.dart'; typedef AccountRecordState = proto.Account; +typedef _sspUpdateState = ( + AccountSpec accountSpec, + Future Function() onSuccess +); /// The saved state of a VeilidChat Account on the DHT /// Used to synchronize status, profile, and options for a specific account @@ -34,16 +39,25 @@ class AccountRecordCubit extends DefaultDHTRecordCubit { @override Future close() async { + await _sspUpdate.close(); await super.close(); } //////////////////////////////////////////////////////////////////////////// // Public Interface - Future updateAccount( - AccountSpec accountSpec, - ) async { + void updateAccount( + AccountSpec accountSpec, Future Function() onSuccess) { + _sspUpdate.updateState((accountSpec, onSuccess), (state) async { + await _updateAccountAsync(state.$1, state.$2); + }); + } + + Future _updateAccountAsync( + AccountSpec accountSpec, Future Function() onSuccess) async { + var changed = false; await record.eventualUpdateProtobuf(proto.Account.fromBuffer, (old) async { + changed = false; if (old == null) { return null; } @@ -63,7 +77,6 @@ class AccountRecordCubit extends DefaultDHTRecordCubit { ..awayMessage = accountSpec.awayMessage ..busyMessage = accountSpec.busyMessage; - var changed = false; if (newAccount.profile != old.profile || newAccount.invisible != old.invisible || newAccount.autodetectAway != old.autodetectAway || @@ -78,5 +91,10 @@ class AccountRecordCubit extends DefaultDHTRecordCubit { } return null; }); + if (changed) { + await onSuccess(); + } } + + final _sspUpdate = SingleStateProcessor<_sspUpdateState>(); } diff --git a/lib/account_manager/views/edit_account_page.dart b/lib/account_manager/views/edit_account_page.dart index 4f19429..e49b48a 100644 --- a/lib/account_manager/views/edit_account_page.dart +++ b/lib/account_manager/views/edit_account_page.dart @@ -50,13 +50,13 @@ class _EditAccountPageState extends WindowSetupState { orientationCapability: OrientationCapability.portraitOnly); Widget _editAccountForm(BuildContext context, - {required Future Function(AccountSpec) onSubmit}) => + {required Future Function(AccountSpec) onUpdate}) => EditProfileForm( header: translate('edit_account_page.header'), instructions: translate('edit_account_page.instructions'), submitText: translate('edit_account_page.update'), submitDisabledText: translate('button.waiting_for_network'), - onSubmit: onSubmit, + onUpdate: onUpdate, initialValueCallback: (key) => switch (key) { EditProfileForm.formFieldName => widget.existingAccount.profile.name, EditProfileForm.formFieldPronouns => @@ -76,7 +76,7 @@ class _EditAccountPageState extends WindowSetupState { EditProfileForm.formFieldAutoAway => widget.existingAccount.autodetectAway, EditProfileForm.formFieldAutoAwayTimeout => - widget.existingAccount.autoAwayTimeoutMin, + widget.existingAccount.autoAwayTimeoutMin.toString(), String() => throw UnimplementedError(), }, ); @@ -214,51 +214,24 @@ class _EditAccountPageState extends WindowSetupState { } } - Future _onSubmit(AccountSpec accountSpec) async { - // dismiss the keyboard by unfocusing the textfield - FocusScope.of(context).unfocus(); - - try { - setState(() { - _isInAsyncCall = true; - }); - try { - // Look up account cubit for this specific account - final perAccountCollectionBlocMapCubit = - context.read(); - final accountRecordCubit = await perAccountCollectionBlocMapCubit - .operate(widget.superIdentityRecordKey, - closure: (c) async => c.accountRecordCubit); - if (accountRecordCubit == null) { - return; - } - - // Update account profile DHT record - // This triggers ConversationCubits to update - await accountRecordCubit.updateAccount(accountSpec); - - // Update local account profile - await AccountRepository.instance - .updateLocalAccount(widget.superIdentityRecordKey, accountSpec); - - if (mounted) { - Navigator.canPop(context) - ? GoRouterHelper(context).pop() - : GoRouterHelper(context).go('/'); - } - } finally { - if (mounted) { - setState(() { - _isInAsyncCall = false; - }); - } - } - } on Exception catch (e) { - if (mounted) { - await showErrorModal( - context, translate('edit_account_page.error'), 'Exception: $e'); - } + Future _onUpdate(AccountSpec accountSpec) async { + // Look up account cubit for this specific account + final perAccountCollectionBlocMapCubit = + context.read(); + final accountRecordCubit = await perAccountCollectionBlocMapCubit.operate( + widget.superIdentityRecordKey, + closure: (c) async => c.accountRecordCubit); + if (accountRecordCubit == null) { + return; } + + // Update account profile DHT record + // This triggers ConversationCubits to update + accountRecordCubit.updateAccount(accountSpec, () async { + // Update local account profile + await AccountRepository.instance + .updateLocalAccount(widget.superIdentityRecordKey, accountSpec); + }); } @override @@ -290,7 +263,7 @@ class _EditAccountPageState extends WindowSetupState { child: Column(children: [ _editAccountForm( context, - onSubmit: _onSubmit, + onUpdate: _onUpdate, ).paddingLTRB(0, 0, 0, 32), OptionBox( instructions: diff --git a/lib/account_manager/views/edit_profile_form.dart b/lib/account_manager/views/edit_profile_form.dart index c9a328e..05e6ffe 100644 --- a/lib/account_manager/views/edit_profile_form.dart +++ b/lib/account_manager/views/edit_profile_form.dart @@ -1,3 +1,4 @@ +import 'package:async_tools/async_tools.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -10,15 +11,18 @@ import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; import '../models/models.dart'; +const _kDoUpdateSubmit = 'doUpdateSubmit'; + class EditProfileForm extends StatefulWidget { const EditProfileForm({ required this.header, required this.instructions, required this.submitText, required this.submitDisabledText, - super.key, + required this.initialValueCallback, + this.onUpdate, this.onSubmit, - this.initialValueCallback, + super.key, }); @override @@ -26,10 +30,11 @@ class EditProfileForm extends StatefulWidget { final String header; final String instructions; + final Future Function(AccountSpec)? onUpdate; final Future Function(AccountSpec)? onSubmit; final String submitText; final String submitDisabledText; - final Object? Function(String key)? initialValueCallback; + final Object Function(String key) initialValueCallback; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -38,11 +43,13 @@ class EditProfileForm extends StatefulWidget { ..add(StringProperty('header', header)) ..add(StringProperty('instructions', instructions)) ..add(ObjectFlagProperty Function(AccountSpec)?>.has( - 'onSubmit', onSubmit)) + 'onUpdate', onUpdate)) ..add(StringProperty('submitText', submitText)) ..add(StringProperty('submitDisabledText', submitDisabledText)) - ..add(ObjectFlagProperty.has( - 'initialValueCallback', initialValueCallback)); + ..add(ObjectFlagProperty.has( + 'initialValueCallback', initialValueCallback)) + ..add(ObjectFlagProperty Function(AccountSpec)?>.has( + 'onSubmit', onSubmit)); } static const String formFieldName = 'name'; @@ -62,15 +69,17 @@ class _EditProfileFormState extends State { @override void initState() { + _autoAwayEnabled = + widget.initialValueCallback(EditProfileForm.formFieldAutoAway) as bool; + super.initState(); } FormBuilderDropdown _availabilityDropDown( BuildContext context) { final initialValueX = - widget.initialValueCallback?.call(EditProfileForm.formFieldAvailability) - as proto.Availability? ?? - proto.Availability.AVAILABILITY_FREE; + widget.initialValueCallback(EditProfileForm.formFieldAvailability) + as proto.Availability; final initialValue = initialValueX == proto.Availability.AVAILABILITY_UNSPECIFIED ? proto.Availability.AVAILABILITY_FREE @@ -86,14 +95,19 @@ class _EditProfileFormState extends State { return FormBuilderDropdown( name: EditProfileForm.formFieldAvailability, initialValue: initialValue, + decoration: InputDecoration( + floatingLabelBehavior: FloatingLabelBehavior.always, + labelText: translate('account.form_availability'), + hintText: translate('account.empty_busy_message')), items: availabilities .map((x) => DropdownMenuItem( value: x, child: Row(mainAxisSize: MainAxisSize.min, children: [ - Icon(AvailabilityWidget.availabilityIcon(x)), + AvailabilityWidget.availabilityIcon(x), Text(x == proto.Availability.AVAILABILITY_OFFLINE - ? translate('availability.always_show_offline') - : AvailabilityWidget.availabilityName(x)), + ? translate('availability.always_show_offline') + : AvailabilityWidget.availabilityName(x)) + .paddingLTRB(8, 0, 0, 0), ]))) .toList(), ); @@ -103,34 +117,26 @@ class _EditProfileFormState extends State { final name = _formKey .currentState!.fields[EditProfileForm.formFieldName]!.value as String; final pronouns = _formKey.currentState! - .fields[EditProfileForm.formFieldPronouns]!.value as String? ?? - ''; - final about = _formKey.currentState!.fields[EditProfileForm.formFieldAbout]! - .value as String? ?? - ''; + .fields[EditProfileForm.formFieldPronouns]!.value as String; + final about = _formKey + .currentState!.fields[EditProfileForm.formFieldAbout]!.value as String; final availability = _formKey - .currentState! - .fields[EditProfileForm.formFieldAvailability]! - .value as proto.Availability? ?? - proto.Availability.AVAILABILITY_FREE; + .currentState! + .fields[EditProfileForm.formFieldAvailability]! + .value as proto.Availability; final invisible = availability == proto.Availability.AVAILABILITY_OFFLINE; - final freeMessage = _formKey.currentState! - .fields[EditProfileForm.formFieldFreeMessage]!.value as String? ?? - ''; + .fields[EditProfileForm.formFieldFreeMessage]!.value as String; final awayMessage = _formKey.currentState! - .fields[EditProfileForm.formFieldAwayMessage]!.value as String? ?? - ''; + .fields[EditProfileForm.formFieldAwayMessage]!.value as String; final busyMessage = _formKey.currentState! - .fields[EditProfileForm.formFieldBusyMessage]!.value as String? ?? - ''; - final autoAway = _formKey.currentState! - .fields[EditProfileForm.formFieldAutoAway]!.value as bool? ?? - false; - final autoAwayTimeout = _formKey.currentState! - .fields[EditProfileForm.formFieldAutoAwayTimeout]!.value as int? ?? - 30; + .fields[EditProfileForm.formFieldBusyMessage]!.value as String; + final autoAway = _formKey + .currentState!.fields[EditProfileForm.formFieldAutoAway]!.value as bool; + final autoAwayTimeoutString = _formKey.currentState! + .fields[EditProfileForm.formFieldAutoAwayTimeout]!.value as String; + final autoAwayTimeout = int.parse(autoAwayTimeoutString); return AccountSpec( name: name, @@ -163,6 +169,7 @@ class _EditProfileFormState extends State { return FormBuilder( key: _formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, child: Column( children: [ AvatarWidget( @@ -179,9 +186,10 @@ class _EditProfileFormState extends State { FormBuilderTextField( autofocus: true, name: EditProfileForm.formFieldName, - initialValue: widget.initialValueCallback - ?.call(EditProfileForm.formFieldName) as String?, + initialValue: widget + .initialValueCallback(EditProfileForm.formFieldName) as String, decoration: InputDecoration( + floatingLabelBehavior: FloatingLabelBehavior.always, labelText: translate('account.form_name'), hintText: translate('account.empty_name')), maxLength: 64, @@ -190,113 +198,149 @@ class _EditProfileFormState extends State { FormBuilderValidators.required(), ]), textInputAction: TextInputAction.next, - ), + ).onFocusChange(_onFocusChange), FormBuilderTextField( name: EditProfileForm.formFieldPronouns, - initialValue: widget.initialValueCallback - ?.call(EditProfileForm.formFieldPronouns) as String?, + initialValue: + widget.initialValueCallback(EditProfileForm.formFieldPronouns) + as String, maxLength: 64, decoration: InputDecoration( + floatingLabelBehavior: FloatingLabelBehavior.always, labelText: translate('account.form_pronouns'), hintText: translate('account.empty_pronouns')), textInputAction: TextInputAction.next, - ), + ).onFocusChange(_onFocusChange), FormBuilderTextField( name: EditProfileForm.formFieldAbout, - initialValue: widget.initialValueCallback - ?.call(EditProfileForm.formFieldAbout) as String?, + initialValue: widget + .initialValueCallback(EditProfileForm.formFieldAbout) as String, maxLength: 1024, maxLines: 8, minLines: 1, decoration: InputDecoration( + floatingLabelBehavior: FloatingLabelBehavior.always, labelText: translate('account.form_about'), hintText: translate('account.empty_about')), textInputAction: TextInputAction.newline, - ), - _availabilityDropDown(context), + ).onFocusChange(_onFocusChange), + _availabilityDropDown(context) + .paddingLTRB(0, 0, 0, 16) + .onFocusChange(_onFocusChange), FormBuilderTextField( name: EditProfileForm.formFieldFreeMessage, - initialValue: widget.initialValueCallback - ?.call(EditProfileForm.formFieldFreeMessage) as String?, + initialValue: widget.initialValueCallback( + EditProfileForm.formFieldFreeMessage) as String, maxLength: 128, decoration: InputDecoration( + floatingLabelBehavior: FloatingLabelBehavior.always, labelText: translate('account.form_free_message'), hintText: translate('account.empty_free_message')), textInputAction: TextInputAction.next, - ), + ).onFocusChange(_onFocusChange), FormBuilderTextField( name: EditProfileForm.formFieldAwayMessage, - initialValue: widget.initialValueCallback - ?.call(EditProfileForm.formFieldAwayMessage) as String?, + initialValue: widget.initialValueCallback( + EditProfileForm.formFieldAwayMessage) as String, maxLength: 128, decoration: InputDecoration( + floatingLabelBehavior: FloatingLabelBehavior.always, labelText: translate('account.form_away_message'), hintText: translate('account.empty_away_message')), textInputAction: TextInputAction.next, - ), + ).onFocusChange(_onFocusChange), FormBuilderTextField( name: EditProfileForm.formFieldBusyMessage, - initialValue: widget.initialValueCallback - ?.call(EditProfileForm.formFieldBusyMessage) as String?, + initialValue: widget.initialValueCallback( + EditProfileForm.formFieldBusyMessage) as String, maxLength: 128, decoration: InputDecoration( + floatingLabelBehavior: FloatingLabelBehavior.always, labelText: translate('account.form_busy_message'), hintText: translate('account.empty_busy_message')), textInputAction: TextInputAction.next, - ), + ).onFocusChange(_onFocusChange), FormBuilderCheckbox( name: EditProfileForm.formFieldAutoAway, - initialValue: widget.initialValueCallback - ?.call(EditProfileForm.formFieldAutoAway) as bool? ?? - false, + initialValue: + widget.initialValueCallback(EditProfileForm.formFieldAutoAway) + as bool, side: BorderSide(color: scale.primaryScale.border, width: 2), title: Text(translate('account.form_auto_away'), style: textTheme.labelMedium), - ), + onChanged: (v) { + setState(() { + _autoAwayEnabled = v ?? false; + }); + }, + ).onFocusChange(_onFocusChange), FormBuilderTextField( name: EditProfileForm.formFieldAutoAwayTimeout, - enabled: _formKey.currentState - ?.value[EditProfileForm.formFieldAutoAway] as bool? ?? - false, - initialValue: widget.initialValueCallback - ?.call(EditProfileForm.formFieldAutoAwayTimeout) - as String? ?? - '15', + enabled: _autoAwayEnabled, + initialValue: widget.initialValueCallback( + EditProfileForm.formFieldAutoAwayTimeout) as String, decoration: InputDecoration( labelText: translate('account.form_auto_away_timeout'), ), validator: FormBuilderValidators.positiveNumber(), textInputAction: TextInputAction.next, - ), + ).onFocusChange(_onFocusChange), Row(children: [ const Spacer(), Text(widget.instructions).toCenter().flexible(flex: 6), const Spacer(), ]).paddingSymmetric(vertical: 4), - ElevatedButton( - onPressed: widget.onSubmit == null - ? null - : () async { - if (_formKey.currentState?.saveAndValidate() ?? false) { - final aus = _makeAccountSpec(); - await widget.onSubmit!(aus); - } - }, - child: Row(mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0), - Text((widget.onSubmit == null) - ? widget.submitDisabledText - : widget.submitText) - .paddingLTRB(0, 0, 4, 0) - ]), - ).paddingSymmetric(vertical: 4).alignAtCenterRight(), + if (widget.onSubmit != null) + ElevatedButton( + onPressed: widget.onSubmit == null ? null : _doSubmit, + child: Row(mainAxisSize: MainAxisSize.min, children: [ + const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0), + Text((widget.onSubmit == null) + ? widget.submitDisabledText + : widget.submitText) + .paddingLTRB(0, 0, 4, 0) + ]), + ) ], ), ); } + void _onFocusChange(bool focused) { + if (!focused) { + _doUpdate(); + } + } + + void _doUpdate() { + final onUpdate = widget.onUpdate; + if (onUpdate != null) { + singleFuture((this, _kDoUpdateSubmit), () async { + if (_formKey.currentState?.saveAndValidate() ?? false) { + final aus = _makeAccountSpec(); + await onUpdate(aus); + } + }); + } + } + + void _doSubmit() { + final onSubmit = widget.onSubmit; + if (onSubmit != null) { + singleFuture((this, _kDoUpdateSubmit), () async { + if (_formKey.currentState?.saveAndValidate() ?? false) { + final aus = _makeAccountSpec(); + await onSubmit(aus); + } + }); + } + } + @override Widget build(BuildContext context) => _editProfileForm( context, ); + + /////////////////////////////////////////////////////////////////////////// + late bool _autoAwayEnabled; } diff --git a/lib/account_manager/views/new_account_page.dart b/lib/account_manager/views/new_account_page.dart index b107a7b..ee2f62c 100644 --- a/lib/account_manager/views/new_account_page.dart +++ b/lib/account_manager/views/new_account_page.dart @@ -7,6 +7,8 @@ import 'package:flutter_translate/flutter_translate.dart'; import 'package:go_router/go_router.dart'; import '../../layout/default_app_bar.dart'; +import '../../notifications/cubits/notifications_cubit.dart'; +import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; import '../../tools/tools.dart'; import '../../veilid_processor/veilid_processor.dart'; @@ -26,25 +28,44 @@ class _NewAccountPageState extends WindowSetupState { titleBarStyle: TitleBarStyle.normal, orientationCapability: OrientationCapability.portraitOnly); - Widget _newAccountForm(BuildContext context, - {required Future Function(AccountSpec) onSubmit}) { - final networkReady = context - .watch() - .state - .asData - ?.value - .isPublicInternetReady ?? - false; - final canSubmit = networkReady; - - return EditProfileForm( - header: translate('new_account_page.header'), - instructions: translate('new_account_page.instructions'), - submitText: translate('new_account_page.create'), - submitDisabledText: translate('button.waiting_for_network'), - onSubmit: !canSubmit ? null : onSubmit); + Object _defaultAccountValues(String key) { + switch (key) { + case EditProfileForm.formFieldName: + return ''; + case EditProfileForm.formFieldPronouns: + return ''; + case EditProfileForm.formFieldAbout: + return ''; + case EditProfileForm.formFieldAvailability: + return proto.Availability.AVAILABILITY_FREE; + case EditProfileForm.formFieldFreeMessage: + return ''; + case EditProfileForm.formFieldAwayMessage: + return ''; + case EditProfileForm.formFieldBusyMessage: + return ''; + // case EditProfileForm.formFieldAvatar: + // return null; + case EditProfileForm.formFieldAutoAway: + return false; + case EditProfileForm.formFieldAutoAwayTimeout: + return '15'; + default: + throw StateError('missing form element'); + } } + Widget _newAccountForm( + BuildContext context, + ) => + EditProfileForm( + header: translate('new_account_page.header'), + instructions: translate('new_account_page.instructions'), + submitText: translate('new_account_page.create'), + submitDisabledText: translate('button.waiting_for_network'), + initialValueCallback: _defaultAccountValues, + onSubmit: _onSubmit); + Future _onSubmit(AccountSpec accountSpec) async { // dismiss the keyboard by unfocusing the textfield FocusScope.of(context).unfocus(); @@ -54,6 +75,22 @@ class _NewAccountPageState extends WindowSetupState { _isInAsyncCall = true; }); try { + final networkReady = context + .read() + .state + .asData + ?.value + .isPublicInternetReady ?? + false; + + final canSubmit = networkReady; + if (!canSubmit) { + context.read().error( + text: translate('new_account_page.network_is_offline'), + title: translate('new_account_page.error')); + return; + } + final writableSuperIdentity = await AccountRepository.instance .createWithNewSuperIdentity(accountSpec); GoRouterHelper(context).pushReplacement('/new_account/recovery_key', @@ -100,7 +137,6 @@ class _NewAccountPageState extends WindowSetupState { body: SingleChildScrollView( child: _newAccountForm( context, - onSubmit: _onSubmit, )).paddingSymmetric(horizontal: 24, vertical: 8), ).withModalHUD(context, displayModalHUD); } 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 7fd38cd..b186290 100644 --- a/lib/chat_list/views/chat_single_contact_item_widget.dart +++ b/lib/chat_list/views/chat_single_contact_item_widget.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_translate/flutter_translate.dart'; import '../../chat/cubits/active_chat_cubit.dart'; +import '../../contacts/contacts.dart'; import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; import '../chat_list.dart'; @@ -23,28 +24,33 @@ class ChatSingleContactItemWidget extends StatelessWidget { Widget build( BuildContext context, ) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + final activeChatCubit = context.watch(); final localConversationRecordKey = _contact.localConversationRecordKey.toVeilid(); final selected = activeChatCubit.state == localConversationRecordKey; - late final String title; - late final String subtitle; - if (_contact.nickname.isNotEmpty) { - title = _contact.nickname; - if (_contact.profile.pronouns.isNotEmpty) { - subtitle = '${_contact.profile.name} (${_contact.profile.pronouns})'; - } else { - subtitle = _contact.profile.name; - } - } else { - title = _contact.profile.name; - if (_contact.profile.pronouns.isNotEmpty) { - subtitle = '(${_contact.profile.pronouns})'; - } else { - subtitle = ''; - } - } + final name = _contact.nameOrNickname; + final title = _contact.displayName; + final subtitle = _contact.profile.status; + + final avatar = AvatarWidget( + name: name, + size: 34, + borderColor: _disabled + ? scale.grayScale.primaryText + : scale.secondaryScale.primaryText, + foregroundColor: _disabled + ? scale.grayScale.primaryText + : scale.secondaryScale.primaryText, + backgroundColor: + _disabled ? scale.grayScale.primary : scale.secondaryScale.primary, + scaleConfig: scaleConfig, + textStyle: theme.textTheme.titleLarge!, + ); return SliderTile( key: ObjectKey(_contact), @@ -53,7 +59,8 @@ class ChatSingleContactItemWidget extends StatelessWidget { tileScale: ScaleKind.secondary, title: title, subtitle: subtitle, - icon: Icons.chat, + leading: avatar, + trailing: AvailabilityWidget(availability: _contact.profile.availability), onTap: () { singleFuture(activeChatCubit, () async { activeChatCubit.setActiveChat(localConversationRecordKey); diff --git a/lib/contact_invitation/views/contact_invitation_item_widget.dart b/lib/contact_invitation/views/contact_invitation_item_widget.dart index 8fccc8a..6e6dfcf 100644 --- a/lib/contact_invitation/views/contact_invitation_item_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_item_widget.dart @@ -45,7 +45,7 @@ class ContactInvitationItemWidget extends StatelessWidget { title: contactInvitationRecord.message.isEmpty ? translate('contact_list.invitation') : contactInvitationRecord.message, - icon: Icons.person_add, + leading: const Icon(Icons.person_add), onTap: () async { if (!context.mounted) { return; diff --git a/lib/contact_invitation/views/create_invitation_dialog.dart b/lib/contact_invitation/views/create_invitation_dialog.dart index 5711a32..f1115d7 100644 --- a/lib/contact_invitation/views/create_invitation_dialog.dart +++ b/lib/contact_invitation/views/create_invitation_dialog.dart @@ -203,34 +203,37 @@ class CreateInvitationDialogState extends State { Text(translate('create_invitation_dialog.protect_this_invitation'), style: textTheme.labelLarge) .paddingAll(8), - Wrap(spacing: 5, children: [ - ChoiceChip( - label: Text(translate('create_invitation_dialog.unlocked')), - selected: _encryptionKeyType == EncryptionKeyType.none, - onSelected: _onNoneEncryptionSelected, - ), - ChoiceChip( - label: Text(translate('create_invitation_dialog.pin')), - selected: _encryptionKeyType == EncryptionKeyType.pin, - onSelected: _onPinEncryptionSelected, - ), - ChoiceChip( - label: Text(translate('create_invitation_dialog.password')), - selected: _encryptionKeyType == EncryptionKeyType.password, - onSelected: _onPasswordEncryptionSelected, - ) - ]).paddingAll(8), + Wrap( + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + runSpacing: 8, + spacing: 8, + children: [ + ChoiceChip( + label: Text(translate('create_invitation_dialog.unlocked')), + selected: _encryptionKeyType == EncryptionKeyType.none, + onSelected: _onNoneEncryptionSelected, + ), + ChoiceChip( + label: Text(translate('create_invitation_dialog.pin')), + selected: _encryptionKeyType == EncryptionKeyType.pin, + onSelected: _onPinEncryptionSelected, + ), + ChoiceChip( + label: Text(translate('create_invitation_dialog.password')), + selected: _encryptionKeyType == EncryptionKeyType.password, + onSelected: _onPasswordEncryptionSelected, + ) + ]).paddingAll(8).toCenter(), Container( - width: double.infinity, - height: 60, padding: const EdgeInsets.all(8), child: ElevatedButton( onPressed: _onGenerateButtonPressed, child: Text( translate('create_invitation_dialog.generate'), - ), + ).paddingAll(16), ), - ), + ).toCenter(), Text(translate('create_invitation_dialog.note')).paddingAll(8), Text( translate('create_invitation_dialog.note_text'), diff --git a/lib/contacts/views/availability_widget.dart b/lib/contacts/views/availability_widget.dart index 8dc66d8..d50e323 100644 --- a/lib/contacts/views/availability_widget.dart +++ b/lib/contacts/views/availability_widget.dart @@ -1,3 +1,4 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_translate/flutter_translate.dart'; @@ -5,21 +6,27 @@ import 'package:flutter_translate/flutter_translate.dart'; import '../../proto/proto.dart' as proto; class AvailabilityWidget extends StatelessWidget { - const AvailabilityWidget({required this.availability, super.key}); + const AvailabilityWidget( + {required this.availability, + this.vertical = true, + this.iconSize = 32, + super.key}); - static IconData availabilityIcon(proto.Availability availability) { - late final IconData iconData; + static Widget availabilityIcon(proto.Availability availability, + {double size = 32}) { + late final Widget iconData; switch (availability) { case proto.Availability.AVAILABILITY_AWAY: - iconData = Icons.hot_tub; + iconData = + ImageIcon(const AssetImage('assets/images/toilet.png'), size: size); case proto.Availability.AVAILABILITY_BUSY: - iconData = Icons.event_busy; + iconData = Icon(Icons.event_busy, size: size); case proto.Availability.AVAILABILITY_FREE: - iconData = Icons.event_available; + iconData = Icon(Icons.event_available, size: size); case proto.Availability.AVAILABILITY_OFFLINE: - iconData = Icons.cloud_off; + iconData = Icon(Icons.cloud_off, size: size); case proto.Availability.AVAILABILITY_UNSPECIFIED: - iconData = Icons.question_mark; + iconData = Icon(Icons.question_mark, size: size); } return iconData; } @@ -49,20 +56,35 @@ class AvailabilityWidget extends StatelessWidget { // final scaleConfig = theme.extension()!; final name = availabilityName(availability); - final iconData = availabilityIcon(availability); + final icon = availabilityIcon(availability, size: iconSize); - return Row(mainAxisSize: MainAxisSize.min, children: [ - Icon(iconData, size: 32), - Text(name, style: textTheme.labelSmall) - ]); + return vertical + ? Column( + mainAxisSize: MainAxisSize.min, + //mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + Text(name, style: textTheme.labelSmall).paddingLTRB(0, 0, 0, 0) + ]) + : Row(mainAxisSize: MainAxisSize.min, children: [ + icon, + Text(name, style: textTheme.labelSmall).paddingLTRB(8, 0, 0, 0) + ]); } + //////////////////////////////////////////////////////////////////////////// + final proto.Availability availability; + final bool vertical; + final double iconSize; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add( - DiagnosticsProperty('availability', availability)); + properties + ..add( + DiagnosticsProperty('availability', availability)) + ..add(DiagnosticsProperty('vertical', vertical)) + ..add(DoubleProperty('iconSize', iconSize)); } } diff --git a/lib/contacts/views/contact_item_widget.dart b/lib/contacts/views/contact_item_widget.dart index 7bf2fa4..46b658a 100644 --- a/lib/contacts/views/contact_item_widget.dart +++ b/lib/contacts/views/contact_item_widget.dart @@ -28,24 +28,28 @@ class ContactItemWidget extends StatelessWidget { Widget build( BuildContext context, ) { - late final String title; - late final String subtitle; + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; - if (_contact.nickname.isNotEmpty) { - title = _contact.nickname; - if (_contact.profile.pronouns.isNotEmpty) { - subtitle = '${_contact.profile.name} (${_contact.profile.pronouns})'; - } else { - subtitle = _contact.profile.name; - } - } else { - title = _contact.profile.name; - if (_contact.profile.pronouns.isNotEmpty) { - subtitle = '(${_contact.profile.pronouns})'; - } else { - subtitle = ''; - } - } + final name = _contact.nameOrNickname; + final title = _contact.displayName; + final subtitle = _contact.profile.status; + + final avatar = AvatarWidget( + name: name, + size: 34, + borderColor: _disabled + ? scale.grayScale.primaryText + : scale.primaryScale.primaryText, + foregroundColor: _disabled + ? scale.grayScale.primaryText + : scale.primaryScale.primaryText, + backgroundColor: + _disabled ? scale.grayScale.primary : scale.primaryScale.primary, + scaleConfig: scaleConfig, + textStyle: theme.textTheme.titleLarge!, + ); return SliderTile( key: ObjectKey(_contact), @@ -54,7 +58,7 @@ class ContactItemWidget extends StatelessWidget { tileScale: ScaleKind.primary, title: title, subtitle: subtitle, - icon: Icons.person, + leading: avatar, onDoubleTap: _onDoubleTap == null ? null : () => singleFuture((this, _kOnTap), () async { diff --git a/lib/contacts/views/contacts_browser.dart b/lib/contacts/views/contacts_browser.dart index f3b03c9..db2a602 100644 --- a/lib/contacts/views/contacts_browser.dart +++ b/lib/contacts/views/contacts_browser.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:searchable_listview/searchable_listview.dart'; +import 'package:star_menu/star_menu.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../chat_list/chat_list.dart'; @@ -71,6 +72,75 @@ class _ContactsBrowserState extends State final scale = theme.extension()!; final scaleConfig = theme.extension()!; + final menuIconColor = scaleConfig.preferBorders + ? scale.primaryScale.hoverBorder + : scale.primaryScale.borderText; + final menuBackgroundColor = scaleConfig.preferBorders + ? scale.primaryScale.elementBackground + : scale.primaryScale.border; + // final menuHoverColor = scaleConfig.preferBorders + // ? scale.primaryScale.hoverElementBackground + // : scale.primaryScale.hoverBorder; + + final menuBorderColor = scale.primaryScale.hoverBorder; + + final menuParams = StarMenuParameters( + shape: MenuShape.grid, + checkItemsScreenBoundaries: true, + centerOffset: const Offset(0, 64), + backgroundParams: + BackgroundParams(backgroundColor: theme.shadowColor.withAlpha(128)), + boundaryBackground: BoundaryBackground( + color: menuBackgroundColor, + decoration: ShapeDecoration( + color: menuBackgroundColor, + shape: RoundedRectangleBorder( + side: scaleConfig.useVisualIndicators + ? BorderSide( + width: 2, color: menuBorderColor, strokeAlign: 0) + : BorderSide.none, + borderRadius: BorderRadius.circular( + 8 * scaleConfig.borderRadiusScale))))); + + final receiveInviteMenuItems = [ + Column(mainAxisSize: MainAxisSize.min, children: [ + IconButton( + onPressed: () async { + _receiveInviteMenuController.closeMenu!(); + await ScanInvitationDialog.show(context); + }, + iconSize: 32, + icon: Icon( + Icons.qr_code_scanner, + size: 32, + color: menuIconColor, + ), + ), + Text(translate('add_contact_sheet.scan_invite'), + maxLines: 2, + textAlign: TextAlign.center, + style: textTheme.labelSmall!.copyWith(color: menuIconColor)) + ]).paddingAll(4), + Column(mainAxisSize: MainAxisSize.min, children: [ + IconButton( + onPressed: () async { + _receiveInviteMenuController.closeMenu!(); + await PasteInvitationDialog.show(context); + }, + iconSize: 32, + icon: Icon( + Icons.paste, + size: 32, + color: menuIconColor, + ), + ), + Text(translate('add_contact_sheet.paste_invite'), + maxLines: 2, + textAlign: TextAlign.center, + style: textTheme.labelSmall!.copyWith(color: menuIconColor)) + ]).paddingAll(4) + ]; + return Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Column(mainAxisSize: MainAxisSize.min, children: [ IconButton( @@ -80,30 +150,36 @@ class _ContactsBrowserState extends State iconSize: 32, icon: const Icon(Icons.contact_page), color: scale.primaryScale.hoverBorder, - tooltip: translate('add_contact_sheet.create_invite'), - ) - ]), - Column(mainAxisSize: MainAxisSize.min, children: [ - IconButton( - onPressed: () async { - await ScanInvitationDialog.show(context); - }, - iconSize: 32, - icon: const Icon(Icons.qr_code_scanner), - color: scale.primaryScale.hoverBorder, - tooltip: translate('add_contact_sheet.scan_invite')), - ]), - Column(mainAxisSize: MainAxisSize.min, children: [ - IconButton( - onPressed: () async { - await PasteInvitationDialog.show(context); - }, - iconSize: 32, - icon: const Icon(Icons.paste), - color: scale.primaryScale.hoverBorder, - tooltip: translate('add_contact_sheet.paste_invite'), ), - ]) + Text(translate('add_contact_sheet.create_invite'), + maxLines: 2, + textAlign: TextAlign.center, + style: textTheme.labelSmall! + .copyWith(color: scale.primaryScale.hoverBorder)) + ]), + StarMenu( + items: receiveInviteMenuItems, + onItemTapped: (_index, controller) { + controller.closeMenu!(); + }, + controller: _receiveInviteMenuController, + params: menuParams, + child: Column(mainAxisSize: MainAxisSize.min, children: [ + IconButton( + onPressed: () {}, + iconSize: 32, + icon: ImageIcon( + const AssetImage('assets/images/handshake.png'), + size: 32, + color: scale.primaryScale.hoverBorder, + )), + Text(translate('add_contact_sheet.receive_invite'), + maxLines: 2, + textAlign: TextAlign.center, + style: textTheme.labelSmall! + .copyWith(color: scale.primaryScale.hoverBorder)) + ]), + ), ]).paddingAll(16); } @@ -112,7 +188,7 @@ class _ContactsBrowserState extends State final theme = Theme.of(context); final textTheme = theme.textTheme; final scale = theme.extension()!; - final scaleConfig = theme.extension()!; + //final scaleConfig = theme.extension()!; final cilState = context.watch().state; final cilBusy = cilState.busy; @@ -244,4 +320,7 @@ class _ContactsBrowserState extends State await chatListCubit.deleteChat( localConversationRecordKey: localConversationRecordKey); } + + //////////////////////////////////////////////////////////////////////////// + final _receiveInviteMenuController = StarMenuController(); } diff --git a/lib/contacts/views/contacts_dialog.dart b/lib/contacts/views/contacts_dialog.dart index d0c1c82..f994148 100644 --- a/lib/contacts/views/contacts_dialog.dart +++ b/lib/contacts/views/contacts_dialog.dart @@ -1,18 +1,14 @@ 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_translate/flutter_translate.dart'; -import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import '../../chat/chat.dart'; import '../../chat_list/chat_list.dart'; -import '../../proto/proto.dart' as proto; -import '../../contact_invitation/contact_invitation.dart'; import '../../layout/layout.dart'; +import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; -import '../../veilid_processor/veilid_processor.dart'; import '../contacts.dart'; class ContactsDialog extends StatefulWidget { @@ -48,9 +44,9 @@ class _ContactsDialogState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final textTheme = theme.textTheme; + // final textTheme = theme.textTheme; final scale = theme.extension()!; - final scaleConfig = theme.extension()!; + // final scaleConfig = theme.extension()!; final enableSplit = !isMobileWidth(context); final enableLeft = enableSplit || _selectedContact == null; @@ -105,7 +101,7 @@ class _ContactsDialogState extends State { .toVeilid(), onContactSelected: onContactSelected, onChatStarted: onChatStarted, - ).paddingAll(8)))), + ).paddingLTRB(8, 0, 8, 8)))), if (enableRight) if (_selectedContact == null) const NoContactWidget().expanded() diff --git a/lib/proto/extensions.dart b/lib/proto/extensions.dart index 4491f89..2f4ad68 100644 --- a/lib/proto/extensions.dart +++ b/lib/proto/extensions.dart @@ -31,6 +31,7 @@ extension MessageExt on proto.Message { } extension ContactExt on proto.Contact { + String get nameOrNickname => nickname.isNotEmpty ? nickname : profile.name; String get displayName => nickname.isNotEmpty ? '$nickname (${profile.name})' : profile.name; } diff --git a/lib/theme/models/slider_tile.dart b/lib/theme/models/slider_tile.dart index 7631303..e70c6bd 100644 --- a/lib/theme/models/slider_tile.dart +++ b/lib/theme/models/slider_tile.dart @@ -31,7 +31,8 @@ class SliderTile extends StatelessWidget { this.startActions = const [], this.onTap, this.onDoubleTap, - this.icon, + this.leading, + this.trailing, super.key}); final bool disabled; @@ -41,7 +42,8 @@ class SliderTile extends StatelessWidget { final List startActions; final GestureTapCallback? onTap; final GestureTapCallback? onDoubleTap; - final IconData? icon; + final Widget? leading; + final Widget? trailing; final String title; final String subtitle; @@ -55,11 +57,12 @@ class SliderTile extends StatelessWidget { ..add(IterableProperty('endActions', endActions)) ..add(IterableProperty('startActions', startActions)) ..add(ObjectFlagProperty.has('onTap', onTap)) - ..add(DiagnosticsProperty('icon', icon)) + ..add(DiagnosticsProperty('leading', leading)) ..add(StringProperty('title', title)) ..add(StringProperty('subtitle', subtitle)) ..add(ObjectFlagProperty.has( - 'onDoubleTap', onDoubleTap)); + 'onDoubleTap', onDoubleTap)) + ..add(DiagnosticsProperty('trailing', trailing)); } @override @@ -156,6 +159,7 @@ class SliderTile extends StatelessWidget { subtitle: subtitle.isNotEmpty ? Text(subtitle) : null, iconColor: textColor, textColor: textColor, - leading: icon == null ? null : Icon(icon)))))); + leading: FittedBox(child: leading), + trailing: FittedBox(child: trailing)))))); } } diff --git a/lib/theme/views/widget_helpers.dart b/lib/theme/views/widget_helpers.dart index 158cc6c..0a5af02 100644 --- a/lib/theme/views/widget_helpers.dart +++ b/lib/theme/views/widget_helpers.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_sticky_header/flutter_sticky_header.dart'; +import 'package:meta/meta.dart'; import 'package:quickalert/quickalert.dart'; import 'package:sliver_expandable/sliver_expandable.dart'; @@ -27,6 +28,38 @@ extension SizeToFixExt on Widget { ); } +extension FocusExt on Widget { + Focus focus( + {Key? key, + FocusNode? focusNode, + FocusNode? parentNode, + bool autofocus = false, + ValueChanged? onFocusChange, + FocusOnKeyEventCallback? onKeyEvent, + bool? canRequestFocus, + bool? skipTraversal, + bool? descendantsAreFocusable, + bool? descendantsAreTraversable, + bool includeSemantics = true, + String? debugLabel}) => + Focus( + key: key, + focusNode: focusNode, + parentNode: parentNode, + autofocus: autofocus, + onFocusChange: onFocusChange, + onKeyEvent: onKeyEvent, + canRequestFocus: canRequestFocus, + skipTraversal: skipTraversal, + descendantsAreFocusable: descendantsAreFocusable, + descendantsAreTraversable: descendantsAreTraversable, + includeSemantics: includeSemantics, + debugLabel: debugLabel, + child: this); + Focus onFocusChange(void Function(bool) onFocusChange) => + Focus(onFocusChange: onFocusChange, child: this); +} + extension ModalProgressExt on Widget { BlurryModalProgressHUD withModalHUD(BuildContext context, bool isLoading) { final theme = Theme.of(context); diff --git a/pubspec.lock b/pubspec.lock index a42b9b0..77484ed 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1468,6 +1468,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.11.1" + star_menu: + dependency: "direct main" + description: + name: star_menu + sha256: f29c7d255677c49ec2412ec2d17220d967f54b72b9e6afc5688fe122ea4d1d78 + url: "https://pub.dev" + source: hosted + version: "4.0.1" stream_channel: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2226bc9..2f38215 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -97,6 +97,7 @@ dependencies: ref: main split_view: ^3.2.1 stack_trace: ^1.11.1 + star_menu: ^4.0.1 stream_transform: ^2.1.0 transitioned_indexed_stack: ^1.0.2 url_launcher: ^6.3.0 @@ -163,6 +164,8 @@ flutter: - assets/images/title.svg - assets/images/vlogo.svg - assets/images/ellet.png + - assets/images/toilet.png + - assets/images/handshake.png # Printing - assets/js/pdf/3.2.146/pdf.min.js # Sounds From aed76c30b096291f4bee8e22faea78bb09e8487e Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 1 Aug 2024 14:52:10 -0500 Subject: [PATCH 171/270] update dependencies --- ios/Podfile.lock | 2 +- lib/theme/views/widget_helpers.dart | 1 - packages/veilid_support/example/pubspec.lock | 8 +- packages/veilid_support/example/pubspec.yaml | 2 +- packages/veilid_support/pubspec.lock | 18 +- packages/veilid_support/pubspec.yaml | 14 +- pubspec.lock | 178 ++++++++++--------- pubspec.yaml | 18 +- 8 files changed, 123 insertions(+), 118 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 3a17844..536f7a4 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -158,7 +158,7 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/veilid/ios" SPEC CHECKSUMS: - camera_avfoundation: 759172d1a77ae7be0de08fc104cfb79738b8a59e + camera_avfoundation: dd002b0330f4981e1bbcb46ae9b62829237459a4 file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 diff --git a/lib/theme/views/widget_helpers.dart b/lib/theme/views/widget_helpers.dart index 0a5af02..b079d2b 100644 --- a/lib/theme/views/widget_helpers.dart +++ b/lib/theme/views/widget_helpers.dart @@ -8,7 +8,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_sticky_header/flutter_sticky_header.dart'; -import 'package:meta/meta.dart'; import 'package:quickalert/quickalert.dart'; import 'package:sliver_expandable/sliver_expandable.dart'; diff --git a/packages/veilid_support/example/pubspec.lock b/packages/veilid_support/example/pubspec.lock index 2c2b1ad..aa857d4 100644 --- a/packages/veilid_support/example/pubspec.lock +++ b/packages/veilid_support/example/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: "direct dev" description: name: async_tools - sha256: "375da1e5b51974a9e84469b7630f36d1361fb53d3fcc818a5a0121ab51fe0343" + sha256: "9166e8fe65fc65eb79202a6d540f4de768553d78141b885f5bd3f8d7d30eef5e" url: "https://pub.dev" source: hosted - version: "0.1.4" + version: "0.1.5" bloc: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: bloc_advanced_tools - sha256: "0599d860eb096c5b12457c60bdf7f66bfcb7171bc94ccf2c7c752b6a716a79f9" + sha256: "2b2dd492a350e7192a933d09f15ea04d5d00e7bd3fe2a906fe629cd461ddbf94" url: "https://pub.dev" source: hosted - version: "0.1.4" + version: "0.1.5" boolean_selector: dependency: transitive description: diff --git a/packages/veilid_support/example/pubspec.yaml b/packages/veilid_support/example/pubspec.yaml index a885f94..eb1c45a 100644 --- a/packages/veilid_support/example/pubspec.yaml +++ b/packages/veilid_support/example/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: path: ../ dev_dependencies: - async_tools: ^0.1.4 + async_tools: ^0.1.5 integration_test: sdk: flutter lint_hard: ^4.0.0 diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index e68b6db..5078198 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -36,10 +36,11 @@ packages: async_tools: dependency: "direct main" description: - path: "../../../dart_async_tools" - relative: true - source: path - version: "0.1.4" + name: async_tools + sha256: "9166e8fe65fc65eb79202a6d540f4de768553d78141b885f5bd3f8d7d30eef5e" + url: "https://pub.dev" + source: hosted + version: "0.1.5" bloc: dependency: "direct main" description: @@ -51,10 +52,11 @@ packages: bloc_advanced_tools: dependency: "direct main" description: - path: "../../../bloc_advanced_tools" - relative: true - source: path - version: "0.1.4" + name: bloc_advanced_tools + sha256: "2b2dd492a350e7192a933d09f15ea04d5d00e7bd3fe2a906fe629cd461ddbf94" + url: "https://pub.dev" + source: hosted + version: "0.1.5" boolean_selector: dependency: transitive description: diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index 51c3e60..cec41f7 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -7,9 +7,9 @@ environment: sdk: '>=3.2.0 <4.0.0' dependencies: - async_tools: ^0.1.4 + async_tools: ^0.1.5 bloc: ^8.1.4 - bloc_advanced_tools: ^0.1.4 + bloc_advanced_tools: ^0.1.5 charcode: ^1.3.1 collection: ^1.18.0 equatable: ^2.0.5 @@ -26,11 +26,11 @@ dependencies: # veilid: ^0.0.1 path: ../../../veilid/veilid-flutter -dependency_overrides: - async_tools: - path: ../../../dart_async_tools - bloc_advanced_tools: - path: ../../../bloc_advanced_tools +# dependency_overrides: +# async_tools: +# path: ../../../dart_async_tools +# bloc_advanced_tools: +# path: ../../../bloc_advanced_tools dev_dependencies: build_runner: ^2.4.10 diff --git a/pubspec.lock b/pubspec.lock index 77484ed..2e8947c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -84,10 +84,11 @@ packages: async_tools: dependency: "direct main" description: - path: "../dart_async_tools" - relative: true - source: path - version: "0.1.4" + name: async_tools + sha256: "9166e8fe65fc65eb79202a6d540f4de768553d78141b885f5bd3f8d7d30eef5e" + url: "https://pub.dev" + source: hosted + version: "0.1.5" awesome_extensions: dependency: "direct main" description: @@ -139,10 +140,11 @@ packages: bloc_advanced_tools: dependency: "direct main" description: - path: "../bloc_advanced_tools" - relative: true - source: path - version: "0.1.4" + name: bloc_advanced_tools + sha256: "2b2dd492a350e7192a933d09f15ea04d5d00e7bd3fe2a906fe629cd461ddbf94" + url: "https://pub.dev" + source: hosted + version: "0.1.5" blurry_modal_progress_hud: dependency: "direct main" description: @@ -227,26 +229,26 @@ packages: dependency: transitive description: name: cached_network_image - sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" + sha256: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.4.0" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" + sha256: ff0c949e323d2a1b52be73acce5b4a7b04063e61414c8ca542dbba47281630a7 url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.1.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7" + sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" camera: dependency: transitive description: @@ -259,34 +261,34 @@ packages: dependency: transitive description: name: camera_android - sha256: "981654e0e56a4c735f7ecc7bd3921385eb5f7dd13deaf4a6431255d9731df01a" + sha256: "134b83167cc3c83199e8d75e5bcfde677fec843e7b2ca6b754a5b0b96d00d921" url: "https://pub.dev" source: hosted - version: "0.10.9+7" + version: "0.10.9+10" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: "7d021e8cd30d9b71b8b92b4ad669e80af432d722d18d6aac338572754a786c15" + sha256: b5093a82537b64bb88d4244f8e00b5ba69e822a5994f47b31d11400e1db975e5 url: "https://pub.dev" source: hosted - version: "0.9.16" + version: "0.9.17+1" camera_platform_interface: dependency: transitive description: name: camera_platform_interface - sha256: a250314a48ea337b35909a4c9d5416a208d736dcb01d0b02c6af122be66660b0 + sha256: b3ede1f171532e0d83111fe0980b46d17f1aa9788a07a2fbed07366bbdbb9061 url: "https://pub.dev" source: hosted - version: "2.7.4" + version: "2.8.0" camera_web: dependency: transitive description: name: camera_web - sha256: "9e9aba2fbab77ce2472924196ff8ac4dd8f9126c4f9a3096171cd1d870d6b26c" + sha256: b9235ec0a2ce949daec546f1f3d86f05c3921ed31c7d9ab6b7c03214d152fc2d url: "https://pub.dev" source: hosted - version: "0.3.3" + version: "0.3.4" change_case: dependency: "direct main" description: @@ -459,10 +461,10 @@ packages: dependency: "direct main" description: name: expansion_tile_group - sha256: "6918433891481c7d98cbc604d7b4c93509986e8134d52940853301ad6fbff404" + sha256: "47615665d4e610dee0b6362de9e81003b56b150b5765ea5444a091762b5dc7d5" url: "https://pub.dev" source: hosted - version: "1.2.4" + version: "1.3.0" fast_immutable_collections: dependency: "direct main" description: @@ -528,10 +530,10 @@ packages: dependency: transitive description: name: flutter_cache_manager - sha256: "395d6b7831f21f3b989ebedbb785545932adb9afe2622c1ffacf7f4b53a7e544" + sha256: a77f77806a790eb9ba0118a5a3a936e81c4fea2b61533033b2b0c3d50bbde5ea url: "https://pub.dev" source: hosted - version: "3.3.2" + version: "3.4.0" flutter_chat_types: dependency: "direct main" description: @@ -606,10 +608,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: c6b0b4c05c458e1c01ad9bcc14041dd7b1f6783d487be4386f793f47a8a4d03e + sha256: "9d98bd47ef9d34e803d438f17fd32b116d31009f534a6fa5ce3a1167f189a6de" url: "https://pub.dev" source: hosted - version: "2.0.20" + version: "2.0.21" flutter_shaders: dependency: transitive description: @@ -622,10 +624,10 @@ packages: dependency: "direct main" description: name: flutter_slidable - sha256: "673403d2eeef1f9e8483bd6d8d92aae73b1d8bd71f382bc3930f699c731bc27c" + sha256: "2c5611c0b44e20d180e4342318e1bbc28b0a44ad2c442f5df16962606fd3e8e3" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" flutter_spinkit: dependency: "direct main" description: @@ -691,10 +693,10 @@ packages: dependency: "direct main" description: name: freezed_annotation - sha256: f54946fdb1fa7b01f780841937b1a80783a20b393485f3f6cdf336fd6f4705f2 + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.4" frontend_server_client: dependency: transitive description: @@ -731,18 +733,18 @@ packages: dependency: "direct main" description: name: go_router - sha256: cdae1b9c8bd7efadcef6112e81c903662ef2ce105cbd220a04bbb7c3425b5554 + sha256: "39dd52168d6c59984454183148dc3a5776960c61083adfc708cc79a7b3ce1ba8" url: "https://pub.dev" source: hosted - version: "14.2.0" + version: "14.2.1" graphs: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" hive: dependency: transitive description: @@ -763,10 +765,10 @@ packages: dependency: transitive description: name: http - sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" http_multi_server: dependency: transitive description: @@ -947,10 +949,10 @@ packages: dependency: transitive description: name: octo_image - sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" package_config: dependency: transitive description: @@ -963,18 +965,18 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0 + sha256: "4de6c36df77ffbcef0a5aefe04669d33f2d18397fea228277b852a2d4e58e860" url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "8.0.1" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e + sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.1" pasteboard: dependency: "direct main" description: @@ -1003,18 +1005,18 @@ packages: dependency: "direct main" description: name: path_provider - sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: bca87b0165ffd7cdb9cad8edd22d18d2201e886d9a9f19b4fb3452ea7df3a72a + sha256: "490539678396d4c3c0b06efdaab75ae60675c3e0c66f72bc04c2e2c1e0e2abeb" url: "https://pub.dev" source: hosted - version: "2.2.6" + version: "2.2.9" path_provider_foundation: dependency: transitive description: @@ -1043,10 +1045,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" pdf: dependency: "direct main" description: @@ -1171,10 +1173,10 @@ packages: dependency: transitive description: name: qr - sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" qr_code_dart_scan: dependency: "direct main" description: @@ -1227,10 +1229,10 @@ packages: dependency: transitive description: name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" url: "https://pub.dev" source: hosted - version: "0.27.7" + version: "0.28.0" screen_retriever: dependency: transitive description: @@ -1258,9 +1260,11 @@ packages: searchable_listview: dependency: "direct main" description: - path: "../Searchable-Listview" - relative: true - source: path + path: "." + ref: main + resolved-ref: db0f7b6f1baec0250ecba82f3d161bac1cf23d7d + url: "https://gitlab.com/veilid/Searchable-Listview.git" + source: git version: "2.14.1" share_plus: dependency: "direct main" @@ -1282,58 +1286,58 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + sha256: c272f9cabca5a81adc9b0894381e9c1def363e980f960fa903c604c471b22f68 url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.1" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577" + sha256: "041be4d9d2dc6079cf342bc8b761b03787e3b71192d658220a56cac9c04a0294" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.0" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" + sha256: "671e7a931f55a08aa45be2a13fe7247f2a41237897df434b30d2012388191833" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.5.0" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + sha256: "2ba0510d3017f91655b7543e9ee46d48619de2a2af38e5c790423f7007c7ccc1" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" + sha256: "3a293170d4d9403c3254ee05b84e62e8a9b3c5808ebd17de6a33fe9ea6457936" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + sha256: "398084b47b7f92110683cac45c6dc4aae853db47e470e5ddcd52cab7f7196ab2" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" shelf: dependency: transitive description: @@ -1496,10 +1500,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" synchronized: dependency: transitive description: @@ -1592,18 +1596,18 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf + sha256: "94d8ad05f44c6d4e2ffe5567ab4d741b82d62e3c8e288cc1fcea45965edf47c9" url: "https://pub.dev" source: hosted - version: "6.3.3" + version: "6.3.8" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89" + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_linux: dependency: transitive description: @@ -1640,18 +1644,18 @@ packages: dependency: transitive description: name: url_launcher_windows - sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" uuid: dependency: "direct main" description: name: uuid - sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" + sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.4.2" value_layout_builder: dependency: transitive description: @@ -1734,26 +1738,26 @@ packages: dependency: transitive description: name: web_socket - sha256: "24301d8c293ce6fe327ffe6f59d8fd8834735f0ec36e4fd383ec7ff8a64aa078" + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: a2d56211ee4d35d9b344d9d4ce60f362e4f5d1aafb988302906bd732bc731276 + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.1" win32: dependency: transitive description: name: win32 - sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 + sha256: "015002c060f1ae9f41a818f2d5640389cc05283e368be19dc8d77cecb43c40c9" url: "https://pub.dev" source: hosted - version: "5.5.1" + version: "5.5.3" window_manager: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 2f38215..9777813 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,12 +14,12 @@ dependencies: animated_theme_switcher: ^2.0.10 ansicolor: ^2.0.2 archive: ^3.6.1 - async_tools: ^0.1.4 + async_tools: ^0.1.5 awesome_extensions: ^2.0.16 badges: ^3.1.2 basic_utils: ^5.7.0 bloc: ^8.1.4 - bloc_advanced_tools: ^0.1.4 + bloc_advanced_tools: ^0.1.5 blurry_modal_progress_hud: ^1.1.1 change_case: ^2.1.0 charcode: ^1.3.1 @@ -111,13 +111,13 @@ dependencies: xterm: ^4.0.0 zxing2: ^0.2.3 -dependency_overrides: - async_tools: - path: ../dart_async_tools - bloc_advanced_tools: - path: ../bloc_advanced_tools - searchable_listview: - path: ../Searchable-Listview +# dependency_overrides: +# async_tools: +# path: ../dart_async_tools +# bloc_advanced_tools: +# path: ../bloc_advanced_tools +# searchable_listview: +# path: ../Searchable-Listview # flutter_chat_ui: # path: ../flutter_chat_ui From 079efb02b93f548c88ed9f4105ffbd11888865ba Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 1 Aug 2024 16:03:50 -0500 Subject: [PATCH 172/270] minor ui cleanups --- lib/chat_list/views/chat_list_widget.dart | 12 ++-- .../chat_single_contact_item_widget.dart | 2 +- lib/contacts/views/contacts_browser.dart | 4 +- .../home/drawer_menu/menu_item_widget.dart | 56 +++++++++---------- lib/layout/home/home_screen.dart | 2 +- lib/theme/views/responsive.dart | 4 ++ lib/theme/views/styled_scaffold.dart | 2 +- 7 files changed, 43 insertions(+), 39 deletions(-) diff --git a/lib/chat_list/views/chat_list_widget.dart b/lib/chat_list/views/chat_list_widget.dart index 67bcc5d..8fc57cf 100644 --- a/lib/chat_list/views/chat_list_widget.dart +++ b/lib/chat_list/views/chat_list_widget.dart @@ -15,13 +15,13 @@ import '../chat_list.dart'; class ChatListWidget extends StatelessWidget { const ChatListWidget({super.key}); - Widget _itemBuilderDirect(proto.DirectChat direct, - IMap contactMap, bool busy) { + Widget _itemBuilderDirect( + proto.DirectChat direct, IMap contactMap) { final contact = contactMap[direct.localConversationRecordKey]; if (contact == null) { return const Text('...'); } - return ChatSingleContactItemWidget(contact: contact, disabled: busy) + return ChatSingleContactItemWidget(contact: contact) .paddingLTRB(0, 4, 0, 0); } @@ -70,8 +70,10 @@ class ChatListWidget extends StatelessWidget { itemBuilder: (c) { switch (c.whichKind()) { case proto.Chat_Kind.direct: - return _itemBuilderDirect(c.direct, contactMap, - contactListV.busy || chatListV.busy); + return _itemBuilderDirect( + c.direct, + contactMap, + ); case proto.Chat_Kind.group: return const Text( 'group chats not yet supported!'); 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 b186290..1bd5f64 100644 --- a/lib/chat_list/views/chat_single_contact_item_widget.dart +++ b/lib/chat_list/views/chat_single_contact_item_widget.dart @@ -11,7 +11,7 @@ import '../chat_list.dart'; class ChatSingleContactItemWidget extends StatelessWidget { const ChatSingleContactItemWidget({ required proto.Contact contact, - required bool disabled, + bool disabled = false, super.key, }) : _contact = contact, _disabled = disabled; diff --git a/lib/contacts/views/contacts_browser.dart b/lib/contacts/views/contacts_browser.dart index db2a602..2eea880 100644 --- a/lib/contacts/views/contacts_browser.dart +++ b/lib/contacts/views/contacts_browser.dart @@ -242,7 +242,7 @@ class _ContactsBrowserState extends State contact: contact, selected: widget.selectedContactRecordKey == contact.localConversationRecordKey.toVeilid(), - disabled: ciBusy, + disabled: false, onTap: _onTapContact, onDoubleTap: _onStartChat, onDelete: _onDeleteContact) @@ -250,7 +250,7 @@ class _ContactsBrowserState extends State case ContactsBrowserElementKind.invitation: final invitation = element.invitation!; return ContactInvitationItemWidget( - contactInvitationRecord: invitation, disabled: cilBusy) + contactInvitationRecord: invitation, disabled: false) .paddingLTRB(0, 4, 0, 0); } }, diff --git a/lib/layout/home/drawer_menu/menu_item_widget.dart b/lib/layout/home/drawer_menu/menu_item_widget.dart index 12c5008..e538b15 100644 --- a/lib/layout/home/drawer_menu/menu_item_widget.dart +++ b/lib/layout/home/drawer_menu/menu_item_widget.dart @@ -27,36 +27,34 @@ class MenuItemWidget extends StatelessWidget { @override Widget build(BuildContext context) => TextButton( - onPressed: callback, - style: TextButton.styleFrom(foregroundColor: foregroundColor).copyWith( - backgroundColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.hovered)) { - return backgroundHoverColor; - } - if (states.contains(WidgetState.focused)) { - return backgroundFocusColor; - } - return backgroundColor; - }), - side: WidgetStateBorderSide.resolveWith((states) { - if (states.contains(WidgetState.hovered)) { + onPressed: callback, + style: TextButton.styleFrom(foregroundColor: foregroundColor).copyWith( + backgroundColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.hovered)) { + return backgroundHoverColor; + } + if (states.contains(WidgetState.focused)) { + return backgroundFocusColor; + } + return backgroundColor; + }), + side: WidgetStateBorderSide.resolveWith((states) { + if (states.contains(WidgetState.hovered)) { + return borderColor != null + ? BorderSide(width: 2, color: borderHoverColor!) + : null; + } + if (states.contains(WidgetState.focused)) { + return borderColor != null + ? BorderSide(width: 2, color: borderFocusColor!) + : null; + } return borderColor != null - ? BorderSide(width: 2, color: borderHoverColor!) + ? BorderSide(width: 2, color: borderColor!) : null; - } - if (states.contains(WidgetState.focused)) { - return borderColor != null - ? BorderSide(width: 2, color: borderFocusColor!) - : null; - } - return borderColor != null - ? BorderSide(width: 2, color: borderColor!) - : null; - }), - shape: WidgetStateProperty.all(RoundedRectangleBorder( - borderRadius: BorderRadius.circular(borderRadius ?? 0)))), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), + }), + shape: WidgetStateProperty.all(RoundedRectangleBorder( + borderRadius: BorderRadius.circular(borderRadius ?? 0)))), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -83,7 +81,7 @@ class MenuItemWidget extends StatelessWidget { onPressed: footerCallback), ], ), - )); + ); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index 5fd463a..893ca97 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -230,7 +230,7 @@ class HomeScreenState extends State menuScreenTapClose: canClose, mainScreenTapClose: canClose, disableDragGesture: !canClose, - mainScreenScale: .15, + mainScreenScale: .25, slideWidth: min(360, MediaQuery.of(context).size.width * 0.9), ))); } diff --git a/lib/theme/views/responsive.dart b/lib/theme/views/responsive.dart index a80faf6..91af81d 100644 --- a/lib/theme/views/responsive.dart +++ b/lib/theme/views/responsive.dart @@ -14,6 +14,10 @@ const kMobileWidthCutoff = 500.0; bool isMobileWidth(BuildContext context) => MediaQuery.of(context).size.width < kMobileWidthCutoff; +bool isMobileSize(BuildContext context) => + MediaQuery.of(context).size.width < kMobileWidthCutoff || + MediaQuery.of(context).size.height < kMobileWidthCutoff; + bool responsiveVisibility({ required BuildContext context, bool phone = true, diff --git a/lib/theme/views/styled_scaffold.dart b/lib/theme/views/styled_scaffold.dart index 61e32aa..9560ecc 100644 --- a/lib/theme/views/styled_scaffold.dart +++ b/lib/theme/views/styled_scaffold.dart @@ -12,7 +12,7 @@ class StyledScaffold extends StatelessWidget { final scale = theme.extension()!; final scaleConfig = theme.extension()!; - final enableBorder = !isMobileWidth(context); + final enableBorder = !isMobileSize(context); final scaffold = clipBorder( clipEnabled: enableBorder, From 5e40fac6c4568c7057d9041cf47dbf9b0d7695ba Mon Sep 17 00:00:00 2001 From: TC Johnson Date: Thu, 1 Aug 2024 19:59:06 -0500 Subject: [PATCH 173/270] Updated changelog for v0.4.0 minor release --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9502be2..0a1a6e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,30 @@ +## v0.4.0 ## +- Long conversation support +- Account and consistency update + - Account and identity system upgrades + - Eventual consistency works better now + - Speed improvements + - High speed messaging torture test passes +- Multiple accounts support + - start of recovery key UI + - multiple accounts support + - many bugfixes + - improved watches + - ui improvements +- UI work + - Support away/busy/free state + - Support away/busy/free messages + - Start of UI for auto-away feature (incomplete) +- *Community Contributions Shoutouts* + - @ethnh + - @sajattack + - @jasikpark + - @kyanha + - @lpmi-13 + - @rivkasegan + - @SalvatoreT + - @hweslin + ## v0.3.0 ## - Beginning of changelog - See commits/merges for history prior to v0.3.0 From 551eedee6fc02c9e8bcc10cb36c62b1bdb5b3f71 Mon Sep 17 00:00:00 2001 From: TC Johnson Date: Thu, 1 Aug 2024 20:00:41 -0500 Subject: [PATCH 174/270] =?UTF-8?q?Version=20update:=20v0.3.0=20=E2=86=92?= =?UTF-8?q?=20v0.4.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index ba3e47f..2b4b328 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.3.0+0 +current_version = 0.4.0+0 commit = False tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)\+(?P\d+) diff --git a/pubspec.yaml b/pubspec.yaml index 9777813..e4ae2fc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: veilidchat description: VeilidChat publish_to: 'none' -version: 0.3.0+12 +version: 0.4.0+13 environment: sdk: '>=3.2.0 <4.0.0' From 4271fb51c685dabda0fa9159abb2fa3af4c7d744 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 2 Aug 2024 18:33:26 -0500 Subject: [PATCH 175/270] use non-bounce scroll physics because a lot of views want 'stick to bottom' scroll behavior fix creating new accounts --- .../repository/account_repository.dart | 28 +++++++++++++++---- lib/app.dart | 8 ++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/lib/account_manager/repository/account_repository.dart b/lib/account_manager/repository/account_repository.dart index 13954ba..c5058ba 100644 --- a/lib/account_manager/repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository.dart @@ -281,15 +281,33 @@ class AccountRepository { parent: parent)) .scope((r) async => r.recordPointer); + final groupChatRecords = await (await DHTShortArray.create( + debugName: 'AccountRepository::_newLocalAccount::GroupChats', + parent: parent)) + .scope((r) async => r.recordPointer); + // Make account object + final profile = proto.Profile() + ..name = accountSpec.name + ..pronouns = accountSpec.pronouns + ..about = accountSpec.about + ..status = accountSpec.status + ..availability = accountSpec.availability + ..timestamp = Veilid.instance.now().toInt64(); + final account = proto.Account() - ..profile.name = accountSpec.name - ..profile.pronouns = accountSpec.pronouns - ..profile.about = accountSpec.about - ..profile.status = accountSpec.status + ..profile = profile + ..invisible = accountSpec.invisible + ..autoAwayTimeoutMin = accountSpec.autoAwayTimeout ..contactList = contactList.toProto() ..contactInvitationRecords = contactInvitationRecords.toProto() - ..chatList = chatRecords.toProto(); + ..chatList = chatRecords.toProto() + ..groupChatList = groupChatRecords.toProto() + ..freeMessage = accountSpec.freeMessage + ..awayMessage = accountSpec.awayMessage + ..busyMessage = accountSpec.busyMessage + ..autodetectAway = accountSpec.autoAway; + return account.writeToBuffer(); }); diff --git a/lib/app.dart b/lib/app.dart index 98e349a..7ef0911 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -30,6 +30,13 @@ class AttachDetachIntent extends Intent { const AttachDetachIntent(); } +class ScrollBehaviorModified extends ScrollBehavior { + const ScrollBehaviorModified(); + @override + ScrollPhysics getScrollPhysics(BuildContext context) => + const ClampingScrollPhysics(); +} + class VeilidChatApp extends StatelessWidget { const VeilidChatApp({ required this.initialThemeData, @@ -159,6 +166,7 @@ class VeilidChatApp extends StatelessWidget { return DecoratedBox( decoration: BoxDecoration(gradient: gradient), child: MaterialApp.router( + scrollBehavior: const ScrollBehaviorModified(), debugShowCheckedModeBanner: false, routerConfig: context.read().router(), title: translate('app.title'), From 4482e59984a10786f9a2d1dcd0b30f2c0ddc1b54 Mon Sep 17 00:00:00 2001 From: TC Johnson Date: Fri, 2 Aug 2024 18:49:04 -0500 Subject: [PATCH 176/270] Updated changelog for v0.4.1 patch release --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a1a6e1..e565bb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## v0.4.1 ## +- Fix creating new accounts +- Switch to non-bounce scroll physics because a lot of views want 'stick to bottom' scroll behavior + ## v0.4.0 ## - Long conversation support - Account and consistency update From e2d57761e33c62186272b299d28b8be42165a71d Mon Sep 17 00:00:00 2001 From: TC Johnson Date: Fri, 2 Aug 2024 18:49:45 -0500 Subject: [PATCH 177/270] =?UTF-8?q?Version=20update:=20v0.4.0=20=E2=86=92?= =?UTF-8?q?=20v0.4.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 2b4b328..f070c6f 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.4.0+0 +current_version = 0.4.1+0 commit = False tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)\+(?P\d+) diff --git a/pubspec.yaml b/pubspec.yaml index e4ae2fc..653396f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: veilidchat description: VeilidChat publish_to: 'none' -version: 0.4.0+13 +version: 0.4.1+14 environment: sdk: '>=3.2.0 <4.0.0' From 1b7ac31085d4ad6117c4a28e28e150a0bfe114ea Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 2 Aug 2024 23:18:39 -0500 Subject: [PATCH 178/270] dialog cleanup --- assets/i18n/en.json | 1 + .../views/edit_account_page.dart | 12 +- .../views/new_account_page.dart | 6 +- lib/layout/home/home_screen.dart | 24 +- lib/theme/views/styled_alert.dart | 276 ++++++++++++++++++ lib/theme/views/views.dart | 1 + lib/theme/views/widget_helpers.dart | 14 - lib/veilid_processor/views/developer.dart | 30 +- .../views/signal_strength_meter.dart | 8 +- pubspec.lock | 18 +- pubspec.yaml | 2 +- 11 files changed, 320 insertions(+), 72 deletions(-) create mode 100644 lib/theme/views/styled_alert.dart diff --git a/assets/i18n/en.json b/assets/i18n/en.json index b2e6907..cb95eaf 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -94,6 +94,7 @@ "waiting_for_network": "Waiting For Network" }, "toast": { + "confirm": "Confirm", "error": "Error", "info": "Info" }, diff --git a/lib/account_manager/views/edit_account_page.dart b/lib/account_manager/views/edit_account_page.dart index e49b48a..eaa0e37 100644 --- a/lib/account_manager/views/edit_account_page.dart +++ b/lib/account_manager/views/edit_account_page.dart @@ -137,10 +137,10 @@ class _EditAccountPageState extends WindowSetupState { }); } } - } on Exception catch (e) { + } on Exception catch (e, st) { if (mounted) { - await showErrorModal( - context, translate('new_account_page.error'), 'Exception: $e'); + await showErrorStacktraceModal( + context: context, error: e, stackTrace: st); } } } @@ -205,10 +205,10 @@ class _EditAccountPageState extends WindowSetupState { }); } } - } on Exception catch (e) { + } on Exception catch (e, st) { if (mounted) { - await showErrorModal( - context, translate('new_account_page.error'), 'Exception: $e'); + await showErrorStacktraceModal( + context: context, error: e, stackTrace: st); } } } diff --git a/lib/account_manager/views/new_account_page.dart b/lib/account_manager/views/new_account_page.dart index ee2f62c..69c75ae 100644 --- a/lib/account_manager/views/new_account_page.dart +++ b/lib/account_manager/views/new_account_page.dart @@ -102,10 +102,10 @@ class _NewAccountPageState extends WindowSetupState { }); } } - } on Exception catch (e) { + } on Exception catch (e, st) { if (mounted) { - await showErrorModal( - context, translate('new_account_page.error'), 'Exception: $e'); + await showErrorStacktraceModal( + context: context, error: e, stackTrace: st); } } } diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index 893ca97..a90e110 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart'; import 'package:provider/provider.dart'; -import 'package:quickalert/quickalert.dart'; import 'package:transitioned_indexed_stack/transitioned_indexed_stack.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:veilid_support/veilid_support.dart'; @@ -63,25 +62,26 @@ class HomeScreenState extends State Future _doBetaDialog(BuildContext context) async { var displayBetaWarning = true; + final theme = Theme.of(context); + final scale = theme.extension()!; - await QuickAlert.show( + await showWarningWidgetModal( context: context, title: translate('splash.beta_title'), - widget: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ RichText( textAlign: TextAlign.center, text: TextSpan( children: [ TextSpan( text: translate('splash.beta_text'), - style: const TextStyle( - color: Colors.black87, - ), + style: theme.textTheme.bodyMedium! + .copyWith(color: scale.primaryScale.appText), ), TextSpan( text: 'https://veilid.com/chat/beta', - style: const TextStyle( - color: Colors.blue, + style: theme.textTheme.bodyMedium!.copyWith( + color: scale.primaryScale.primary, decoration: TextDecoration.underline, ), recognizer: TapGestureRecognizer() @@ -101,11 +101,13 @@ class HomeScreenState extends State }); }, )), - Text(translate('settings_page.display_beta_warning'), - style: const TextStyle(color: Colors.black)), + Text( + translate('settings_page.display_beta_warning'), + style: theme.textTheme.bodyMedium! + .copyWith(color: scale.primaryScale.appText), + ), ]), ]), - type: QuickAlertType.warning, ); final preferencesInstance = PreferencesRepository.instance; diff --git a/lib/theme/views/styled_alert.dart b/lib/theme/views/styled_alert.dart new file mode 100644 index 0000000..2104fa1 --- /dev/null +++ b/lib/theme/views/styled_alert.dart @@ -0,0 +1,276 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_translate/flutter_translate.dart'; +import 'package:rflutter_alert/rflutter_alert.dart'; + +import '../theme.dart'; + +AlertStyle _alertStyle(BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + + return AlertStyle( + animationType: AnimationType.grow, + //animationDuration: const Duration(milliseconds: 200), + alertBorder: RoundedRectangleBorder( + side: !scaleConfig.useVisualIndicators + ? BorderSide.none + : BorderSide( + strokeAlign: BorderSide.strokeAlignCenter, + color: scale.primaryScale.border, + width: 2), + borderRadius: BorderRadius.all( + Radius.circular(12 * scaleConfig.borderRadiusScale))), + // isButtonVisible: true, + // isCloseButton: true, + // isOverlayTapDismiss: true, + backgroundColor: scale.primaryScale.subtleBackground, + // overlayColor: Colors.black87, + titleStyle: theme.textTheme.titleMedium! + .copyWith(color: scale.primaryScale.appText), + // titleTextAlign: TextAlign.center, + descStyle: + theme.textTheme.bodyMedium!.copyWith(color: scale.primaryScale.appText), + // descTextAlign: TextAlign.center, + // buttonAreaPadding: const EdgeInsets.all(20.0), + // constraints: null, + // buttonsDirection: ButtonsDirection.row, + // alertElevation: null, + // alertPadding: defaultAlertPadding, + // alertAlignment: Alignment.center, + // isTitleSelectable: false, + // isDescSelectable: false, + // titlePadding: null, + //descPadding: const EdgeInsets.all(0.0), + ); +} + +Color _buttonColor(BuildContext context, bool highlight) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + + if (scaleConfig.useVisualIndicators && !scaleConfig.preferBorders) { + return scale.secondaryScale.border; + } + + return highlight + ? scale.secondaryScale.elementBackground + : scale.secondaryScale.hoverElementBackground; +} + +TextStyle _buttonTextStyle(BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + + if (scaleConfig.useVisualIndicators && !scaleConfig.preferBorders) { + return theme.textTheme.bodyMedium! + .copyWith(color: scale.secondaryScale.borderText); + } + + return theme.textTheme.bodyMedium! + .copyWith(color: scale.secondaryScale.appText); +} + +BoxBorder _buttonBorder(BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + + return Border.fromBorderSide(BorderSide( + color: scale.secondaryScale.border, + width: scaleConfig.preferBorders ? 2 : 0)); +} + +BorderRadius _buttonRadius(BuildContext context) { + final theme = Theme.of(context); + final scaleConfig = theme.extension()!; + + return BorderRadius.circular(8 * scaleConfig.borderRadiusScale); +} + +Future showErrorModal( + {required BuildContext context, + required String title, + required String text}) async { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + + await Alert( + context: context, + style: _alertStyle(context), + useRootNavigator: false, + type: AlertType.error, + //style: AlertStyle(), + title: title, + desc: text, + buttons: [ + DialogButton( + color: _buttonColor(context, false), + highlightColor: _buttonColor(context, true), + border: _buttonBorder(context), + radius: _buttonRadius(context), + width: 120, + onPressed: () { + Navigator.pop(context); + }, + child: Text( + translate('button.ok'), + style: _buttonTextStyle(context), + ), + ) + ], + + //backgroundColor: Colors.black, + //titleColor: Colors.white, + //textColor: Colors.white, + ).show(); +} + +Future showErrorStacktraceModal( + {required BuildContext context, + required Object error, + StackTrace? stackTrace}) async { + await showErrorModal( + context: context, + title: translate('toast.error'), + text: 'Error: {e}\n StackTrace: {st}', + ); +} + +Future showWarningModal( + {required BuildContext context, + required String title, + required String text}) async { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + + await Alert( + context: context, + style: _alertStyle(context), + useRootNavigator: false, + type: AlertType.warning, + //style: AlertStyle(), + title: title, + desc: text, + buttons: [ + DialogButton( + color: _buttonColor(context, false), + highlightColor: _buttonColor(context, true), + border: _buttonBorder(context), + radius: _buttonRadius(context), + width: 120, + onPressed: () { + Navigator.pop(context); + }, + child: Text( + translate('button.ok'), + style: _buttonTextStyle(context), + ), + ) + ], + + //backgroundColor: Colors.black, + //titleColor: Colors.white, + //textColor: Colors.white, + ).show(); +} + +Future showWarningWidgetModal( + {required BuildContext context, + required String title, + required Widget child}) async { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + + await Alert( + context: context, + style: _alertStyle(context), + useRootNavigator: false, + type: AlertType.warning, + //style: AlertStyle(), + title: title, + content: child, + buttons: [ + DialogButton( + color: _buttonColor(context, false), + highlightColor: _buttonColor(context, true), + border: _buttonBorder(context), + radius: _buttonRadius(context), + width: 120, + onPressed: () { + Navigator.pop(context); + }, + child: Text( + translate('button.ok'), + style: _buttonTextStyle(context), + ), + ) + ], + + //backgroundColor: Colors.black, + //titleColor: Colors.white, + //textColor: Colors.white, + ).show(); +} + +Future showConfirmModal( + {required BuildContext context, + required String title, + required String text}) async { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + + var confirm = false; + + await Alert( + context: context, + style: _alertStyle(context), + useRootNavigator: false, + type: AlertType.none, + title: title, + desc: text, + buttons: [ + DialogButton( + color: _buttonColor(context, false), + highlightColor: _buttonColor(context, true), + border: _buttonBorder(context), + radius: _buttonRadius(context), + width: 120, + onPressed: () { + Navigator.pop(context); + }, + child: Text( + translate('button.no_cancel'), + style: _buttonTextStyle(context), + ), + ), + DialogButton( + color: _buttonColor(context, false), + highlightColor: _buttonColor(context, true), + border: _buttonBorder(context), + radius: _buttonRadius(context), + width: 120, + onPressed: () { + confirm = true; + Navigator.pop(context); + }, + child: Text( + translate('button.yes_proceed'), + style: _buttonTextStyle(context), + ), + ) + ], + + //backgroundColor: Colors.black, + //titleColor: Colors.white, + //textColor: Colors.white, + ).show(); + + return confirm; +} diff --git a/lib/theme/views/views.dart b/lib/theme/views/views.dart index b81f184..e8aa1d8 100644 --- a/lib/theme/views/views.dart +++ b/lib/theme/views/views.dart @@ -8,6 +8,7 @@ export 'pop_control.dart'; export 'recovery_key_widget.dart'; export 'responsive.dart'; export 'scanner_error_widget.dart'; +export 'styled_alert.dart'; export 'styled_dialog.dart'; export 'styled_scaffold.dart'; export 'widget_helpers.dart'; diff --git a/lib/theme/views/widget_helpers.dart b/lib/theme/views/widget_helpers.dart index b079d2b..51cb7b6 100644 --- a/lib/theme/views/widget_helpers.dart +++ b/lib/theme/views/widget_helpers.dart @@ -8,7 +8,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_sticky_header/flutter_sticky_header.dart'; -import 'package:quickalert/quickalert.dart'; import 'package:sliver_expandable/sliver_expandable.dart'; import '../theme.dart'; @@ -196,19 +195,6 @@ class AsyncBlocBuilder>, S> data: (d) => builder(context, d))); } -Future showErrorModal( - BuildContext context, String title, String text) async { - await QuickAlert.show( - context: context, - type: QuickAlertType.error, - title: title, - text: text, - //backgroundColor: Colors.black, - //titleColor: Colors.white, - //textColor: Colors.white, - ); -} - SliverAppBar styledSliverAppBar( {required BuildContext context, required String title, Color? titleColor}) { final theme = Theme.of(context); diff --git a/lib/veilid_processor/views/developer.dart b/lib/veilid_processor/views/developer.dart index f4fe836..549d228 100644 --- a/lib/veilid_processor/views/developer.dart +++ b/lib/veilid_processor/views/developer.dart @@ -12,7 +12,6 @@ import 'package:flutter_bloc/flutter_bloc.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'; @@ -208,27 +207,14 @@ class _DeveloperPageState extends State { color: scale.primaryScale.primaryText, disabledColor: scale.primaryScale.primaryText.withAlpha(0x3F), onPressed: () async { - await QuickAlert.show( - context: context, - type: QuickAlertType.confirm, - title: translate('developer.are_you_sure_clear'), - titleColor: scale.primaryScale.appText, - textColor: scale.primaryScale.subtleText, - confirmBtnColor: scale.primaryScale.primary, - cancelBtnTextStyle: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 18, - color: scale.primaryScale.appText), - backgroundColor: scale.primaryScale.appBackground, - headerBackgroundColor: scale.primaryScale.primary, - confirmBtnText: translate('button.ok'), - cancelBtnText: translate('button.cancel'), - onConfirmBtnTap: () async { - Navigator.pop(context); - if (context.mounted) { - await clear(context); - } - }); + final confirm = await showConfirmModal( + context: context, + title: translate('toast.confirm'), + text: translate('developer.are_you_sure_clear'), + ); + if (confirm && context.mounted) { + await clear(context); + } }), CoolDropdown( controller: _logLevelController, diff --git a/lib/veilid_processor/views/signal_strength_meter.dart b/lib/veilid_processor/views/signal_strength_meter.dart index 73842f1..74230ed 100644 --- a/lib/veilid_processor/views/signal_strength_meter.dart +++ b/lib/veilid_processor/views/signal_strength_meter.dart @@ -4,7 +4,6 @@ import 'package:flutter/foundation.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'; @@ -75,11 +74,8 @@ class SignalStrengthMeterWidget extends StatelessWidget { 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}'), + () async => showErrorStacktraceModal( + context: context, error: e, stackTrace: st), ) }); diff --git a/pubspec.lock b/pubspec.lock index 2e8947c..d4f1ce7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1193,14 +1193,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" - quickalert: - dependency: "direct main" - description: - name: quickalert - sha256: b5d62b1e20b08cc0ff5f40b6da519bdc7a5de6082f13d90572cf4e72eea56c5e - url: "https://pub.dev" - source: hosted - version: "1.1.0" quiver: dependency: transitive description: @@ -1225,6 +1217,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.10" + rflutter_alert: + dependency: "direct main" + description: + name: rflutter_alert + sha256: "8ff35e3f9712ba24c746499cfa95bf320385edf38901a1a4eab0fe555867f66c" + url: "https://pub.dev" + source: hosted + version: "2.0.7" rxdart: dependency: transitive description: @@ -1702,7 +1702,7 @@ packages: path: "../veilid/veilid-flutter" relative: true source: path - version: "0.3.3" + version: "0.3.4" veilid_support: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 653396f..fe464ea 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -76,9 +76,9 @@ dependencies: provider: ^6.1.2 qr_code_dart_scan: ^0.8.0 qr_flutter: ^4.1.0 - quickalert: ^1.1.0 radix_colors: ^1.0.4 reorderable_grid: ^1.0.10 + rflutter_alert: ^2.0.7 screenshot: ^3.0.0 scroll_to_index: ^3.0.1 searchable_listview: From cbac96de99703d014e6b5046b8d2cfd40fc8d5c5 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 3 Aug 2024 11:50:33 -0500 Subject: [PATCH 179/270] exception handling work --- .../cubits/account_record_cubit.dart | 2 +- .../waiting_invitations_bloc_map_cubit.dart | 2 +- .../cubits/conversation_cubit.dart | 2 +- .../home/drawer_menu/menu_item_widget.dart | 2 +- lib/theme/views/avatar_widget.dart | 2 +- lib/theme/views/widget_helpers.dart | 2 +- .../dht_record/default_dht_record_cubit.dart | 9 +++++--- .../src/dht_record/dht_record_cubit.dart | 22 +++++++++---------- .../src/dht_record/dht_record_pool.dart | 2 +- 9 files changed, 24 insertions(+), 21 deletions(-) diff --git a/lib/account_manager/cubits/account_record_cubit.dart b/lib/account_manager/cubits/account_record_cubit.dart index 2a3d9e2..ee8ec89 100644 --- a/lib/account_manager/cubits/account_record_cubit.dart +++ b/lib/account_manager/cubits/account_record_cubit.dart @@ -56,7 +56,7 @@ class AccountRecordCubit extends DefaultDHTRecordCubit { Future _updateAccountAsync( AccountSpec accountSpec, Future Function() onSuccess) async { var changed = false; - await record.eventualUpdateProtobuf(proto.Account.fromBuffer, (old) async { + await record?.eventualUpdateProtobuf(proto.Account.fromBuffer, (old) async { changed = false; if (old == null) { return null; 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 97e1c76..3787205 100644 --- a/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart @@ -41,7 +41,7 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit close() async { - await _singleInvitationStatusProcessor.unfollow(); + await _singleInvitationStatusProcessor.close(); await super.close(); } diff --git a/lib/conversation/cubits/conversation_cubit.dart b/lib/conversation/cubits/conversation_cubit.dart index 11ba12f..b42d3a6 100644 --- a/lib/conversation/cubits/conversation_cubit.dart +++ b/lib/conversation/cubits/conversation_cubit.dart @@ -209,7 +209,7 @@ class ConversationCubit extends Cubit> { return; } serialFuture((this, _sfUpdateAccountChange), () async { - await cubit.record.eventualUpdateProtobuf(proto.Conversation.fromBuffer, + await cubit.record?.eventualUpdateProtobuf(proto.Conversation.fromBuffer, (old) async { if (old == null || old.profile == account.profile) { return null; diff --git a/lib/layout/home/drawer_menu/menu_item_widget.dart b/lib/layout/home/drawer_menu/menu_item_widget.dart index e538b15..8529411 100644 --- a/lib/layout/home/drawer_menu/menu_item_widget.dart +++ b/lib/layout/home/drawer_menu/menu_item_widget.dart @@ -80,7 +80,7 @@ class MenuItemWidget extends StatelessWidget { ), onPressed: footerCallback), ], - ), + ).paddingAll(2), ); @override diff --git a/lib/theme/views/avatar_widget.dart b/lib/theme/views/avatar_widget.dart index 43d351b..7a1f610 100644 --- a/lib/theme/views/avatar_widget.dart +++ b/lib/theme/views/avatar_widget.dart @@ -39,7 +39,7 @@ class AvatarWidget extends StatelessWidget { width: _size, decoration: BoxDecoration( shape: BoxShape.circle, - border: _scaleConfig.preferBorders + border: _scaleConfig.useVisualIndicators ? Border.all( color: _borderColor, width: 1 * (_size ~/ 32 + 1), diff --git a/lib/theme/views/widget_helpers.dart b/lib/theme/views/widget_helpers.dart index 51cb7b6..131ec8c 100644 --- a/lib/theme/views/widget_helpers.dart +++ b/lib/theme/views/widget_helpers.dart @@ -116,7 +116,7 @@ Widget buildProgressIndicator() => Builder(builder: (context) { return FittedBox( fit: BoxFit.scaleDown, child: SpinKitFoldingCube( - color: scale.tertiaryScale.primary, + color: scale.tertiaryScale.border, size: 80, )); }); diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart index 5ea6761..e5fb513 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart @@ -52,8 +52,11 @@ class DefaultDHTRecordCubit extends DHTRecordCubit { Future refreshDefault() async { await initWait(); - - final defaultSubkey = record.subkeyOrDefault(-1); - await refresh([ValueSubkeyRange(low: defaultSubkey, high: defaultSubkey)]); + final rec = record; + if (rec != null) { + final defaultSubkey = rec.subkeyOrDefault(-1); + await refresh( + [ValueSubkeyRange(low: defaultSubkey, high: defaultSubkey)]); + } } } 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 ac33716..6d42753 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 @@ -26,7 +26,7 @@ abstract class DHTRecordCubit extends Cubit> { // Do record open/create while (!cancel.isCompleted) { try { - _record = await open(); + record = await open(); _wantsCloseRecord = true; break; } on DHTExceptionNotAvailable { @@ -49,7 +49,7 @@ abstract class DHTRecordCubit extends Cubit> { ) async { // Make initial state update try { - final initialState = await initialStateFunction(_record); + final initialState = await initialStateFunction(record!); if (initialState != null) { emit(AsyncValue.data(initialState)); } @@ -57,7 +57,7 @@ abstract class DHTRecordCubit extends Cubit> { emit(AsyncValue.error(e)); } - _subscription = await _record.listen((record, data, subkeys) async { + _subscription = await record!.listen((record, data, subkeys) async { try { final newState = await stateFunction(record, subkeys, data); if (newState != null) { @@ -68,17 +68,17 @@ abstract class DHTRecordCubit extends Cubit> { } }); - await watchFunction(_record); + await watchFunction(record!); } @override Future close() async { await initWait(cancelValue: true); - await _record.cancelWatch(); + await record?.cancelWatch(); await _subscription?.cancel(); _subscription = null; if (_wantsCloseRecord) { - await _record.close(); + await record?.close(); _wantsCloseRecord = false; } await super.close(); @@ -91,10 +91,10 @@ abstract class DHTRecordCubit extends Cubit> { for (final skr in subkeys) { for (var sk = skr.low; sk <= skr.high; sk++) { - final data = await _record.get( - subkey: sk, refreshMode: DHTRecordRefreshMode.update); + final data = await record! + .get(subkey: sk, refreshMode: DHTRecordRefreshMode.update); if (data != null) { - final newState = await _stateFunction(_record, updateSubkeys, data); + final newState = await _stateFunction(record!, updateSubkeys, data); if (newState != null) { // Emit the new state emit(AsyncValue.data(newState)); @@ -108,13 +108,13 @@ abstract class DHTRecordCubit extends Cubit> { } } - DHTRecord get record => _record; + // DHTRecord get record => _record; @protected final WaitSet initWait = WaitSet(); StreamSubscription? _subscription; - late DHTRecord _record; + DHTRecord? record; bool _wantsCloseRecord; final StateFunction _stateFunction; } 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 68ea53d..3cfa784 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 @@ -849,7 +849,7 @@ class DHTRecordPool with TableDBBackedJson { openedRecordInfo.shared.needsWatchStateUpdate = false; } } on VeilidAPIException catch (e) { - // Failed to cancel DHT watch, try again next tick + // Failed to update DHT watch, try again next tick log('Exception in watch update: $e'); } } From ab9838f375a1f09141e95bd8e7329f493238da03 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 3 Aug 2024 15:53:11 -0500 Subject: [PATCH 180/270] cancellable waitingpage --- lib/chat/views/chat_component_widget.dart | 27 ++++++++++++++--------- lib/layout/home/home_account_ready.dart | 10 +++++++-- lib/theme/views/widget_helpers.dart | 16 +++++++++++--- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/lib/chat/views/chat_component_widget.dart b/lib/chat/views/chat_component_widget.dart index 5de02a7..3349f7f 100644 --- a/lib/chat/views/chat_component_widget.dart +++ b/lib/chat/views/chat_component_widget.dart @@ -20,9 +20,14 @@ import '../chat.dart'; const onEndReachedThreshold = 0.75; class ChatComponentWidget extends StatelessWidget { - const ChatComponentWidget( - {required super.key, required TypedKey localConversationRecordKey}) - : _localConversationRecordKey = localConversationRecordKey; + const ChatComponentWidget({ + required super.key, + required TypedKey localConversationRecordKey, + required void Function() onCancel, + required void Function() onClose, + }) : _localConversationRecordKey = localConversationRecordKey, + _onCancel = onCancel, + _onClose = onClose; ///////////////////////////////////////////////////////////////////// @@ -48,7 +53,7 @@ class ChatComponentWidget extends StatelessWidget { (x) => x.tryOperateSync(_localConversationRecordKey, closure: (cubit) => cubit)); if (activeConversationCubit == null) { - return waitingPage(); + return waitingPage(onCancel: _onCancel); } // Get the messages cubit @@ -57,7 +62,7 @@ class ChatComponentWidget extends StatelessWidget { (x) => x.tryOperateSync(_localConversationRecordKey, closure: (cubit) => cubit)); if (messagesCubit == null) { - return waitingPage(); + return waitingPage(onCancel: _onCancel); } // Make chat component state @@ -97,7 +102,7 @@ class ChatComponentWidget extends StatelessWidget { final localUser = chatComponentState.localUser; if (localUser == null) { - return waitingPage(); + return const EmptyChatWidget(); } final messageWindow = chatComponentState.messageWindow.asData?.value; @@ -135,10 +140,10 @@ class ChatComponentWidget extends StatelessWidget { )), const Spacer(), IconButton( - icon: Icon(Icons.close, color: scale.primaryScale.borderText), - onPressed: () async { - context.read().setActiveChat(null); - }).paddingLTRB(16, 0, 16, 0) + icon: + Icon(Icons.close, color: scale.primaryScale.borderText), + onPressed: _onClose) + .paddingLTRB(16, 0, 16, 0) ]), ), DecoratedBox( @@ -339,4 +344,6 @@ class ChatComponentWidget extends StatelessWidget { //////////////////////////////////////////////////////////////////////////// final TypedKey _localConversationRecordKey; + final void Function() _onCancel; + final void Function() _onClose; } diff --git a/lib/layout/home/home_account_ready.dart b/lib/layout/home/home_account_ready.dart index 5a6ab2b..7b67c0f 100644 --- a/lib/layout/home/home_account_ready.dart +++ b/lib/layout/home/home_account_ready.dart @@ -122,13 +122,19 @@ class _HomeAccountReadyState extends State { Material(color: Colors.transparent, child: buildUserPanel())); Widget buildRightPane(BuildContext context) { - final activeChatLocalConversationKey = - context.watch().state; + final activeChatCubit = context.watch(); + final activeChatLocalConversationKey = activeChatCubit.state; if (activeChatLocalConversationKey == null) { return const NoConversationWidget(); } return ChatComponentWidget( localConversationRecordKey: activeChatLocalConversationKey, + onCancel: () { + activeChatCubit.setActiveChat(null); + }, + onClose: () { + activeChatCubit.setActiveChat(null); + }, key: ValueKey(activeChatLocalConversationKey)); } diff --git a/lib/theme/views/widget_helpers.dart b/lib/theme/views/widget_helpers.dart index 131ec8c..8404e72 100644 --- a/lib/theme/views/widget_helpers.dart +++ b/lib/theme/views/widget_helpers.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_sticky_header/flutter_sticky_header.dart'; +import 'package:flutter_translate/flutter_translate.dart'; import 'package:sliver_expandable/sliver_expandable.dart'; import '../theme.dart'; @@ -121,7 +122,8 @@ Widget buildProgressIndicator() => Builder(builder: (context) { )); }); -Widget waitingPage({String? text}) => Builder(builder: (context) { +Widget waitingPage({String? text, void Function()? onCancel}) => + Builder(builder: (context) { final theme = Theme.of(context); final scale = theme.extension()!; return ColoredBox( @@ -130,12 +132,20 @@ Widget waitingPage({String? text}) => Builder(builder: (context) { crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.center, children: [ - buildProgressIndicator(), + buildProgressIndicator().paddingAll(24), if (text != null) Text(text, textAlign: TextAlign.center, style: theme.textTheme.bodySmall! - .copyWith(color: scale.tertiaryScale.appText)) + .copyWith(color: scale.tertiaryScale.appText)), + if (onCancel != null) + ElevatedButton( + onPressed: onCancel, + child: Text(translate('button.cancel'), + textAlign: TextAlign.center, + style: theme.textTheme.bodySmall!.copyWith( + color: scale.tertiaryScale.appText))) + .alignAtCenter(), ])); }); From 83880d79ba8f462e964446984a96e1718cf5e64b Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 3 Aug 2024 19:12:25 -0500 Subject: [PATCH 181/270] fix DHTRecordCubit open() retry bug improve error state reporting for cubits --- lib/chat/cubits/chat_component_cubit.dart | 1 + lib/chat/cubits/single_contact_messages_cubit.dart | 1 + lib/chat/views/empty_chat_widget.dart | 1 - .../active_single_contact_chat_bloc_map_cubit.dart | 1 + .../lib/dht_support/src/dht_log/dht_log_cubit.dart | 1 + .../dht_support/src/dht_record/dht_record_cubit.dart | 11 +++++++---- .../dht_support/src/dht_record/dht_record_pool.dart | 4 +++- .../src/dht_short_array/dht_short_array_cubit.dart | 6 ++++-- .../lib/src/async_table_db_backed_cubit.dart | 10 ++++++---- .../lib/src/table_db_array_protobuf_cubit.dart | 2 ++ packages/veilid_support/pubspec.lock | 6 +++--- packages/veilid_support/pubspec.yaml | 8 ++++---- pubspec.lock | 4 ++-- pubspec.yaml | 6 +++--- 14 files changed, 38 insertions(+), 24 deletions(-) diff --git a/lib/chat/cubits/chat_component_cubit.dart b/lib/chat/cubits/chat_component_cubit.dart index 2f2a2d0..17a481f 100644 --- a/lib/chat/cubits/chat_component_cubit.dart +++ b/lib/chat/cubits/chat_component_cubit.dart @@ -358,6 +358,7 @@ class ChatComponentCubit extends Cubit { final asError = avMessagesState.asError; if (asError != null) { + addError(asError.error, asError.stackTrace); return currentState.copyWith( unknownUsers: const IMap.empty(), messageWindow: AsyncValue.error(asError.error, asError.stackTrace)); diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 97e474f..cd2d7e5 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -180,6 +180,7 @@ class SingleContactMessagesCubit extends Cubit { _reconciliation = MessageReconciliation( output: _reconciledMessagesCubit!, onError: (e, st) { + addError(e, st); emit(AsyncValue.error(e, st)); }); diff --git a/lib/chat/views/empty_chat_widget.dart b/lib/chat/views/empty_chat_widget.dart index c975722..e441a46 100644 --- a/lib/chat/views/empty_chat_widget.dart +++ b/lib/chat/views/empty_chat_widget.dart @@ -7,7 +7,6 @@ class EmptyChatWidget extends StatelessWidget { const EmptyChatWidget({super.key}); @override - // ignore: prefer_expression_function_bodies Widget build( BuildContext context, ) { diff --git a/lib/conversation/cubits/active_single_contact_chat_bloc_map_cubit.dart b/lib/conversation/cubits/active_single_contact_chat_bloc_map_cubit.dart index 02202ed..f3dadcb 100644 --- a/lib/conversation/cubits/active_single_contact_chat_bloc_map_cubit.dart +++ b/lib/conversation/cubits/active_single_contact_chat_bloc_map_cubit.dart @@ -105,6 +105,7 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit extends Cubit> } } } on Exception catch (e, st) { + addError(e, st); emit(DHTLogBusyState(AsyncValue.error(e, st))); return; } 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 6d42753..cab5a77 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 @@ -35,6 +35,7 @@ abstract class DHTRecordCubit extends Cubit> { } } } on Exception catch (e, st) { + addError(e, st); emit(AsyncValue.error(e, st)); return; } @@ -53,8 +54,9 @@ abstract class DHTRecordCubit extends Cubit> { if (initialState != null) { emit(AsyncValue.data(initialState)); } - } on Exception catch (e) { - emit(AsyncValue.error(e)); + } on Exception catch (e, st) { + addError(e, st); + emit(AsyncValue.error(e, st)); } _subscription = await record!.listen((record, data, subkeys) async { @@ -63,8 +65,9 @@ abstract class DHTRecordCubit extends Cubit> { if (newState != null) { emit(AsyncValue.data(newState)); } - } on Exception catch (e) { - emit(AsyncValue.error(e)); + } on Exception catch (e, st) { + addError(e, st); + emit(AsyncValue.error(e, st)); } }); 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 3cfa784..f1ce324 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 @@ -402,11 +402,13 @@ class DHTRecordPool with TableDBBackedJson { recordDescriptor = await dhtctx.openDHTRecord(recordKey, writer: writer); break; + } on VeilidAPIExceptionTryAgain { + throw const DHTExceptionNotAvailable(); } on VeilidAPIExceptionKeyNotFound { await asyncSleep(); retry--; if (retry == 0) { - throw DHTExceptionNotAvailable(); + throw const DHTExceptionNotAvailable(); } } } 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 30309a3..8bdda7c 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 @@ -46,6 +46,7 @@ class DHTShortArrayCubit extends Cubit> } } } on Exception catch (e, st) { + addError(e, st); emit(DHTShortArrayBusyState(AsyncValue.error(e, st))); return; } @@ -96,8 +97,9 @@ class DHTShortArrayCubit extends Cubit> } emit(AsyncValue.data(newState)); setRefreshed(); - } on Exception catch (e) { - emit(AsyncValue.error(e)); + } on Exception catch (e, st) { + addError(e, st); + emit(AsyncValue.error(e, st)); } } 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 d637ee1..a41f4a3 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 @@ -27,8 +27,9 @@ abstract class AsyncTableDBBackedCubit extends Cubit> await _mutex.protect(() async { emit(AsyncValue.data(await load())); }); - } on Exception catch (e, stackTrace) { - emit(AsyncValue.error(e, stackTrace)); + } on Exception catch (e, st) { + addError(e, st); + emit(AsyncValue.error(e, st)); } } @@ -37,8 +38,9 @@ abstract class AsyncTableDBBackedCubit extends Cubit> await _initWait(); try { emit(AsyncValue.data(await store(newState))); - } on Exception catch (e, stackTrace) { - emit(AsyncValue.error(e, stackTrace)); + } on Exception catch (e, st) { + addError(e, st); + emit(AsyncValue.error(e, st)); } } diff --git a/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart b/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart index 89408ac..81d7f57 100644 --- a/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart +++ b/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart @@ -92,6 +92,7 @@ class TableDBArrayProtobufCubit final avElements = await _loadElements(_tail, _count); final err = avElements.asError; if (err != null) { + addError(err.error, err.stackTrace); emit(AsyncValue.error(err.error, err.stackTrace)); return; } @@ -123,6 +124,7 @@ class TableDBArrayProtobufCubit final allItems = (await _array.getRange(start, end)).toIList(); return AsyncValue.data(allItems); } on Exception catch (e, st) { + addError(e, st); return AsyncValue.error(e, st); } } diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index 5078198..c3162cf 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -53,10 +53,10 @@ packages: dependency: "direct main" description: name: bloc_advanced_tools - sha256: "2b2dd492a350e7192a933d09f15ea04d5d00e7bd3fe2a906fe629cd461ddbf94" + sha256: "2ad82be752ab5e983ad9097ed9f334e47a4472c04d5c6b61c99a1bb14a039053" url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.1.6" boolean_selector: dependency: transitive description: @@ -718,7 +718,7 @@ packages: path: "../../../veilid/veilid-flutter" relative: true source: path - version: "0.3.3" + version: "0.3.4" vm_service: dependency: transitive description: diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index cec41f7..280ef3b 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -9,7 +9,7 @@ environment: dependencies: async_tools: ^0.1.5 bloc: ^8.1.4 - bloc_advanced_tools: ^0.1.5 + bloc_advanced_tools: ^0.1.6 charcode: ^1.3.1 collection: ^1.18.0 equatable: ^2.0.5 @@ -26,11 +26,11 @@ dependencies: # veilid: ^0.0.1 path: ../../../veilid/veilid-flutter -# dependency_overrides: +#dependency_overrides: # async_tools: # path: ../../../dart_async_tools -# bloc_advanced_tools: -# path: ../../../bloc_advanced_tools +# bloc_advanced_tools: +# path: ../../../bloc_advanced_tools dev_dependencies: build_runner: ^2.4.10 diff --git a/pubspec.lock b/pubspec.lock index d4f1ce7..33a7671 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -141,10 +141,10 @@ packages: dependency: "direct main" description: name: bloc_advanced_tools - sha256: "2b2dd492a350e7192a933d09f15ea04d5d00e7bd3fe2a906fe629cd461ddbf94" + sha256: "2ad82be752ab5e983ad9097ed9f334e47a4472c04d5c6b61c99a1bb14a039053" url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.1.6" blurry_modal_progress_hud: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index fe464ea..871ba4d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: badges: ^3.1.2 basic_utils: ^5.7.0 bloc: ^8.1.4 - bloc_advanced_tools: ^0.1.5 + bloc_advanced_tools: ^0.1.6 blurry_modal_progress_hud: ^1.1.1 change_case: ^2.1.0 charcode: ^1.3.1 @@ -114,8 +114,8 @@ dependencies: # dependency_overrides: # async_tools: # path: ../dart_async_tools -# bloc_advanced_tools: -# path: ../bloc_advanced_tools +# bloc_advanced_tools: +# path: ../bloc_advanced_tools # searchable_listview: # path: ../Searchable-Listview # flutter_chat_ui: From 47287ba8d4436be08c6bbb7f41bc8aa6beec5843 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 3 Aug 2024 21:55:20 -0500 Subject: [PATCH 182/270] incremental chat state work --- lib/chat/cubits/chat_component_cubit.dart | 3 +- .../cubits/single_contact_messages_cubit.dart | 39 +++++++++++++++++-- .../active_conversations_bloc_map_cubit.dart | 10 ++--- ...ve_single_contact_chat_bloc_map_cubit.dart | 18 +++++++-- .../cubits/conversation_cubit.dart | 18 ++++----- lib/tools/loggy.dart | 6 +-- lib/tools/state_logger.dart | 6 +-- 7 files changed, 70 insertions(+), 30 deletions(-) diff --git a/lib/chat/cubits/chat_component_cubit.dart b/lib/chat/cubits/chat_component_cubit.dart index 17a481f..326a597 100644 --- a/lib/chat/cubits/chat_component_cubit.dart +++ b/lib/chat/cubits/chat_component_cubit.dart @@ -233,7 +233,8 @@ class ChatComponentCubit extends Cubit { return types.User( id: remoteIdentityPublicKey.toString(), - firstName: activeConversationState.remoteConversation.profile.name, + firstName: activeConversationState.remoteConversation?.profile.name ?? + '', metadata: {metadataKeyIdentityPublicKey: remoteIdentityPublicKey}); } diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index cd2d7e5..ab9c5ab 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -55,7 +55,7 @@ class SingleContactMessagesCubit extends Cubit { required TypedKey localConversationRecordKey, required TypedKey localMessagesRecordKey, required TypedKey remoteConversationRecordKey, - required TypedKey remoteMessagesRecordKey, + required TypedKey? remoteMessagesRecordKey, }) : _accountInfo = accountInfo, _remoteIdentityPublicKey = remoteIdentityPublicKey, _localConversationRecordKey = localConversationRecordKey, @@ -147,8 +147,14 @@ class SingleContactMessagesCubit extends Cubit { // Open remote messages key Future _initRcvdMessagesCubit() async { + // Don't bother if we don't have a remote messages record key yet + if (_remoteMessagesRecordKey == null) { + return; + } + + // Open new cubit if one is desired _rcvdMessagesCubit = DHTLogCubit( - open: () async => DHTLog.openRead(_remoteMessagesRecordKey, + open: () async => DHTLog.openRead(_remoteMessagesRecordKey!, debugName: 'SingleContactMessagesCubit::_initRcvdMessagesCubit::' 'RcvdMessages', parent: _remoteConversationRecordKey, @@ -159,6 +165,31 @@ class SingleContactMessagesCubit extends Cubit { _updateRcvdMessagesState(_rcvdMessagesCubit!.state); } + Future updateRemoteMessagesRecordKey( + TypedKey? remoteMessagesRecordKey) async { + await _initWait(); + + _sspRemoteConversationRecordKey.updateState(remoteMessagesRecordKey, + (remoteMessagesRecordKey) async { + // Don't bother if nothing is changing + if (_remoteMessagesRecordKey == remoteMessagesRecordKey) { + return; + } + + // Close existing cubit if we have one + final rcvdMessagesCubit = _rcvdMessagesCubit; + _rcvdMessagesCubit = null; + _remoteMessagesRecordKey = null; + await _rcvdSubscription?.cancel(); + _rcvdSubscription = null; + await rcvdMessagesCubit?.close(); + + // Init the new cubit if we should + _remoteMessagesRecordKey = remoteMessagesRecordKey; + await _initRcvdMessagesCubit(); + }); + } + Future _makeLocalMessagesCrypto() async => VeilidCryptoPrivate.fromTypedKey( _accountInfo.userLogin!.identitySecret, 'tabledb'); @@ -452,7 +483,7 @@ class SingleContactMessagesCubit extends Cubit { final TypedKey _localConversationRecordKey; final TypedKey _localMessagesRecordKey; final TypedKey _remoteConversationRecordKey; - final TypedKey _remoteMessagesRecordKey; + TypedKey? _remoteMessagesRecordKey; late final VeilidCrypto _conversationCrypto; late final MessageIntegrity _senderMessageIntegrity; @@ -471,4 +502,6 @@ class SingleContactMessagesCubit extends Cubit { _reconciledSubscription; final StreamController Function()> _commandController; late final Future _commandRunnerFut; + + final _sspRemoteConversationRecordKey = SingleStateProcessor(); } diff --git a/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart b/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart index b983265..4330db6 100644 --- a/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart +++ b/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart @@ -24,7 +24,7 @@ class ActiveConversationState extends Equatable { final TypedKey localConversationRecordKey; final TypedKey remoteConversationRecordKey; final proto.Conversation localConversation; - final proto.Conversation remoteConversation; + final proto.Conversation? remoteConversation; @override List get props => [ @@ -102,7 +102,8 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit, ConversationCubit>(conversationCubit, transform: (avstate) => avstate.when( - data: (data) => (data.localConversation == null || - data.remoteConversation == null) + data: (data) => (data.localConversation == null) ? const AsyncValue.loading() : AsyncValue.data(ActiveConversationState( localConversation: data.localConversation!, - remoteConversation: data.remoteConversation!, + remoteConversation: data.remoteConversation, remoteIdentityPublicKey: remoteIdentityPublicKey, localConversationRecordKey: localConversationRecordKey, remoteConversationRecordKey: diff --git a/lib/conversation/cubits/active_single_contact_chat_bloc_map_cubit.dart b/lib/conversation/cubits/active_single_contact_chat_bloc_map_cubit.dart index f3dadcb..90adafa 100644 --- a/lib/conversation/cubits/active_single_contact_chat_bloc_map_cubit.dart +++ b/lib/conversation/cubits/active_single_contact_chat_bloc_map_cubit.dart @@ -25,7 +25,7 @@ class _SingleContactChatState extends Equatable { final TypedKey localConversationRecordKey; final TypedKey remoteConversationRecordKey; final TypedKey localMessagesRecordKey; - final TypedKey remoteMessagesRecordKey; + final TypedKey? remoteMessagesRecordKey; @override List get props => [ @@ -53,8 +53,16 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit _addConversationMessages(_SingleContactChatState state) async => - add(() => MapEntry( + Future _addConversationMessages(_SingleContactChatState state) async { + // xxx could use atomic update() function + + final cubit = await tryOperateAsync( + state.localConversationRecordKey, closure: (cubit) async { + await cubit.updateRemoteMessagesRecordKey(state.remoteMessagesRecordKey); + return cubit; + }); + if (cubit == null) { + await add(() => MapEntry( state.localConversationRecordKey, SingleContactMessagesCubit( accountInfo: _accountInfo, @@ -64,6 +72,8 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit avInputState) { @@ -78,7 +88,7 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit> { localConversation: conv, remoteConversation: _incrementalState.remoteConversation); // return loading still if state isn't complete - if ((_localConversationRecordKey != null && - _incrementalState.localConversation == null) || - (_remoteConversationRecordKey != null && - _incrementalState.remoteConversation == null)) { + if (_localConversationRecordKey != null && + _incrementalState.localConversation == null) { return const AsyncValue.loading(); } - // state is complete, all required keys are open + // local state is complete, all remote state is emitted incrementally return AsyncValue.data(_incrementalState); }, loading: AsyncValue.loading, @@ -247,14 +245,12 @@ class ConversationCubit extends Cubit> { _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 loading still if the local state isn't complete + if (_localConversationRecordKey != null && + _incrementalState.localConversation == null) { return const AsyncValue.loading(); } - // state is complete, all required keys are open + // local state is complete, all remote state is emitted incrementally return AsyncValue.data(_incrementalState); }, loading: AsyncValue.loading, diff --git a/lib/tools/loggy.dart b/lib/tools/loggy.dart index 69faeb7..1df4c82 100644 --- a/lib/tools/loggy.dart +++ b/lib/tools/loggy.dart @@ -112,9 +112,9 @@ class CallbackPrinter extends LoggyPrinter { @override void onLog(LogRecord record) { final out = record.pretty(); - if (isDesktop) { - debugPrintSynchronously(out); - } + //if (isDesktop) { + debugPrintSynchronously(out); + //} globalDebugTerminal.write('$out\n'.replaceAll('\n', '\r\n')); callback?.call(record); } diff --git a/lib/tools/state_logger.dart b/lib/tools/state_logger.dart index 08e32b3..6baacae 100644 --- a/lib/tools/state_logger.dart +++ b/lib/tools/state_logger.dart @@ -4,12 +4,12 @@ import 'loggy.dart'; const Map _blocChangeLogLevels = { 'ConnectionStateCubit': LogLevel.off, - 'ActiveSingleContactChatBlocMapCubit': LogLevel.off, - 'ActiveConversationsBlocMapCubit': LogLevel.off, + //'ActiveSingleContactChatBlocMapCubit': LogLevel.off, + //'ActiveConversationsBlocMapCubit': LogLevel.off, 'PersistentQueueCubit': LogLevel.off, 'TableDBArrayProtobufCubit': LogLevel.off, 'DHTLogCubit': LogLevel.off, - 'SingleContactMessagesCubit': LogLevel.off, + //'SingleContactMessagesCubit': LogLevel.off, 'ChatComponentCubit': LogLevel.off, }; From 22390f31ff18a1c65f1ee92f2352821f0dc415dc Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 4 Aug 2024 07:27:25 -0500 Subject: [PATCH 183/270] log fix for ios and ability to delete orphaned chats --- assets/i18n/en.json | 1 + lib/chat_list/views/chat_list_widget.dart | 8 ++--- .../chat_single_contact_item_widget.dart | 33 +++++++++++-------- lib/tools/loggy.dart | 10 +++--- lib/tools/state_logger.dart | 6 ++-- 5 files changed, 34 insertions(+), 24 deletions(-) diff --git a/assets/i18n/en.json b/assets/i18n/en.json index cb95eaf..552da6f 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -233,6 +233,7 @@ "password_does_not_match": "Password does not match" }, "chat_list": { + "deleted_contact": "Deleted Contact", "search": "Search chats", "start_a_conversation": "Start A Conversation", "chats": "Chats", diff --git a/lib/chat_list/views/chat_list_widget.dart b/lib/chat_list/views/chat_list_widget.dart index 8fc57cf..f9a6ad3 100644 --- a/lib/chat_list/views/chat_list_widget.dart +++ b/lib/chat_list/views/chat_list_widget.dart @@ -18,10 +18,10 @@ class ChatListWidget extends StatelessWidget { Widget _itemBuilderDirect( proto.DirectChat direct, IMap contactMap) { final contact = contactMap[direct.localConversationRecordKey]; - if (contact == null) { - return const Text('...'); - } - return ChatSingleContactItemWidget(contact: contact) + return ChatSingleContactItemWidget( + localConversationRecordKey: + direct.localConversationRecordKey.toVeilid(), + contact: contact) .paddingLTRB(0, 4, 0, 0); } 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 1bd5f64..4ac992f 100644 --- a/lib/chat_list/views/chat_single_contact_item_widget.dart +++ b/lib/chat_list/views/chat_single_contact_item_widget.dart @@ -2,6 +2,7 @@ import 'package:async_tools/async_tools.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 '../../chat/cubits/active_chat_cubit.dart'; import '../../contacts/contacts.dart'; import '../../proto/proto.dart' as proto; @@ -10,13 +11,16 @@ import '../chat_list.dart'; class ChatSingleContactItemWidget extends StatelessWidget { const ChatSingleContactItemWidget({ - required proto.Contact contact, + required TypedKey localConversationRecordKey, + required proto.Contact? contact, bool disabled = false, super.key, - }) : _contact = contact, + }) : _localConversationRecordKey = localConversationRecordKey, + _contact = contact, _disabled = disabled; - final proto.Contact _contact; + final TypedKey _localConversationRecordKey; + final proto.Contact? _contact; final bool _disabled; @override @@ -29,13 +33,16 @@ class ChatSingleContactItemWidget extends StatelessWidget { final scaleConfig = theme.extension()!; final activeChatCubit = context.watch(); - final localConversationRecordKey = - _contact.localConversationRecordKey.toVeilid(); - final selected = activeChatCubit.state == localConversationRecordKey; + final selected = activeChatCubit.state == _localConversationRecordKey; - final name = _contact.nameOrNickname; - final title = _contact.displayName; - final subtitle = _contact.profile.status; + final name = _contact == null ? '?' : _contact.nameOrNickname; + final title = _contact == null + ? translate('chat_list.deleted_contact') + : _contact.displayName; + final subtitle = _contact == null ? '' : _contact.profile.status; + final availability = _contact == null + ? proto.Availability.AVAILABILITY_UNSPECIFIED + : _contact.profile.availability; final avatar = AvatarWidget( name: name, @@ -53,17 +60,17 @@ class ChatSingleContactItemWidget extends StatelessWidget { ); return SliderTile( - key: ObjectKey(_contact), + key: ValueKey(_localConversationRecordKey), disabled: _disabled, selected: selected, tileScale: ScaleKind.secondary, title: title, subtitle: subtitle, leading: avatar, - trailing: AvailabilityWidget(availability: _contact.profile.availability), + trailing: AvailabilityWidget(availability: availability), onTap: () { singleFuture(activeChatCubit, () async { - activeChatCubit.setActiveChat(localConversationRecordKey); + activeChatCubit.setActiveChat(_localConversationRecordKey); }); }, endActions: [ @@ -74,7 +81,7 @@ class ChatSingleContactItemWidget extends StatelessWidget { onPressed: (context) async { final chatListCubit = context.read(); await chatListCubit.deleteChat( - localConversationRecordKey: localConversationRecordKey); + localConversationRecordKey: _localConversationRecordKey); }) ], ); diff --git a/lib/tools/loggy.dart b/lib/tools/loggy.dart index 1df4c82..b22025e 100644 --- a/lib/tools/loggy.dart +++ b/lib/tools/loggy.dart @@ -9,7 +9,6 @@ import 'package:loggy/loggy.dart'; import 'package:veilid_support/veilid_support.dart'; import '../veilid_processor/views/developer.dart'; -import '../theme/views/responsive.dart'; import 'state_logger.dart'; String wrapWithLogColor(LogLevel? level, String text) { @@ -112,13 +111,16 @@ class CallbackPrinter extends LoggyPrinter { @override void onLog(LogRecord record) { final out = record.pretty(); - //if (isDesktop) { - debugPrintSynchronously(out); - //} + if (Platform.isAndroid) { + debugPrint(out); + } else { + debugPrintSynchronously(out); + } globalDebugTerminal.write('$out\n'.replaceAll('\n', '\r\n')); callback?.call(record); } + // ignore: use_setters_to_change_properties void setCallback(void Function(LogRecord)? cb) { callback = cb; } diff --git a/lib/tools/state_logger.dart b/lib/tools/state_logger.dart index 6baacae..08e32b3 100644 --- a/lib/tools/state_logger.dart +++ b/lib/tools/state_logger.dart @@ -4,12 +4,12 @@ import 'loggy.dart'; const Map _blocChangeLogLevels = { 'ConnectionStateCubit': LogLevel.off, - //'ActiveSingleContactChatBlocMapCubit': LogLevel.off, - //'ActiveConversationsBlocMapCubit': LogLevel.off, + 'ActiveSingleContactChatBlocMapCubit': LogLevel.off, + 'ActiveConversationsBlocMapCubit': LogLevel.off, 'PersistentQueueCubit': LogLevel.off, 'TableDBArrayProtobufCubit': LogLevel.off, 'DHTLogCubit': LogLevel.off, - //'SingleContactMessagesCubit': LogLevel.off, + 'SingleContactMessagesCubit': LogLevel.off, 'ChatComponentCubit': LogLevel.off, }; From 8edccb8a0f3b47999533048b1cf5149d32840e16 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 4 Aug 2024 18:49:49 -0500 Subject: [PATCH 184/270] fix deadlock clean up async handling improve styled alerts --- assets/i18n/en.json | 1 + .../cubits/account_record_cubit.dart | 4 +- ...per_account_collection_bloc_map_cubit.dart | 6 +- .../cubits/per_account_collection_cubit.dart | 8 +-- .../views/edit_account_page.dart | 55 +++++++++---------- .../cubits/contact_invitation_list_cubit.dart | 2 +- .../waiting_invitations_bloc_map_cubit.dart | 6 +- .../models/valid_contact_invitation.dart | 2 +- .../active_conversations_bloc_map_cubit.dart | 4 +- ...ve_single_contact_chat_bloc_map_cubit.dart | 29 ++++------ .../cubits/conversation_cubit.dart | 5 +- lib/theme/views/styled_alert.dart | 16 +++--- lib/tick.dart | 6 +- packages/veilid_support/example/pubspec.lock | 10 ++-- packages/veilid_support/example/pubspec.yaml | 2 +- .../src/dht_record/dht_record.dart | 2 +- .../src/dht_record/dht_record_pool.dart | 41 +++++++++++--- packages/veilid_support/pubspec.lock | 8 +-- packages/veilid_support/pubspec.yaml | 10 ++-- pubspec.lock | 8 +-- pubspec.yaml | 8 +-- 21 files changed, 125 insertions(+), 108 deletions(-) diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 552da6f..7e968da 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -4,6 +4,7 @@ }, "menu": { "accounts_menu_tooltip": "Accounts Menu", + "settings_tooltip": "Settings", "contacts_tooltip": "Contacts List", "new_chat_tooltip": "Start New Chat", "add_account_tooltip": "Add Account", diff --git a/lib/account_manager/cubits/account_record_cubit.dart b/lib/account_manager/cubits/account_record_cubit.dart index ee8ec89..a0a24a7 100644 --- a/lib/account_manager/cubits/account_record_cubit.dart +++ b/lib/account_manager/cubits/account_record_cubit.dart @@ -8,7 +8,7 @@ import '../../proto/proto.dart' as proto; import '../account_manager.dart'; typedef AccountRecordState = proto.Account; -typedef _sspUpdateState = ( +typedef _SspUpdateState = ( AccountSpec accountSpec, Future Function() onSuccess ); @@ -96,5 +96,5 @@ class AccountRecordCubit extends DefaultDHTRecordCubit { } } - final _sspUpdate = SingleStateProcessor<_sspUpdateState>(); + final _sspUpdate = SingleStateProcessor<_SspUpdateState>(); } diff --git a/lib/account_manager/cubits/per_account_collection_bloc_map_cubit.dart b/lib/account_manager/cubits/per_account_collection_bloc_map_cubit.dart index f5334c1..ded50e4 100644 --- a/lib/account_manager/cubits/per_account_collection_bloc_map_cubit.dart +++ b/lib/account_manager/cubits/per_account_collection_bloc_map_cubit.dart @@ -24,13 +24,13 @@ class PerAccountCollectionBlocMapCubit extends BlocMapCubit _addPerAccountCollectionCubit( {required TypedKey superIdentityRecordKey}) async => - add(() => MapEntry( + add( superIdentityRecordKey, - PerAccountCollectionCubit( + () async => PerAccountCollectionCubit( locator: _locator, accountInfoCubit: AccountInfoCubit( accountRepository: _accountRepository, - superIdentityRecordKey: superIdentityRecordKey)))); + superIdentityRecordKey: superIdentityRecordKey))); /// StateFollower ///////////////////////// diff --git a/lib/account_manager/cubits/per_account_collection_cubit.dart b/lib/account_manager/cubits/per_account_collection_cubit.dart index 226d213..089443a 100644 --- a/lib/account_manager/cubits/per_account_collection_cubit.dart +++ b/lib/account_manager/cubits/per_account_collection_cubit.dart @@ -78,13 +78,13 @@ class PerAccountCollectionCubit extends Cubit { await _accountRecordSubscription?.cancel(); _accountRecordSubscription = null; - // Update state to 'loading' - nextState = _updateAccountRecordState(nextState, null); - emit(nextState); - // Close AccountRecordCubit await accountRecordCubit?.close(); accountRecordCubit = null; + + // Update state to 'loading' + nextState = _updateAccountRecordState(nextState, null); + emit(nextState); } else { ///////////////// Logged in /////////////////// diff --git a/lib/account_manager/views/edit_account_page.dart b/lib/account_manager/views/edit_account_page.dart index eaa0e37..5674449 100644 --- a/lib/account_manager/views/edit_account_page.dart +++ b/lib/account_manager/views/edit_account_page.dart @@ -120,22 +120,22 @@ class _EditAccountPageState extends WindowSetupState { try { final success = await AccountRepository.instance.deleteLocalAccount( widget.superIdentityRecordKey, widget.accountRecord); - if (success && mounted) { - context - .read() - .info(text: translate('edit_account_page.account_removed')); - GoRouterHelper(context).pop(); - } else if (mounted) { - context - .read() - .error(text: translate('edit_account_page.failed_to_remove')); + if (mounted) { + if (success) { + context + .read() + .info(text: translate('edit_account_page.account_removed')); + GoRouterHelper(context).pop(); + } else { + context + .read() + .error(text: translate('edit_account_page.failed_to_remove')); + } } } finally { - if (mounted) { - setState(() { - _isInAsyncCall = false; - }); - } + setState(() { + _isInAsyncCall = false; + }); } } on Exception catch (e, st) { if (mounted) { @@ -188,22 +188,21 @@ class _EditAccountPageState extends WindowSetupState { try { final success = await AccountRepository.instance.destroyAccount( widget.superIdentityRecordKey, widget.accountRecord); - if (success && mounted) { - context - .read() - .info(text: translate('edit_account_page.account_destroyed')); - GoRouterHelper(context).pop(); - } else if (mounted) { - context - .read() - .error(text: translate('edit_account_page.failed_to_destroy')); + if (mounted) { + if (success) { + context + .read() + .info(text: translate('edit_account_page.account_destroyed')); + GoRouterHelper(context).pop(); + } else { + context.read().error( + text: translate('edit_account_page.failed_to_destroy')); + } } } finally { - if (mounted) { - setState(() { - _isInAsyncCall = false; - }); - } + setState(() { + _isInAsyncCall = false; + }); } } on Exception catch (e, st) { if (mounted) { diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index f5663af..959445c 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -252,7 +252,7 @@ class ContactInvitationListCubit .openRecordRead(contactRequestInboxKey, debugName: 'ContactInvitationListCubit::validateInvitation::' 'ContactRequestInbox', - parent: pool.getParentRecordKey(contactRequestInboxKey) ?? + parent: await pool.getParentRecordKey(contactRequestInboxKey) ?? _accountInfo.accountRecordKey) .withCancel(cancelRequest)) .maybeDeleteScope(!isSelf, (contactRequestInbox) async { 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 3787205..953575d 100644 --- a/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart @@ -48,15 +48,15 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit _addWaitingInvitation( {required proto.ContactInvitationRecord contactInvitationRecord}) async => - add(() => MapEntry( + add( contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(), - WaitingInvitationCubit( + () async => WaitingInvitationCubit( ContactRequestInboxCubit( accountInfo: _accountInfo, contactInvitationRecord: contactInvitationRecord), accountInfo: _accountInfo, accountRecordCubit: _accountRecordCubit, - contactInvitationRecord: contactInvitationRecord))); + contactInvitationRecord: contactInvitationRecord)); // Process all accepted or rejected invitations Future _invitationStatusListener( diff --git a/lib/contact_invitation/models/valid_contact_invitation.dart b/lib/contact_invitation/models/valid_contact_invitation.dart index fb8b8de..f19e951 100644 --- a/lib/contact_invitation/models/valid_contact_invitation.dart +++ b/lib/contact_invitation/models/valid_contact_invitation.dart @@ -37,7 +37,7 @@ class ValidContactInvitation { return (await pool.openRecordWrite(_contactRequestInboxKey, _writer, debugName: 'ValidContactInvitation::accept::' 'ContactRequestInbox', - parent: pool.getParentRecordKey(_contactRequestInboxKey) ?? + parent: await pool.getParentRecordKey(_contactRequestInboxKey) ?? _accountInfo.accountRecordKey)) // ignore: prefer_expression_function_bodies .maybeDeleteScope(!isSelf, (contactRequestInbox) async { diff --git a/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart b/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart index 4330db6..fbf0e80 100644 --- a/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart +++ b/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart @@ -78,7 +78,7 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit - add(() { + add(localConversationRecordKey, () async { // Conversation cubit the tracks the state between the local // and remote halves of a contact's relationship with this account final conversationCubit = ConversationCubit( @@ -123,7 +123,7 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit _addConversationMessages(_SingleContactChatState state) async { // xxx could use atomic update() function - - final cubit = await tryOperateAsync( - state.localConversationRecordKey, closure: (cubit) async { - await cubit.updateRemoteMessagesRecordKey(state.remoteMessagesRecordKey); - return cubit; - }); - if (cubit == null) { - await add(() => MapEntry( - state.localConversationRecordKey, - SingleContactMessagesCubit( - accountInfo: _accountInfo, - remoteIdentityPublicKey: state.remoteIdentityPublicKey, - localConversationRecordKey: state.localConversationRecordKey, - remoteConversationRecordKey: state.remoteConversationRecordKey, - localMessagesRecordKey: state.localMessagesRecordKey, - remoteMessagesRecordKey: state.remoteMessagesRecordKey, - ))); - } + await update(state.localConversationRecordKey, + onUpdate: (cubit) async => + cubit.updateRemoteMessagesRecordKey(state.remoteMessagesRecordKey), + onCreate: () async => SingleContactMessagesCubit( + accountInfo: _accountInfo, + remoteIdentityPublicKey: state.remoteIdentityPublicKey, + localConversationRecordKey: state.localConversationRecordKey, + remoteConversationRecordKey: state.remoteConversationRecordKey, + localMessagesRecordKey: state.localMessagesRecordKey, + remoteMessagesRecordKey: state.remoteMessagesRecordKey, + )); } _SingleContactChatState? _mapStateValue( diff --git a/lib/conversation/cubits/conversation_cubit.dart b/lib/conversation/cubits/conversation_cubit.dart index b5895d9..e2a1802 100644 --- a/lib/conversation/cubits/conversation_cubit.dart +++ b/lib/conversation/cubits/conversation_cubit.dart @@ -73,8 +73,9 @@ class ConversationCubit extends Cubit> { final record = await pool.openRecordRead(_remoteConversationRecordKey, debugName: 'ConversationCubit::RemoteConversation', - parent: pool.getParentRecordKey(_remoteConversationRecordKey) ?? - accountInfo.accountRecordKey, + parent: + await pool.getParentRecordKey(_remoteConversationRecordKey) ?? + accountInfo.accountRecordKey, crypto: crypto); return record; diff --git a/lib/theme/views/styled_alert.dart b/lib/theme/views/styled_alert.dart index 2104fa1..eb5b492 100644 --- a/lib/theme/views/styled_alert.dart +++ b/lib/theme/views/styled_alert.dart @@ -95,8 +95,8 @@ Future showErrorModal( required String title, required String text}) async { final theme = Theme.of(context); - final scale = theme.extension()!; - final scaleConfig = theme.extension()!; + // final scale = theme.extension()!; + // final scaleConfig = theme.extension()!; await Alert( context: context, @@ -145,8 +145,8 @@ Future showWarningModal( required String title, required String text}) async { final theme = Theme.of(context); - final scale = theme.extension()!; - final scaleConfig = theme.extension()!; + // final scale = theme.extension()!; + // final scaleConfig = theme.extension()!; await Alert( context: context, @@ -184,8 +184,8 @@ Future showWarningWidgetModal( required String title, required Widget child}) async { final theme = Theme.of(context); - final scale = theme.extension()!; - final scaleConfig = theme.extension()!; + // final scale = theme.extension()!; + // final scaleConfig = theme.extension()!; await Alert( context: context, @@ -223,8 +223,8 @@ Future showConfirmModal( required String title, required String text}) async { final theme = Theme.of(context); - final scale = theme.extension()!; - final scaleConfig = theme.extension()!; + // final scale = theme.extension()!; + // final scaleConfig = theme.extension()!; var confirm = false; diff --git a/lib/tick.dart b/lib/tick.dart index 8ec3db7..21c18a3 100644 --- a/lib/tick.dart +++ b/lib/tick.dart @@ -28,11 +28,7 @@ class BackgroundTickerState extends State { @override void dispose() { - final tickTimer = _tickTimer; - if (tickTimer != null) { - tickTimer.cancel(); - } - + _tickTimer?.cancel(); super.dispose(); } diff --git a/packages/veilid_support/example/pubspec.lock b/packages/veilid_support/example/pubspec.lock index aa857d4..236861d 100644 --- a/packages/veilid_support/example/pubspec.lock +++ b/packages/veilid_support/example/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: "direct dev" description: name: async_tools - sha256: "9166e8fe65fc65eb79202a6d540f4de768553d78141b885f5bd3f8d7d30eef5e" + sha256: "93df8b92d54d92e3323c630277e902b4ad4f05f798b55cfbc451e98c3e2fb7ba" url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.1.6" bloc: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: bloc_advanced_tools - sha256: "2b2dd492a350e7192a933d09f15ea04d5d00e7bd3fe2a906fe629cd461ddbf94" + sha256: f0b2dbe028792c97d1eb30480ed4e8035b5c70ea3bcc95a9c5255142592857f7 url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.1.7" boolean_selector: dependency: transitive description: @@ -650,7 +650,7 @@ packages: path: "../../../../veilid/veilid-flutter" relative: true source: path - version: "0.3.3" + version: "0.3.4" veilid_support: dependency: "direct main" description: diff --git a/packages/veilid_support/example/pubspec.yaml b/packages/veilid_support/example/pubspec.yaml index eb1c45a..17d17f4 100644 --- a/packages/veilid_support/example/pubspec.yaml +++ b/packages/veilid_support/example/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: path: ../ dev_dependencies: - async_tools: ^0.1.5 + async_tools: ^0.1.6 integration_test: sdk: flutter lint_hard: ^4.0.0 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 a0994bf..9aa380c 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 @@ -79,7 +79,7 @@ class DHTRecord implements DHTDeleteable { return false; } - await serialFuturePause((this, _sfListen)); + await serialFutureClose((this, _sfListen)); await _watchController?.close(); _watchController = null; await DHTRecordPool.instance._recordClosed(this); 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 f1ce324..cddea38 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 @@ -65,7 +65,7 @@ class OwnedDHTRecordPointer with _$OwnedDHTRecordPointer { class DHTRecordPool with TableDBBackedJson { DHTRecordPool._(Veilid veilid, VeilidRoutingContext routingContext) : _state = const DHTRecordPoolAllocations(), - _mutex = Mutex(), + _mutex = Mutex(debugLockTimeout: 30), _recordTagLock = AsyncTagLock(), _opened = {}, _markedForDelete = {}, @@ -207,10 +207,8 @@ class DHTRecordPool with TableDBBackedJson { ); /// Get the parent of a DHTRecord key if it exists - TypedKey? getParentRecordKey(TypedKey child) { - final childJson = child.toJson(); - return _state.parentByChild[childJson]; - } + Future getParentRecordKey(TypedKey child) => + _mutex.protect(() async => _getParentRecordKeyInner(child)); /// Check if record is allocated Future isValidRecordKey(TypedKey key) => @@ -505,12 +503,16 @@ class DHTRecordPool with TableDBBackedJson { // Check to see if this key can finally be deleted // If any parents are marked for deletion, try them first Future _checkForLateDeletesInner(TypedKey key) async { + if (!_mutex.isLocked) { + throw StateError('should be locked here'); + } + // Get parent list in bottom up order including our own key final parents = []; TypedKey? nextParent = key; while (nextParent != null) { parents.add(nextParent); - nextParent = getParentRecordKey(nextParent); + nextParent = _getParentRecordKeyInner(nextParent); } // If any parent is ready to delete all its children do it @@ -547,6 +549,10 @@ class DHTRecordPool with TableDBBackedJson { // Actual delete function Future _finalizeDeleteRecordInner(TypedKey recordKey) async { + if (!_mutex.isLocked) { + throw StateError('should be locked here'); + } + log('_finalizeDeleteRecordInner: key=$recordKey'); // Remove this child from parents @@ -557,6 +563,10 @@ class DHTRecordPool with TableDBBackedJson { // Deep delete mechanism inside mutex Future _deleteRecordInner(TypedKey recordKey) async { + if (!_mutex.isLocked) { + throw StateError('should be locked here'); + } + final toDelete = _readyForDeleteInner(recordKey); if (toDelete.isNotEmpty) { // delete now @@ -656,7 +666,20 @@ class DHTRecordPool with TableDBBackedJson { } } + TypedKey? _getParentRecordKeyInner(TypedKey child) { + if (!_mutex.isLocked) { + throw StateError('should be locked here'); + } + + final childJson = child.toJson(); + return _state.parentByChild[childJson]; + } + bool _isValidRecordKeyInner(TypedKey key) { + if (!_mutex.isLocked) { + throw StateError('should be locked here'); + } + if (_state.rootRecords.contains(key)) { return true; } @@ -667,6 +690,10 @@ class DHTRecordPool with TableDBBackedJson { } bool _isDeletedRecordKeyInner(TypedKey key) { + if (!_mutex.isLocked) { + throw StateError('should be locked here'); + } + // Is this key gone? if (!_isValidRecordKeyInner(key)) { return true; @@ -679,7 +706,7 @@ class DHTRecordPool with TableDBBackedJson { if (_markedForDelete.contains(nextParent)) { return true; } - nextParent = getParentRecordKey(nextParent); + nextParent = _getParentRecordKeyInner(nextParent); } return false; diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index c3162cf..1fdb0d3 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: "direct main" description: name: async_tools - sha256: "9166e8fe65fc65eb79202a6d540f4de768553d78141b885f5bd3f8d7d30eef5e" + sha256: "93df8b92d54d92e3323c630277e902b4ad4f05f798b55cfbc451e98c3e2fb7ba" url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.1.6" bloc: dependency: "direct main" description: @@ -53,10 +53,10 @@ packages: dependency: "direct main" description: name: bloc_advanced_tools - sha256: "2ad82be752ab5e983ad9097ed9f334e47a4472c04d5c6b61c99a1bb14a039053" + sha256: f0b2dbe028792c97d1eb30480ed4e8035b5c70ea3bcc95a9c5255142592857f7 url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "0.1.7" boolean_selector: dependency: transitive description: diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index 280ef3b..59b821d 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -7,9 +7,9 @@ environment: sdk: '>=3.2.0 <4.0.0' dependencies: - async_tools: ^0.1.5 + async_tools: ^0.1.6 bloc: ^8.1.4 - bloc_advanced_tools: ^0.1.6 + bloc_advanced_tools: ^0.1.7 charcode: ^1.3.1 collection: ^1.18.0 equatable: ^2.0.5 @@ -26,11 +26,11 @@ dependencies: # veilid: ^0.0.1 path: ../../../veilid/veilid-flutter -#dependency_overrides: +# dependency_overrides: # async_tools: # path: ../../../dart_async_tools -# bloc_advanced_tools: -# path: ../../../bloc_advanced_tools +# bloc_advanced_tools: +# path: ../../../bloc_advanced_tools dev_dependencies: build_runner: ^2.4.10 diff --git a/pubspec.lock b/pubspec.lock index 33a7671..bc10095 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -85,10 +85,10 @@ packages: dependency: "direct main" description: name: async_tools - sha256: "9166e8fe65fc65eb79202a6d540f4de768553d78141b885f5bd3f8d7d30eef5e" + sha256: "93df8b92d54d92e3323c630277e902b4ad4f05f798b55cfbc451e98c3e2fb7ba" url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.1.6" awesome_extensions: dependency: "direct main" description: @@ -141,10 +141,10 @@ packages: dependency: "direct main" description: name: bloc_advanced_tools - sha256: "2ad82be752ab5e983ad9097ed9f334e47a4472c04d5c6b61c99a1bb14a039053" + sha256: f0b2dbe028792c97d1eb30480ed4e8035b5c70ea3bcc95a9c5255142592857f7 url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "0.1.7" blurry_modal_progress_hud: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 871ba4d..e9efd02 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,12 +14,12 @@ dependencies: animated_theme_switcher: ^2.0.10 ansicolor: ^2.0.2 archive: ^3.6.1 - async_tools: ^0.1.5 + async_tools: ^0.1.6 awesome_extensions: ^2.0.16 badges: ^3.1.2 basic_utils: ^5.7.0 bloc: ^8.1.4 - bloc_advanced_tools: ^0.1.6 + bloc_advanced_tools: ^0.1.7 blurry_modal_progress_hud: ^1.1.1 change_case: ^2.1.0 charcode: ^1.3.1 @@ -114,8 +114,8 @@ dependencies: # dependency_overrides: # async_tools: # path: ../dart_async_tools -# bloc_advanced_tools: -# path: ../bloc_advanced_tools +# bloc_advanced_tools: +# path: ../bloc_advanced_tools # searchable_listview: # path: ../Searchable-Listview # flutter_chat_ui: From 6665e5dd0f818041b2911439e63f0c2a9679fa8f Mon Sep 17 00:00:00 2001 From: TC Johnson Date: Sun, 4 Aug 2024 19:27:06 -0500 Subject: [PATCH 185/270] Updated changelog for v0.4.2 patch release --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e565bb1..da4c057 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## v0.4.2 ## +- Dialogs cleanup +- Incremental chat state work +- Exception handling work +- Cancellable waiting page +- Fix DHTRecordCubit open() retry bug +- Log fix for iOS +- Add ability to delete orphaned chats +- Fix deadlock + ## v0.4.1 ## - Fix creating new accounts - Switch to non-bounce scroll physics because a lot of views want 'stick to bottom' scroll behavior From 120a7105c855848c02375879f7158be4bdea5a9e Mon Sep 17 00:00:00 2001 From: TC Johnson Date: Sun, 4 Aug 2024 19:27:38 -0500 Subject: [PATCH 186/270] =?UTF-8?q?Version=20update:=20v0.4.1=20=E2=86=92?= =?UTF-8?q?=20v0.4.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index f070c6f..ec9cca0 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.4.1+0 +current_version = 0.4.2+0 commit = False tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)\+(?P\d+) diff --git a/pubspec.yaml b/pubspec.yaml index e9efd02..7ad490d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: veilidchat description: VeilidChat publish_to: 'none' -version: 0.4.1+14 +version: 0.4.2+15 environment: sdk: '>=3.2.0 <4.0.0' From 103975bb5656e98d64aa9380ffd63708e6fd37f2 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 6 Aug 2024 08:51:19 -0700 Subject: [PATCH 187/270] mutex debugging --- assets/i18n/en.json | 4 +-- .../cubits/account_record_cubit.dart | 1 + .../views/edit_account_page.dart | 8 ++--- .../views/edit_profile_form.dart | 32 +++++++++++++------ .../cubits/single_contact_messages_cubit.dart | 2 +- .../views/empty_contact_list_widget.dart | 6 ++-- lib/init.dart | 2 +- lib/router/cubits/router_cubit.dart | 2 +- lib/theme/views/styled_alert.dart | 4 +-- lib/tools/loggy.dart | 2 +- .../lib/dht_support/src/dht_log/dht_log.dart | 4 +-- .../src/dht_log/dht_log_spine.dart | 5 +-- .../src/dht_record/dht_record.dart | 2 +- .../src/dht_record/dht_record_pool.dart | 27 +++++++++------- .../src/dht_short_array/dht_short_array.dart | 4 +-- .../dht_short_array/dht_short_array_head.dart | 2 +- .../lib/src/async_table_db_backed_cubit.dart | 3 +- packages/veilid_support/lib/src/config.dart | 12 +++---- .../lib/src/persistent_queue.dart | 3 +- .../lib/src/table_db_array.dart | 2 +- packages/veilid_support/pubspec.lock | 8 ++--- packages/veilid_support/pubspec.yaml | 4 +-- pubspec.lock | 8 ++--- pubspec.yaml | 6 ++-- 24 files changed, 88 insertions(+), 65 deletions(-) diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 7e968da..22f5840 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -90,8 +90,8 @@ "accept": "Accept", "reject": "Reject", "finish": "Finish", - "yes_proceed": "Yes, proceed", - "no_cancel": "No, cancel", + "yes": "Yes", + "no": "No", "waiting_for_network": "Waiting For Network" }, "toast": { diff --git a/lib/account_manager/cubits/account_record_cubit.dart b/lib/account_manager/cubits/account_record_cubit.dart index a0a24a7..9a73246 100644 --- a/lib/account_manager/cubits/account_record_cubit.dart +++ b/lib/account_manager/cubits/account_record_cubit.dart @@ -56,6 +56,7 @@ class AccountRecordCubit extends DefaultDHTRecordCubit { Future _updateAccountAsync( AccountSpec accountSpec, Future Function() onSuccess) async { var changed = false; + await record?.eventualUpdateProtobuf(proto.Account.fromBuffer, (old) async { changed = false; if (old == null) { diff --git a/lib/account_manager/views/edit_account_page.dart b/lib/account_manager/views/edit_account_page.dart index 5674449..3ba0f65 100644 --- a/lib/account_manager/views/edit_account_page.dart +++ b/lib/account_manager/views/edit_account_page.dart @@ -97,7 +97,7 @@ class _EditAccountPageState extends WindowSetupState { }, child: Row(mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.cancel, size: 16).paddingLTRB(0, 0, 4, 0), - Text(translate('button.no_cancel')).paddingLTRB(0, 0, 4, 0) + Text(translate('button.no')).paddingLTRB(0, 0, 4, 0) ])), ElevatedButton( onPressed: () { @@ -105,7 +105,7 @@ class _EditAccountPageState extends WindowSetupState { }, child: Row(mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0), - Text(translate('button.yes_proceed')).paddingLTRB(0, 0, 4, 0) + Text(translate('button.yes')).paddingLTRB(0, 0, 4, 0) ])) ]).paddingAll(24) ])); @@ -165,7 +165,7 @@ class _EditAccountPageState extends WindowSetupState { }, child: Row(mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.cancel, size: 16).paddingLTRB(0, 0, 4, 0), - Text(translate('button.no_cancel')).paddingLTRB(0, 0, 4, 0) + Text(translate('button.no')).paddingLTRB(0, 0, 4, 0) ])), ElevatedButton( onPressed: () { @@ -173,7 +173,7 @@ class _EditAccountPageState extends WindowSetupState { }, child: Row(mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0), - Text(translate('button.yes_proceed')).paddingLTRB(0, 0, 4, 0) + Text(translate('button.yes')).paddingLTRB(0, 0, 4, 0) ])) ]).paddingAll(24) ])); diff --git a/lib/account_manager/views/edit_profile_form.dart b/lib/account_manager/views/edit_profile_form.dart index 05e6ffe..54e61d1 100644 --- a/lib/account_manager/views/edit_profile_form.dart +++ b/lib/account_manager/views/edit_profile_form.dart @@ -2,6 +2,7 @@ import 'package:async_tools/async_tools.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:form_builder_validators/form_builder_validators.dart'; @@ -9,6 +10,7 @@ import 'package:form_builder_validators/form_builder_validators.dart'; import '../../contacts/contacts.dart'; import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; +import '../../veilid_processor/veilid_processor.dart'; import '../models/models.dart'; const _kDoUpdateSubmit = 'doUpdateSubmit'; @@ -291,16 +293,26 @@ class _EditProfileFormState extends State { const Spacer(), ]).paddingSymmetric(vertical: 4), if (widget.onSubmit != null) - ElevatedButton( - onPressed: widget.onSubmit == null ? null : _doSubmit, - child: Row(mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0), - Text((widget.onSubmit == null) - ? widget.submitDisabledText - : widget.submitText) - .paddingLTRB(0, 0, 4, 0) - ]), - ) + Builder(builder: (context) { + final networkReady = context + .watch() + .state + .asData + ?.value + .isPublicInternetReady ?? + false; + + return ElevatedButton( + onPressed: networkReady ? _doSubmit : null, + child: Row(mainAxisSize: MainAxisSize.min, children: [ + const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0), + Text(networkReady + ? widget.submitText + : widget.submitDisabledText) + .paddingLTRB(0, 0, 4, 0) + ]), + ); + }), ], ), ); diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index ab9c5ab..187a556 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -249,7 +249,7 @@ class SingleContactMessagesCubit extends Cubit { void runCommand(String command) { final (cmd, rest) = command.splitOnce(' '); - if (kDebugMode) { + if (kIsDebugMode) { if (cmd == '/repeat' && rest != null) { final (countStr, text) = rest.splitOnce(' '); final count = int.tryParse(countStr); diff --git a/lib/contacts/views/empty_contact_list_widget.dart b/lib/contacts/views/empty_contact_list_widget.dart index ae8a852..d787da3 100644 --- a/lib/contacts/views/empty_contact_list_widget.dart +++ b/lib/contacts/views/empty_contact_list_widget.dart @@ -1,3 +1,4 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter_translate/flutter_translate.dart'; @@ -15,7 +16,8 @@ class EmptyContactListWidget extends StatelessWidget { final textTheme = theme.textTheme; final scale = theme.extension()!; - return Column( + return Expanded( + child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.center, @@ -33,6 +35,6 @@ class EmptyContactListWidget extends StatelessWidget { ), ), ], - ); + )); } } diff --git a/lib/init.dart b/lib/init.dart index fc836a5..d2744c7 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -18,7 +18,7 @@ class VeilidChatGlobalInit { await getDefaultVeilidPlatformConfig(kIsWeb, VeilidChatApp.name)); // Veilid logging - initVeilidLog(kDebugMode); + initVeilidLog(kIsDebugMode); // Startup Veilid await ProcessorRepository.instance.startup(); diff --git a/lib/router/cubits/router_cubit.dart b/lib/router/cubits/router_cubit.dart index d442485..974319a 100644 --- a/lib/router/cubits/router_cubit.dart +++ b/lib/router/cubits/router_cubit.dart @@ -134,7 +134,7 @@ class RouterCubit extends Cubit { return _router = GoRouter( navigatorKey: _rootNavKey, refreshListenable: StreamListenable(stream.startWith(state).distinct()), - debugLogDiagnostics: kDebugMode, + debugLogDiagnostics: kIsDebugMode, initialLocation: '/', routes: routes, redirect: redirect, diff --git a/lib/theme/views/styled_alert.dart b/lib/theme/views/styled_alert.dart index eb5b492..23b9e9e 100644 --- a/lib/theme/views/styled_alert.dart +++ b/lib/theme/views/styled_alert.dart @@ -246,7 +246,7 @@ Future showConfirmModal( Navigator.pop(context); }, child: Text( - translate('button.no_cancel'), + translate('button.no'), style: _buttonTextStyle(context), ), ), @@ -261,7 +261,7 @@ Future showConfirmModal( Navigator.pop(context); }, child: Text( - translate('button.yes_proceed'), + translate('button.yes'), style: _buttonTextStyle(context), ), ) diff --git a/lib/tools/loggy.dart b/lib/tools/loggy.dart index b22025e..0bb259c 100644 --- a/lib/tools/loggy.dart +++ b/lib/tools/loggy.dart @@ -152,7 +152,7 @@ void initLoggy() { if (isTrace) { logLevel = traceLevel; } else { - logLevel = kDebugMode ? LogLevel.debug : LogLevel.info; + logLevel = kIsDebugMode ? LogLevel.debug : LogLevel.info; } Loggy('').level = getLogOptions(logLevel); diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart index 71bcfa2..8fb1999 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart @@ -305,10 +305,10 @@ class DHTLog implements DHTDeleteable { // Openable int _openCount; - final _mutex = Mutex(); + final _mutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); // Watch mutex to ensure we keep the representation valid - final Mutex _listenMutex = Mutex(); + final Mutex _listenMutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); // Stream of external changes StreamController? _watchController; } diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index e9442f0..7d8c519 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -713,7 +713,7 @@ class _DHTLogSpine { DHTShortArray.maxElements; // Spine head mutex to ensure we keep the representation valid - final Mutex _spineMutex = Mutex(); + final Mutex _spineMutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); // Subscription to head record internal changes StreamSubscription? _subscription; // Notify closure for external spine head changes @@ -733,7 +733,8 @@ class _DHTLogSpine { // LRU cache of DHT spine elements accessed recently // Pair of position and associated shortarray segment - final Mutex _spineCacheMutex = Mutex(); + final Mutex _spineCacheMutex = + Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); final List _openCache; final Map _openedSegments; static const int _openCacheSize = 3; 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 9aa380c..bddb4e7 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 @@ -562,7 +562,7 @@ class DHTRecord implements DHTDeleteable { final KeyPair? _writer; final VeilidCrypto _crypto; final String debugName; - final _mutex = Mutex(); + final _mutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); int _openCount; StreamController? _watchController; _WatchState? _watchState; 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 cddea38..3f55687 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 @@ -65,7 +65,7 @@ class OwnedDHTRecordPointer with _$OwnedDHTRecordPointer { class DHTRecordPool with TableDBBackedJson { DHTRecordPool._(Veilid veilid, VeilidRoutingContext routingContext) : _state = const DHTRecordPoolAllocations(), - _mutex = Mutex(debugLockTimeout: 30), + _mutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null), _recordTagLock = AsyncTagLock(), _opened = {}, _markedForDelete = {}, @@ -835,9 +835,11 @@ class DHTRecordPool with TableDBBackedJson { openedRecordInfo.shared.unionWatchState = null; openedRecordInfo.shared.needsWatchStateUpdate = false; + } on VeilidAPIExceptionTimeout { + log('Timeout in watch cancel for key=$openedRecordKey'); } on VeilidAPIException catch (e) { // Failed to cancel DHT watch, try again next tick - log('Exception in watch cancel: $e'); + log('Exception in watch cancel for key=$openedRecordKey: $e'); } return; } @@ -877,12 +879,22 @@ class DHTRecordPool with TableDBBackedJson { openedRecordInfo.records, realExpiration, renewalTime); openedRecordInfo.shared.needsWatchStateUpdate = false; } + } on VeilidAPIExceptionTimeout { + log('Timeout in watch update for key=$openedRecordKey'); } on VeilidAPIException catch (e) { // Failed to update DHT watch, try again next tick - log('Exception in watch update: $e'); + log('Exception in watch update for key=$openedRecordKey: $e'); + } + + // If we still need a state update after this then do a poll instead + if (openedRecordInfo.shared.needsWatchStateUpdate) { + _pollWatch(openedRecordKey, openedRecordInfo, unionWatchState); } } + // In lieu of a completed watch, set off a polling operation + // on the first value of the watched range, which, due to current + // veilid limitations can only be one subkey at a time right now void _pollWatch(TypedKey openedRecordKey, _OpenedRecordInfo openedRecordInfo, _WatchState unionWatchState) { singleFuture((this, _sfPollWatch, openedRecordKey), () async { @@ -942,18 +954,11 @@ class DHTRecordPool with TableDBBackedJson { final unionWatchState = _collectUnionWatchState(openedRecordInfo.records); - final processed = _watchStateProcessors.updateState( + _watchStateProcessors.updateState( openedRecordKey, unionWatchState, (newState) => _watchStateChange(openedRecordKey, unionWatchState)); - - // In lieu of a completed watch, set off a polling operation - // on the first value of the watched range, which, due to current - // veilid limitations can only be one subkey at a time right now - if (!processed && unionWatchState != null) { - _pollWatch(openedRecordKey, openedRecordInfo, unionWatchState); - } } } }); 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 d0d26a8..c0ec901 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 @@ -289,10 +289,10 @@ class DHTShortArray implements DHTDeleteable { // Openable int _openCount; - final _mutex = Mutex(); + final _mutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); // Watch mutex to ensure we keep the representation valid - final Mutex _listenMutex = Mutex(); + final Mutex _listenMutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); // Stream of external changes StreamController? _watchController; } 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 ff550e8..4a2c79a 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 @@ -518,7 +518,7 @@ class _DHTShortArrayHead { //////////////////////////////////////////////////////////////////////////// // Head/element mutex to ensure we keep the representation valid - final Mutex _headMutex = Mutex(); + final Mutex _headMutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); // Subscription to head record internal changes StreamSubscription? _subscription; // Notify closure for external head changes 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 a41f4a3..313d3e2 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 @@ -4,6 +4,7 @@ import 'package:async_tools/async_tools.dart'; import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; +import 'config.dart'; import 'table_db.dart'; abstract class AsyncTableDBBackedCubit extends Cubit> @@ -45,5 +46,5 @@ abstract class AsyncTableDBBackedCubit extends Cubit> } final WaitSet _initWait = WaitSet(); - final Mutex _mutex = Mutex(); + final Mutex _mutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); } diff --git a/packages/veilid_support/lib/src/config.dart b/packages/veilid_support/lib/src/config.dart index a8b5ea0..9f7703c 100644 --- a/packages/veilid_support/lib/src/config.dart +++ b/packages/veilid_support/lib/src/config.dart @@ -5,10 +5,10 @@ import 'package:path_provider/path_provider.dart'; import 'package:veilid/veilid.dart'; // ignore: do_not_use_environment -const bool _kReleaseMode = bool.fromEnvironment('dart.vm.product'); +const bool kIsReleaseMode = bool.fromEnvironment('dart.vm.product'); // ignore: do_not_use_environment -const bool _kProfileMode = bool.fromEnvironment('dart.vm.profile'); -const bool _kDebugMode = !_kReleaseMode && !_kProfileMode; +const bool kIsProfileMode = bool.fromEnvironment('dart.vm.profile'); +const bool kIsDebugMode = !kIsReleaseMode && !kIsProfileMode; Future> getDefaultVeilidPlatformConfig( bool isWeb, String appName) async { @@ -34,7 +34,7 @@ Future> getDefaultVeilidPlatformConfig( logging: VeilidWASMConfigLogging( performance: VeilidWASMConfigLoggingPerformance( enabled: true, - level: _kDebugMode + level: kIsDebugMode ? VeilidConfigLogLevel.debug : VeilidConfigLogLevel.info, logsInTimings: true, @@ -50,8 +50,8 @@ Future> getDefaultVeilidPlatformConfig( logging: VeilidFFIConfigLogging( terminal: VeilidFFIConfigLoggingTerminal( enabled: - _kDebugMode && (Platform.isIOS || Platform.isAndroid), - level: _kDebugMode + kIsDebugMode && (Platform.isIOS || Platform.isAndroid), + level: kIsDebugMode ? VeilidConfigLogLevel.debug : VeilidConfigLogLevel.info, ignoreLogTargets: ignoreLogTargets), diff --git a/packages/veilid_support/lib/src/persistent_queue.dart b/packages/veilid_support/lib/src/persistent_queue.dart index e59a470..750c48e 100644 --- a/packages/veilid_support/lib/src/persistent_queue.dart +++ b/packages/veilid_support/lib/src/persistent_queue.dart @@ -5,6 +5,7 @@ import 'package:async_tools/async_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:protobuf/protobuf.dart'; +import 'config.dart'; import 'table_db.dart'; class PersistentQueue @@ -203,7 +204,7 @@ class PersistentQueue final T Function(Uint8List) _fromBuffer; final bool _deleteOnClose; final WaitSet _initWait = WaitSet(); - final Mutex _queueMutex = Mutex(); + final Mutex _queueMutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); IList _queue = IList.empty(); final StreamController> _syncAddController = StreamController(); final StreamController _queueReady = StreamController(); diff --git a/packages/veilid_support/lib/src/table_db_array.dart b/packages/veilid_support/lib/src/table_db_array.dart index 7c3d4d4..c1d54cf 100644 --- a/packages/veilid_support/lib/src/table_db_array.dart +++ b/packages/veilid_support/lib/src/table_db_array.dart @@ -614,7 +614,7 @@ class _TableDBArrayBase { var _initDone = false; final VeilidCrypto _crypto; final WaitSet _initWait = WaitSet(); - final Mutex _mutex = Mutex(); + final Mutex _mutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); // Change tracking int _headDelta = 0; diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index 1fdb0d3..20798e2 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: "direct main" description: name: async_tools - sha256: "93df8b92d54d92e3323c630277e902b4ad4f05f798b55cfbc451e98c3e2fb7ba" + sha256: bbded696bfcb1437d0ca510ac047f261f9c7494fea2c488dd32ba2800e7f49e8 url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "0.1.7" bloc: dependency: "direct main" description: @@ -53,10 +53,10 @@ packages: dependency: "direct main" description: name: bloc_advanced_tools - sha256: f0b2dbe028792c97d1eb30480ed4e8035b5c70ea3bcc95a9c5255142592857f7 + sha256: d8a680d8a0469456399fb26bae9f7a1d2a1420b5bdf75e204e0fadab9edb0811 url: "https://pub.dev" source: hosted - version: "0.1.7" + version: "0.1.8" boolean_selector: dependency: transitive description: diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index 59b821d..2634603 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -7,9 +7,9 @@ environment: sdk: '>=3.2.0 <4.0.0' dependencies: - async_tools: ^0.1.6 + async_tools: ^0.1.7 bloc: ^8.1.4 - bloc_advanced_tools: ^0.1.7 + bloc_advanced_tools: ^0.1.8 charcode: ^1.3.1 collection: ^1.18.0 equatable: ^2.0.5 diff --git a/pubspec.lock b/pubspec.lock index bc10095..840746a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -85,10 +85,10 @@ packages: dependency: "direct main" description: name: async_tools - sha256: "93df8b92d54d92e3323c630277e902b4ad4f05f798b55cfbc451e98c3e2fb7ba" + sha256: bbded696bfcb1437d0ca510ac047f261f9c7494fea2c488dd32ba2800e7f49e8 url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "0.1.7" awesome_extensions: dependency: "direct main" description: @@ -141,10 +141,10 @@ packages: dependency: "direct main" description: name: bloc_advanced_tools - sha256: f0b2dbe028792c97d1eb30480ed4e8035b5c70ea3bcc95a9c5255142592857f7 + sha256: d8a680d8a0469456399fb26bae9f7a1d2a1420b5bdf75e204e0fadab9edb0811 url: "https://pub.dev" source: hosted - version: "0.1.7" + version: "0.1.8" blurry_modal_progress_hud: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 7ad490d..526a3e1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,12 +14,12 @@ dependencies: animated_theme_switcher: ^2.0.10 ansicolor: ^2.0.2 archive: ^3.6.1 - async_tools: ^0.1.6 + async_tools: ^0.1.7 awesome_extensions: ^2.0.16 badges: ^3.1.2 basic_utils: ^5.7.0 bloc: ^8.1.4 - bloc_advanced_tools: ^0.1.7 + bloc_advanced_tools: ^0.1.8 blurry_modal_progress_hud: ^1.1.1 change_case: ^2.1.0 charcode: ^1.3.1 @@ -112,7 +112,7 @@ dependencies: zxing2: ^0.2.3 # dependency_overrides: -# async_tools: +# async_tools: # path: ../dart_async_tools # bloc_advanced_tools: # path: ../bloc_advanced_tools From 19a366dcab9dc94bc0bca9a19d9f8665dfa0fec4 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 6 Aug 2024 10:29:03 -0700 Subject: [PATCH 188/270] contacts ui cleanup --- assets/i18n/en.json | 11 ++++--- .../cubits/single_contact_messages_cubit.dart | 1 - lib/contacts/views/contact_item_widget.dart | 13 +++++++-- lib/contacts/views/contacts_browser.dart | 8 ++--- lib/contacts/views/contacts_dialog.dart | 29 +++++++++++++++---- lib/contacts/views/edit_contact_form.dart | 25 ++++++++-------- .../views/empty_contact_list_widget.dart | 12 ++++---- lib/theme/views/styled_alert.dart | 8 ++--- lib/theme/views/widget_helpers.dart | 16 ++++++++++ 9 files changed, 84 insertions(+), 39 deletions(-) diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 22f5840..aa4d16e 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -86,10 +86,12 @@ "button": { "ok": "Ok", "cancel": "Cancel", + "edit": "Edit", "delete": "Delete", "accept": "Accept", "reject": "Reject", "finish": "Finish", + "close": "Close", "yes": "Yes", "no": "No", "waiting_for_network": "Waiting For Network" @@ -120,12 +122,13 @@ "contacts": "Contacts", "edit_contact": "Edit Contact", "invitations": "Invitations", - "no_contact_selected": "No contact selected", - "new_chat": "New Chat" + "no_contact_selected": "Double-click a contact to edit it", + "new_chat": "Open Chat", + "close_contact": "Close Contact" }, "contact_list": { "contacts": "Contacts", - "invite_people": "Invite people to VeilidChat", + "invite_people": "No contacts\n\nPress 'Create Invitation' to invite a contact to VeilidChat", "search": "Search contacts", "invitation": "Invitation", "loading_contacts": "Loading contacts..." @@ -134,7 +137,7 @@ "form_name": "Name", "form_pronouns": "Pronouns", "form_about": "About", - "form_status": "Current Status", + "form_status": "Status", "form_nickname": "Nickname", "form_notes": "Notes", "form_fingerprint": "Fingerprint", diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 187a556..b4e77db 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:async_tools/async_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:veilid_support/veilid_support.dart'; diff --git a/lib/contacts/views/contact_item_widget.dart b/lib/contacts/views/contact_item_widget.dart index 46b658a..4cb874d 100644 --- a/lib/contacts/views/contact_item_widget.dart +++ b/lib/contacts/views/contact_item_widget.dart @@ -5,7 +5,6 @@ import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; const _kOnTap = 'onTap'; -const _kOnDelete = 'onDelete'; class ContactItemWidget extends StatelessWidget { const ContactItemWidget( @@ -70,13 +69,23 @@ class ContactItemWidget extends StatelessWidget { await _onTap(_contact); }), endActions: [ + if (_onDoubleTap != null) + SliderTileAction( + icon: Icons.edit, + label: translate('button.edit'), + actionScale: ScaleKind.secondary, + onPressed: (_context) => + singleFuture((this, _kOnTap), () async { + await _onDoubleTap(_contact); + }), + ), if (_onDelete != null) SliderTileAction( icon: Icons.delete, label: translate('button.delete'), actionScale: ScaleKind.tertiary, onPressed: (_context) => - singleFuture((this, _kOnDelete), () async { + singleFuture((this, _kOnTap), () async { await _onDelete(_contact); }), ), diff --git a/lib/contacts/views/contacts_browser.dart b/lib/contacts/views/contacts_browser.dart index 2eea880..89cea88 100644 --- a/lib/contacts/views/contacts_browser.dart +++ b/lib/contacts/views/contacts_browser.dart @@ -191,13 +191,13 @@ class _ContactsBrowserState extends State //final scaleConfig = theme.extension()!; final cilState = context.watch().state; - final cilBusy = cilState.busy; + //final cilBusy = cilState.busy; final contactInvitationRecordList = cilState.state.asData?.value.map((x) => x.value).toIList() ?? const IListConst([]); final ciState = context.watch().state; - final ciBusy = ciState.busy; + //final ciBusy = ciState.busy; final contactList = ciState.state.asData?.value.map((x) => x.value).toIList(); @@ -243,8 +243,8 @@ class _ContactsBrowserState extends State selected: widget.selectedContactRecordKey == contact.localConversationRecordKey.toVeilid(), disabled: false, - onTap: _onTapContact, - onDoubleTap: _onStartChat, + onDoubleTap: _onTapContact, + onTap: _onStartChat, onDelete: _onDeleteContact) .paddingLTRB(0, 4, 0, 0); case ContactsBrowserElementKind.invitation: diff --git a/lib/contacts/views/contacts_dialog.dart b/lib/contacts/views/contacts_dialog.dart index f994148..8d65338 100644 --- a/lib/contacts/views/contacts_dialog.dart +++ b/lib/contacts/views/contacts_dialog.dart @@ -75,12 +75,29 @@ class _ContactsDialogState extends State { : null, actions: [ if (_selectedContact != null) - IconButton( - icon: const Icon(Icons.chat_bubble), - tooltip: translate('contacts_dialog.new_chat'), - onPressed: () async { - await onChatStarted(_selectedContact!); - }) + Column(mainAxisSize: MainAxisSize.min, children: [ + IconButton( + icon: const Icon(Icons.chat_bubble), + tooltip: translate('contacts_dialog.new_chat'), + onPressed: () async { + await onChatStarted(_selectedContact!); + }), + Text(translate('contacts_dialog.new_chat'), + style: theme.textTheme.labelSmall! + .copyWith(color: scale.primaryScale.borderText)), + ]).paddingLTRB(8, 0, 8, 0), + if (enableSplit && _selectedContact != null) + Column(mainAxisSize: MainAxisSize.min, children: [ + IconButton( + icon: const Icon(Icons.close), + tooltip: translate('contacts_dialog.close_contact'), + onPressed: () async { + await onContactSelected(null); + }), + Text(translate('contacts_dialog.close_contact'), + style: theme.textTheme.labelSmall! + .copyWith(color: scale.primaryScale.borderText)), + ]).paddingLTRB(8, 0, 8, 0), ]), body: LayoutBuilder(builder: (context, constraint) { final maxWidth = constraint.maxWidth; diff --git a/lib/contacts/views/edit_contact_form.dart b/lib/contacts/views/edit_contact_form.dart index 0e8acc1..e515f14 100644 --- a/lib/contacts/views/edit_contact_form.dart +++ b/lib/contacts/views/edit_contact_form.dart @@ -67,6 +67,7 @@ class _EditContactFormState extends State { return FormBuilder( key: widget.formKey, child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ AvatarWidget( name: widget.contact.profile.name, @@ -79,53 +80,53 @@ class _EditContactFormState extends State { ).paddingLTRB(0, 0, 0, 16), SelectableText(widget.contact.profile.name, style: textTheme.headlineMedium) - .decoratorLabel( + .noEditDecoratorLabel( context, translate('contact_form.form_name'), scale: scale.secondaryScale, ) - .paddingSymmetric(vertical: 8), + .paddingSymmetric(vertical: 4), SelectableText(widget.contact.profile.pronouns, style: textTheme.headlineSmall) - .decoratorLabel( + .noEditDecoratorLabel( context, translate('contact_form.form_pronouns'), scale: scale.secondaryScale, ) - .paddingSymmetric(vertical: 8), - Row(children: [ + .paddingSymmetric(vertical: 4), + Row(mainAxisSize: MainAxisSize.min, children: [ _availabilityWidget(context, widget.contact.profile.availability), SelectableText(widget.contact.profile.status, style: textTheme.bodyMedium) .paddingSymmetric(horizontal: 8) ]) - .decoratorLabel( + .noEditDecoratorLabel( context, translate('contact_form.form_status'), scale: scale.secondaryScale, ) - .paddingSymmetric(vertical: 8), + .paddingSymmetric(vertical: 4), SelectableText(widget.contact.profile.about, minLines: 1, maxLines: 8, style: textTheme.bodyMedium) - .decoratorLabel( + .noEditDecoratorLabel( context, translate('contact_form.form_about'), scale: scale.secondaryScale, ) - .paddingSymmetric(vertical: 8), + .paddingSymmetric(vertical: 4), SelectableText( widget.contact.identityPublicKey.value.toVeilid().toString(), style: textTheme.labelMedium! .copyWith(fontFamily: 'Source Code Pro')) - .decoratorLabel( + .noEditDecoratorLabel( context, translate('contact_form.form_fingerprint'), scale: scale.secondaryScale, ) - .paddingSymmetric(vertical: 8), + .paddingSymmetric(vertical: 4), Divider(color: border).paddingLTRB(8, 0, 8, 8), FormBuilderTextField( - autofocus: true, + //autofocus: true, name: EditContactForm.formFieldNickname, initialValue: widget.contact.nickname, decoration: InputDecoration( diff --git a/lib/contacts/views/empty_contact_list_widget.dart b/lib/contacts/views/empty_contact_list_widget.dart index d787da3..2563a1d 100644 --- a/lib/contacts/views/empty_contact_list_widget.dart +++ b/lib/contacts/views/empty_contact_list_widget.dart @@ -1,4 +1,3 @@ -import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter_translate/flutter_translate.dart'; @@ -22,14 +21,15 @@ class EmptyContactListWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.person_add_sharp, - color: scale.primaryScale.subtleBorder, - size: 48, - ), + // Icon( + // Icons.person_add_sharp, + // color: scale.primaryScale.subtleBorder, + // size: 48, + // ), Text( textAlign: TextAlign.center, translate('contact_list.invite_people'), + //maxLines: 3, style: textTheme.bodyMedium?.copyWith( color: scale.primaryScale.subtleBorder, ), diff --git a/lib/theme/views/styled_alert.dart b/lib/theme/views/styled_alert.dart index 23b9e9e..39fa6f2 100644 --- a/lib/theme/views/styled_alert.dart +++ b/lib/theme/views/styled_alert.dart @@ -94,7 +94,7 @@ Future showErrorModal( {required BuildContext context, required String title, required String text}) async { - final theme = Theme.of(context); + // final theme = Theme.of(context); // final scale = theme.extension()!; // final scaleConfig = theme.extension()!; @@ -144,7 +144,7 @@ Future showWarningModal( {required BuildContext context, required String title, required String text}) async { - final theme = Theme.of(context); + // final theme = Theme.of(context); // final scale = theme.extension()!; // final scaleConfig = theme.extension()!; @@ -183,7 +183,7 @@ Future showWarningWidgetModal( {required BuildContext context, required String title, required Widget child}) async { - final theme = Theme.of(context); + // final theme = Theme.of(context); // final scale = theme.extension()!; // final scaleConfig = theme.extension()!; @@ -222,7 +222,7 @@ Future showConfirmModal( {required BuildContext context, required String title, required String text}) async { - final theme = Theme.of(context); + // final theme = Theme.of(context); // final scale = theme.extension()!; // final scaleConfig = theme.extension()!; diff --git a/lib/theme/views/widget_helpers.dart b/lib/theme/views/widget_helpers.dart index 8404e72..970e065 100644 --- a/lib/theme/views/widget_helpers.dart +++ b/lib/theme/views/widget_helpers.dart @@ -109,6 +109,22 @@ extension LabelExt on Widget { ), child: this); } + + Widget noEditDecoratorLabel(BuildContext context, String label, + {ScaleColor? scale}) { + final theme = Theme.of(context); + final scaleScheme = theme.extension()!; + // final scaleConfig = theme.extension()!; + scale = scale ?? scaleScheme.primaryScale; + + return Wrap(crossAxisAlignment: WrapCrossAlignment.center, children: [ + Text( + '$label:', + style: theme.textTheme.titleLarge!.copyWith(color: scale.border), + ).paddingLTRB(0, 0, 8, 8), + this + ]); + } } Widget buildProgressIndicator() => Builder(builder: (context) { From 6102f32587fe1ad13074336266f94d8cf43222db Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 6 Aug 2024 10:36:18 -0700 Subject: [PATCH 189/270] fix fitting --- lib/contacts/views/contacts_dialog.dart | 51 ++++++++++++++----------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/lib/contacts/views/contacts_dialog.dart b/lib/contacts/views/contacts_dialog.dart index 8d65338..6a963f5 100644 --- a/lib/contacts/views/contacts_dialog.dart +++ b/lib/contacts/views/contacts_dialog.dart @@ -75,29 +75,36 @@ class _ContactsDialogState extends State { : null, actions: [ if (_selectedContact != null) - Column(mainAxisSize: MainAxisSize.min, children: [ - IconButton( - icon: const Icon(Icons.chat_bubble), - tooltip: translate('contacts_dialog.new_chat'), - onPressed: () async { - await onChatStarted(_selectedContact!); - }), - Text(translate('contacts_dialog.new_chat'), - style: theme.textTheme.labelSmall! - .copyWith(color: scale.primaryScale.borderText)), - ]).paddingLTRB(8, 0, 8, 0), + FittedBox( + fit: BoxFit.scaleDown, + child: + Column(mainAxisSize: MainAxisSize.min, children: [ + IconButton( + icon: const Icon(Icons.chat_bubble), + tooltip: translate('contacts_dialog.new_chat'), + onPressed: () async { + await onChatStarted(_selectedContact!); + }), + Text(translate('contacts_dialog.new_chat'), + style: theme.textTheme.labelSmall!.copyWith( + color: scale.primaryScale.borderText)), + ])).paddingLTRB(8, 0, 8, 0), if (enableSplit && _selectedContact != null) - Column(mainAxisSize: MainAxisSize.min, children: [ - IconButton( - icon: const Icon(Icons.close), - tooltip: translate('contacts_dialog.close_contact'), - onPressed: () async { - await onContactSelected(null); - }), - Text(translate('contacts_dialog.close_contact'), - style: theme.textTheme.labelSmall! - .copyWith(color: scale.primaryScale.borderText)), - ]).paddingLTRB(8, 0, 8, 0), + FittedBox( + fit: BoxFit.scaleDown, + child: + Column(mainAxisSize: MainAxisSize.min, children: [ + IconButton( + icon: const Icon(Icons.close), + tooltip: + translate('contacts_dialog.close_contact'), + onPressed: () async { + await onContactSelected(null); + }), + Text(translate('contacts_dialog.close_contact'), + style: theme.textTheme.labelSmall!.copyWith( + color: scale.primaryScale.borderText)), + ])).paddingLTRB(8, 0, 8, 0), ]), body: LayoutBuilder(builder: (context, constraint) { final maxWidth = constraint.maxWidth; From 9fbe97c035272ca52765306e5b824c3d8c799020 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 6 Aug 2024 19:00:10 -0700 Subject: [PATCH 190/270] flutter upgrade and fix some ui --- ios/Runner/AppDelegate.swift | 2 +- lib/contacts/views/contacts_dialog.dart | 66 ++++++++++++++----------- macos/Runner/AppDelegate.swift | 2 +- pubspec.lock | 8 +-- 4 files changed, 42 insertions(+), 36 deletions(-) diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 70693e4..b636303 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/lib/contacts/views/contacts_dialog.dart b/lib/contacts/views/contacts_dialog.dart index 6a963f5..e6e5391 100644 --- a/lib/contacts/views/contacts_dialog.dart +++ b/lib/contacts/views/contacts_dialog.dart @@ -46,7 +46,11 @@ class _ContactsDialogState extends State { final theme = Theme.of(context); // final textTheme = theme.textTheme; final scale = theme.extension()!; - // final scaleConfig = theme.extension()!; + final scaleConfig = theme.extension()!; + + final appBarIconColor = scaleConfig.useVisualIndicators + ? scale.secondaryScale.border + : scale.secondaryScale.borderText; final enableSplit = !isMobileWidth(context); final enableLeft = enableSplit || _selectedContact == null; @@ -86,8 +90,8 @@ class _ContactsDialogState extends State { await onChatStarted(_selectedContact!); }), Text(translate('contacts_dialog.new_chat'), - style: theme.textTheme.labelSmall!.copyWith( - color: scale.primaryScale.borderText)), + style: theme.textTheme.labelSmall! + .copyWith(color: appBarIconColor)), ])).paddingLTRB(8, 0, 8, 0), if (enableSplit && _selectedContact != null) FittedBox( @@ -102,38 +106,40 @@ class _ContactsDialogState extends State { await onContactSelected(null); }), Text(translate('contacts_dialog.close_contact'), - style: theme.textTheme.labelSmall!.copyWith( - color: scale.primaryScale.borderText)), + style: theme.textTheme.labelSmall! + .copyWith(color: appBarIconColor)), ])).paddingLTRB(8, 0, 8, 0), ]), body: LayoutBuilder(builder: (context, constraint) { final maxWidth = constraint.maxWidth; - return Row(children: [ - Offstage( - offstage: !enableLeft, - child: SizedBox( - width: enableLeft && !enableRight - ? maxWidth - : (maxWidth / 3).clamp(200, 500), - child: DecoratedBox( - decoration: BoxDecoration( - color: scale.primaryScale.subtleBackground), - child: ContactsBrowser( - selectedContactRecordKey: _selectedContact - ?.localConversationRecordKey - .toVeilid(), - onContactSelected: onContactSelected, - onChatStarted: onChatStarted, - ).paddingLTRB(8, 0, 8, 8)))), - if (enableRight) - if (_selectedContact == null) - const NoContactWidget().expanded() - else - ContactDetailsWidget(contact: _selectedContact!) - .paddingAll(8) - .expanded(), - ]); + return ColoredBox( + color: scale.primaryScale.appBackground, + child: Row(children: [ + Offstage( + offstage: !enableLeft, + child: SizedBox( + width: enableLeft && !enableRight + ? maxWidth + : (maxWidth / 3).clamp(200, 500), + child: DecoratedBox( + decoration: BoxDecoration( + color: scale.primaryScale.subtleBackground), + child: ContactsBrowser( + selectedContactRecordKey: _selectedContact + ?.localConversationRecordKey + .toVeilid(), + onContactSelected: onContactSelected, + onChatStarted: onChatStarted, + ).paddingLTRB(8, 0, 8, 8)))), + if (enableRight) + if (_selectedContact == null) + const NoContactWidget().expanded() + else + ContactDetailsWidget(contact: _selectedContact!) + .paddingAll(8) + .expanded(), + ])); }))); } diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index d53ef64..8e02df2 100644 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true diff --git a/pubspec.lock b/pubspec.lock index 840746a..17cdf9b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -893,18 +893,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: "direct main" description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: transitive description: From c3df5bec8cf1d257c63d773d7bde73f7db5e9633 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 6 Aug 2024 19:54:07 -0700 Subject: [PATCH 191/270] Update CHANGELOG.md for v0.4.3 [ci skip] --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index da4c057..bcc4639 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## v0.4.3 ## +- Flutter upgrade to 3.24.0 +- Contacts UI cleanup +- Incorporate symmetric NAT fix from veilid-core +- Initial public beta release + ## v0.4.2 ## - Dialogs cleanup - Incremental chat state work From 31074732b60e56421bafec9e23f89317b52f0163 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 6 Aug 2024 19:55:57 -0700 Subject: [PATCH 192/270] =?UTF-8?q?Version=20update:=20v0.4.2=20=E2=86=92?= =?UTF-8?q?=20v0.4.3=20[ci=20skip]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index ec9cca0..275e341 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.4.2+0 +current_version = 0.4.3+0 commit = False tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)\+(?P\d+) diff --git a/pubspec.yaml b/pubspec.yaml index 526a3e1..a0f3a51 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: veilidchat description: VeilidChat publish_to: 'none' -version: 0.4.2+15 +version: 0.4.3+16 environment: sdk: '>=3.2.0 <4.0.0' From 574fa499a058463ac4abf8c980eaf125710ab7a3 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 7 Aug 2024 12:03:23 -0700 Subject: [PATCH 193/270] update beta dialog --- assets/i18n/en.json | 2 +- lib/layout/home/home_screen.dart | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/assets/i18n/en.json b/assets/i18n/en.json index aa4d16e..d45a746 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -13,7 +13,7 @@ }, "splash": { "beta_title": "VeilidChat is BETA SOFTWARE", - "beta_text": "DO NOT USE THIS FOR ANYTHING IMPORTANT\n\nUntil 1.0 is released:\n\n• You should have no expectations of actual privacy, or guarantees of security.\n• You will likely lose accounts, contacts, and messages and need to recreate them.\n\nPlease read our BETA PARTICIPATION GUIDE located here:\n\n" + "beta_text": "DO NOT USE THIS FOR ANYTHING IMPORTANT\n\nUntil 1.0 is released:\n\n• You should have no expectations of actual privacy, or guarantees of security.\n• You will likely lose accounts, contacts, and messages and need to recreate them.\n\nTo know what to expect, review our known issues located here:\n\n" }, "account": { "form_name": "Name", diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index a90e110..f226717 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -64,6 +64,7 @@ class HomeScreenState extends State var displayBetaWarning = true; final theme = Theme.of(context); final scale = theme.extension()!; + final scaleConfig = theme.extension()!; await showWarningWidgetModal( context: context, @@ -79,14 +80,16 @@ class HomeScreenState extends State .copyWith(color: scale.primaryScale.appText), ), TextSpan( - text: 'https://veilid.com/chat/beta', + text: 'https://veilid.com/chat/knownissues', style: theme.textTheme.bodyMedium!.copyWith( - color: scale.primaryScale.primary, + color: scaleConfig.useVisualIndicators + ? scale.secondaryScale.primaryText + : scale.secondaryScale.primary, decoration: TextDecoration.underline, ), recognizer: TapGestureRecognizer() - ..onTap = - () => launchUrlString('https://veilid.com/chat/beta'), + ..onTap = () => + launchUrlString('https://veilid.com/chat/knownissues'), ), ], ), From 013b24146e5f6ae75d5c10a723c0844cced8cc51 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 7 Aug 2024 12:05:31 -0700 Subject: [PATCH 194/270] Changelog for v0.4.4 --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcc4639..b6f43c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,11 @@ +## v0.4.4 ## +- Update beta dialog with expectations page +- Temporarily disable relay selection aggressiveness + ## v0.4.3 ## - Flutter upgrade to 3.24.0 - Contacts UI cleanup - Incorporate symmetric NAT fix from veilid-core -- Initial public beta release ## v0.4.2 ## - Dialogs cleanup From dde9d40bf2581b6653664b03d0e4d3f2d9dc71d6 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 7 Aug 2024 12:06:22 -0700 Subject: [PATCH 195/270] =?UTF-8?q?Version=20update:=20v0.4.3=20=E2=86=92?= =?UTF-8?q?=20v0.4.4=20[ci=20skip]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 275e341..9571b62 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.4.3+0 +current_version = 0.4.4+0 commit = False tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)\+(?P\d+) diff --git a/pubspec.yaml b/pubspec.yaml index a0f3a51..c71d310 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: veilidchat description: VeilidChat publish_to: 'none' -version: 0.4.3+16 +version: 0.4.4+17 environment: sdk: '>=3.2.0 <4.0.0' From 71cbfe0d6b4e76c59877ec2094b08817f14e02c0 Mon Sep 17 00:00:00 2001 From: TC Date: Thu, 8 Aug 2024 16:44:48 +0000 Subject: [PATCH 196/270] Added template to new issue creation [ci skip] --- .gitlab/issue_templates/mytemplate.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .gitlab/issue_templates/mytemplate.md diff --git a/.gitlab/issue_templates/mytemplate.md b/.gitlab/issue_templates/mytemplate.md new file mode 100644 index 0000000..759ecc2 --- /dev/null +++ b/.gitlab/issue_templates/mytemplate.md @@ -0,0 +1,21 @@ +First, please search through the existing issues on GitLab ***and*** read our [known issues](https://veilid.com/chat/knownissues) page before opening a new issue. + +Please provide the following information to the best of your ability: + +## Platform in use (Apple or Android) + + +## Network type (Wifi or Cell) +### If you know it, what type of NAT? + + +## Paste in relevant logs +1. Long press the signal meter in VeilidChat to open the console logs +2. Switch the logs to debug +3. Make the issue happen again +4. Go back into the logs and hit the copy all button +5. Paste the logs somewhere you can make edits -- remove all IPs (v4 and v6) +6. Paste or attach that redacted log here + + +## Description of the issue \ No newline at end of file From abf31369a1da8831e351adf17cf3c9d48623fd7e Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 9 Aug 2024 10:54:54 -0700 Subject: [PATCH 197/270] fix stupid colors --- lib/chat/views/empty_chat_widget.dart | 2 +- lib/theme/models/contrast_generator.dart | 3 +++ lib/theme/models/radix_generator.dart | 3 +++ lib/theme/models/scale_scheme.dart | 8 ++++---- lib/theme/models/slider_tile.dart | 4 ++-- lib/theme/models/theme_preference.dart | 2 +- 6 files changed, 14 insertions(+), 8 deletions(-) diff --git a/lib/chat/views/empty_chat_widget.dart b/lib/chat/views/empty_chat_widget.dart index e441a46..946fc3f 100644 --- a/lib/chat/views/empty_chat_widget.dart +++ b/lib/chat/views/empty_chat_widget.dart @@ -17,7 +17,7 @@ class EmptyChatWidget extends StatelessWidget { width: double.infinity, height: double.infinity, decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, + color: scale.primaryScale.appBackground, ), child: Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/theme/models/contrast_generator.dart b/lib/theme/models/contrast_generator.dart index 09cef2b..308b6b3 100644 --- a/lib/theme/models/contrast_generator.dart +++ b/lib/theme/models/contrast_generator.dart @@ -265,6 +265,9 @@ ThemeData contrastGenerator({ final themeData = ThemeData.from( colorScheme: colorScheme, textTheme: textTheme, useMaterial3: true); return themeData.copyWith( + appBarTheme: themeData.appBarTheme.copyWith( + backgroundColor: scaleScheme.primaryScale.border, + foregroundColor: scaleScheme.primaryScale.borderText), bottomSheetTheme: themeData.bottomSheetTheme.copyWith( elevation: 0, modalElevation: 0, diff --git a/lib/theme/models/radix_generator.dart b/lib/theme/models/radix_generator.dart index 92d52c8..c3802e6 100644 --- a/lib/theme/models/radix_generator.dart +++ b/lib/theme/models/radix_generator.dart @@ -636,6 +636,9 @@ ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) { } return scaleScheme.primaryScale.subtleBorder; })), + appBarTheme: themeData.appBarTheme.copyWith( + backgroundColor: scaleScheme.primaryScale.border, + foregroundColor: scaleScheme.primaryScale.borderText), bottomSheetTheme: themeData.bottomSheetTheme.copyWith( elevation: 0, modalElevation: 0, diff --git a/lib/theme/models/scale_scheme.dart b/lib/theme/models/scale_scheme.dart index 512fda6..ac266bc 100644 --- a/lib/theme/models/scale_scheme.dart +++ b/lib/theme/models/scale_scheme.dart @@ -94,10 +94,10 @@ class ScaleScheme extends ThemeExtension { // onErrorContainer: errorScale.subtleText, background: grayScale.appBackground, // reviewed onBackground: grayScale.appText, // reviewed - surface: primaryScale.primary, // reviewed - onSurface: primaryScale.primaryText, // reviewed - surfaceVariant: secondaryScale.primary, - onSurfaceVariant: secondaryScale.primaryText, // ?? reviewed a little + surface: primaryScale.appBackground, // reviewed + onSurface: primaryScale.appText, // reviewed + surfaceVariant: secondaryScale.appBackground, + onSurfaceVariant: secondaryScale.appText, outline: primaryScale.border, outlineVariant: secondaryScale.border, shadow: primaryScale.primary.darken(80), diff --git a/lib/theme/models/slider_tile.dart b/lib/theme/models/slider_tile.dart index e70c6bd..2ce81c9 100644 --- a/lib/theme/models/slider_tile.dart +++ b/lib/theme/models/slider_tile.dart @@ -113,7 +113,7 @@ class SliderTile extends StatelessWidget { ? tileColor.border : tileColor.borderText) : scale.scale(a.actionScale).primaryText, - icon: subtitle.isNotEmpty ? a.icon : null, + icon: subtitle.isEmpty ? a.icon : null, label: a.label, padding: const EdgeInsets.all(2)), ) @@ -136,7 +136,7 @@ class SliderTile extends StatelessWidget { ? tileColor.border : tileColor.borderText) : scale.scale(a.actionScale).primaryText, - icon: subtitle.isNotEmpty ? a.icon : null, + icon: subtitle.isEmpty ? a.icon : null, label: a.label, padding: const EdgeInsets.all(2)), ) diff --git a/lib/theme/models/theme_preference.dart b/lib/theme/models/theme_preference.dart index cfa05c9..ec7a40e 100644 --- a/lib/theme/models/theme_preference.dart +++ b/lib/theme/models/theme_preference.dart @@ -123,7 +123,7 @@ extension ThemePreferencesExt on ThemePreferences { scaleConfig: ScaleConfig( useVisualIndicators: true, preferBorders: true, - borderRadiusScale: 0.5), + borderRadiusScale: 0.2), primaryFront: const Color(0xFF000000), primaryBack: const Color(0xFF00FF00), secondaryFront: const Color(0xFF000000), From 4966349ac9e9e4c6c111bddc8c3470f85e5a8046 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 10 Aug 2024 17:37:12 -0700 Subject: [PATCH 198/270] turn off automatic link preview --- lib/chat/views/chat_component_widget.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/chat/views/chat_component_widget.dart b/lib/chat/views/chat_component_widget.dart index 3349f7f..c4816ae 100644 --- a/lib/chat/views/chat_component_widget.dart +++ b/lib/chat/views/chat_component_widget.dart @@ -224,6 +224,7 @@ class ChatComponentWidget extends StatelessWidget { //onAttachmentPressed: _handleAttachmentPressed, //onMessageTap: _handleMessageTap, //onPreviewDataFetched: _handlePreviewDataFetched, + usePreviewData: false, // onSendPressed: (pt) { try { if (!messageIsValid) { From e8810d208d0cc1c5458fb8413666c8f4715a797f Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 20 Oct 2024 14:57:35 -0400 Subject: [PATCH 199/270] fixes for veilid 0.4.0 --- ios/Podfile.lock | 16 +- .../views/edit_profile_form.dart | 6 +- .../chat_single_contact_item_widget.dart | 9 +- lib/contacts/views/availability_widget.dart | 19 +- lib/contacts/views/edit_contact_form.dart | 8 +- lib/theme/models/scale_scheme.dart | 30 ++ .../repository/processor_repository.dart | 10 +- lib/veilid_processor/views/developer.dart | 2 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 +- macos/Podfile.lock | 18 +- .../example/macos/Runner/AppDelegate.swift | 2 +- packages/veilid_support/example/pubspec.lock | 169 +++++----- .../lib/dht_support/src/dht_log/dht_log.dart | 9 +- .../src/dht_log/dht_log_spine.dart | 9 +- packages/veilid_support/pubspec.lock | 197 ++++++------ pubspec.lock | 289 ++++++++++-------- 16 files changed, 454 insertions(+), 341 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 536f7a4..81058ea 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -50,7 +50,7 @@ PODS: - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) - MLImage (= 1.0.0-beta5) - MLKitCommon (~> 11.0) - - mobile_scanner (5.1.1): + - mobile_scanner (5.2.3): - Flutter - GoogleMLKit/BarcodeScanning (~> 6.0.0) - nanopb (2.30910.0): @@ -77,7 +77,7 @@ PODS: - FlutterMacOS - smart_auth (0.0.1): - Flutter - - sqflite (0.0.3): + - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS - system_info_plus (0.0.1): @@ -101,7 +101,7 @@ 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/darwin`) + - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/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`) @@ -148,8 +148,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" smart_auth: :path: ".symlinks/plugins/smart_auth/ios" - sqflite: - :path: ".symlinks/plugins/sqflite/darwin" + sqflite_darwin: + :path: ".symlinks/plugins/sqflite_darwin/darwin" system_info_plus: :path: ".symlinks/plugins/system_info_plus/ios" url_launcher_ios: @@ -172,10 +172,10 @@ SPEC CHECKSUMS: MLKitBarcodeScanning: 10ca0845a6d15f2f6e911f682a1998b68b973e8b MLKitCommon: afec63980417d29ffbb4790529a1b0a2291699e1 MLKitVision: e858c5f125ecc288e4a31127928301eaba9ae0c1 - mobile_scanner: 8564358885a9253c43f822435b70f9345c87224f + mobile_scanner: 96e91f2e1fb396bb7df8da40429ba8dfad664740 nanopb: 438bc412db1928dac798aa6fd75726007be04262 native_device_orientation: 348b10c346a60ebbc62fb235a4fdb5d1b61a8f55 - package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c + package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 printing: 233e1b73bd1f4a05615548e9b5a324c98588640b @@ -183,7 +183,7 @@ SPEC CHECKSUMS: share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 smart_auth: 4bedbc118723912d0e45a07e8ab34039c19e04f2 - sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13 system_info_plus: 5393c8da281d899950d751713575fbf91c7709aa url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe veilid: f5c2e662f91907b30cf95762619526ac3e4512fd diff --git a/lib/account_manager/views/edit_profile_form.dart b/lib/account_manager/views/edit_profile_form.dart index 54e61d1..1774bff 100644 --- a/lib/account_manager/views/edit_profile_form.dart +++ b/lib/account_manager/views/edit_profile_form.dart @@ -79,6 +79,9 @@ class _EditProfileFormState extends State { FormBuilderDropdown _availabilityDropDown( BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final initialValueX = widget.initialValueCallback(EditProfileForm.formFieldAvailability) as proto.Availability; @@ -105,7 +108,8 @@ class _EditProfileFormState extends State { .map((x) => DropdownMenuItem( value: x, child: Row(mainAxisSize: MainAxisSize.min, children: [ - AvailabilityWidget.availabilityIcon(x), + AvailabilityWidget.availabilityIcon( + x, scale.primaryScale.primaryText), Text(x == proto.Availability.AVAILABILITY_OFFLINE ? translate('availability.always_show_offline') : AvailabilityWidget.availabilityName(x)) 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 4ac992f..d3db153 100644 --- a/lib/chat_list/views/chat_single_contact_item_widget.dart +++ b/lib/chat_list/views/chat_single_contact_item_widget.dart @@ -67,7 +67,12 @@ class ChatSingleContactItemWidget extends StatelessWidget { title: title, subtitle: subtitle, leading: avatar, - trailing: AvailabilityWidget(availability: availability), + trailing: AvailabilityWidget( + availability: availability, + color: _disabled + ? scale.grayScale.primaryText + : scale.secondaryScale.primaryText, + ), onTap: () { singleFuture(activeChatCubit, () async { activeChatCubit.setActiveChat(_localConversationRecordKey); @@ -75,7 +80,7 @@ class ChatSingleContactItemWidget extends StatelessWidget { }, endActions: [ SliderTileAction( - icon: Icons.delete, + //icon: Icons.delete, label: translate('button.delete'), actionScale: ScaleKind.tertiary, onPressed: (context) async { diff --git a/lib/contacts/views/availability_widget.dart b/lib/contacts/views/availability_widget.dart index d50e323..cf3e51a 100644 --- a/lib/contacts/views/availability_widget.dart +++ b/lib/contacts/views/availability_widget.dart @@ -8,17 +8,18 @@ import '../../proto/proto.dart' as proto; class AvailabilityWidget extends StatelessWidget { const AvailabilityWidget( {required this.availability, + required this.color, this.vertical = true, this.iconSize = 32, super.key}); - static Widget availabilityIcon(proto.Availability availability, + static Widget availabilityIcon(proto.Availability availability, Color color, {double size = 32}) { late final Widget iconData; switch (availability) { case proto.Availability.AVAILABILITY_AWAY: - iconData = - ImageIcon(const AssetImage('assets/images/toilet.png'), size: size); + iconData = ImageIcon(const AssetImage('assets/images/toilet.png'), + size: size, color: color); case proto.Availability.AVAILABILITY_BUSY: iconData = Icon(Icons.event_busy, size: size); case proto.Availability.AVAILABILITY_FREE: @@ -56,7 +57,7 @@ class AvailabilityWidget extends StatelessWidget { // final scaleConfig = theme.extension()!; final name = availabilityName(availability); - final icon = availabilityIcon(availability, size: iconSize); + final icon = availabilityIcon(availability, color, size: iconSize); return vertical ? Column( @@ -64,17 +65,20 @@ class AvailabilityWidget extends StatelessWidget { //mainAxisAlignment: MainAxisAlignment.center, children: [ icon, - Text(name, style: textTheme.labelSmall).paddingLTRB(0, 0, 0, 0) + Text(name, style: textTheme.labelSmall!.copyWith(color: color)) + .paddingLTRB(0, 0, 0, 0) ]) : Row(mainAxisSize: MainAxisSize.min, children: [ icon, - Text(name, style: textTheme.labelSmall).paddingLTRB(8, 0, 0, 0) + Text(name, style: textTheme.labelSmall!.copyWith(color: color)) + .paddingLTRB(8, 0, 0, 0) ]); } //////////////////////////////////////////////////////////////////////////// final proto.Availability availability; + final Color color; final bool vertical; final double iconSize; @@ -85,6 +89,7 @@ class AvailabilityWidget extends StatelessWidget { ..add( DiagnosticsProperty('availability', availability)) ..add(DiagnosticsProperty('vertical', vertical)) - ..add(DoubleProperty('iconSize', iconSize)); + ..add(DoubleProperty('iconSize', iconSize)) + ..add(ColorProperty('color', color)); } } diff --git a/lib/contacts/views/edit_contact_form.dart b/lib/contacts/views/edit_contact_form.dart index e515f14..7803ab2 100644 --- a/lib/contacts/views/edit_contact_form.dart +++ b/lib/contacts/views/edit_contact_form.dart @@ -46,9 +46,8 @@ class _EditContactFormState extends State { super.initState(); } - Widget _availabilityWidget( - BuildContext context, proto.Availability availability) => - AvailabilityWidget(availability: availability); + Widget _availabilityWidget(proto.Availability availability, Color color) => + AvailabilityWidget(availability: availability, color: color); @override Widget build(BuildContext context) { @@ -95,7 +94,8 @@ class _EditContactFormState extends State { ) .paddingSymmetric(vertical: 4), Row(mainAxisSize: MainAxisSize.min, children: [ - _availabilityWidget(context, widget.contact.profile.availability), + _availabilityWidget(widget.contact.profile.availability, + scale.primaryScale.primaryText), SelectableText(widget.contact.profile.status, style: textTheme.bodyMedium) .paddingSymmetric(horizontal: 8) diff --git a/lib/theme/models/scale_scheme.dart b/lib/theme/models/scale_scheme.dart index ac266bc..6dbc248 100644 --- a/lib/theme/models/scale_scheme.dart +++ b/lib/theme/models/scale_scheme.dart @@ -145,3 +145,33 @@ class ScaleConfig extends ThemeExtension { lerpDouble(borderRadiusScale, other.borderRadiusScale, t) ?? 1); } } + +class ScaleTheme extends ThemeExtension { + ScaleTheme({ + required this.scheme, + required this.config, + }); + + final ScaleScheme scheme; + final ScaleConfig config; + + @override + ScaleTheme copyWith({ + ScaleScheme? scheme, + ScaleConfig? config, + }) => + ScaleTheme( + scheme: scheme ?? this.scheme, + config: config ?? this.config, + ); + + @override + ScaleTheme lerp(ScaleTheme? other, double t) { + if (other is! ScaleTheme) { + return this; + } + return ScaleTheme( + scheme: scheme.lerp(other.scheme, t), + config: config.lerp(other.config, t)); + } +} diff --git a/lib/veilid_processor/repository/processor_repository.dart b/lib/veilid_processor/repository/processor_repository.dart index e021648..3622219 100644 --- a/lib/veilid_processor/repository/processor_repository.dart +++ b/lib/veilid_processor/repository/processor_repository.dart @@ -12,10 +12,12 @@ class ProcessorRepository { : startedUp = false, _controllerConnectionState = StreamController.broadcast(sync: true), processorConnectionState = ProcessorConnectionState( - attachment: const VeilidStateAttachment( + attachment: VeilidStateAttachment( state: AttachmentState.detached, publicInternetReady: false, - localNetworkReady: false), + localNetworkReady: false, + uptime: TimestampDuration(value: BigInt.zero), + attachedUptime: null), network: VeilidStateNetwork( started: false, bpsDown: BigInt.zero, @@ -96,7 +98,9 @@ class ProcessorRepository { attachment: VeilidStateAttachment( state: updateAttachment.state, publicInternetReady: updateAttachment.publicInternetReady, - localNetworkReady: updateAttachment.localNetworkReady)); + localNetworkReady: updateAttachment.localNetworkReady, + uptime: updateAttachment.uptime, + attachedUptime: updateAttachment.attachedUptime)); } void processUpdateConfig(VeilidUpdateConfig updateConfig) { diff --git a/lib/veilid_processor/views/developer.dart b/lib/veilid_processor/views/developer.dart index 549d228..e40677d 100644 --- a/lib/veilid_processor/views/developer.dart +++ b/lib/veilid_processor/views/developer.dart @@ -366,7 +366,7 @@ class _DeveloperPageState extends State { final _terminalController = TerminalController(); late final HistoryTextEditingController _historyController; - final _logLevelController = DropdownController(duration: 250.ms); + final _logLevelController = DropdownController(duration: 250.ms); final List> _logLevelDropdownItems = []; var _logLevelDropDown = log.level.logLevel; var _showEllet = false; diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index c311845..238bf3e 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -15,7 +15,7 @@ import screen_retriever import share_plus import shared_preferences_foundation import smart_auth -import sqflite +import sqflite_darwin import url_launcher_macos import veilid import window_manager diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 3a69524..9078901 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -2,7 +2,7 @@ PODS: - file_saver (0.0.1): - FlutterMacOS - FlutterMacOS (1.0.0) - - mobile_scanner (5.1.1): + - mobile_scanner (5.2.3): - FlutterMacOS - package_info_plus (0.0.1): - FlutterMacOS @@ -22,7 +22,7 @@ PODS: - FlutterMacOS - smart_auth (0.0.1): - FlutterMacOS - - sqflite (0.0.3): + - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS - url_launcher_macos (0.0.1): @@ -44,7 +44,7 @@ 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/darwin`) + - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/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`) @@ -72,8 +72,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin smart_auth: :path: Flutter/ephemeral/.symlinks/plugins/smart_auth/macos - sqflite: - :path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin + sqflite_darwin: + :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos veilid: @@ -84,8 +84,8 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: file_saver: 44e6fbf666677faf097302460e214e977fdd977b FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - mobile_scanner: 1efac1e53c294b24e3bb55bcc7f4deee0233a86b - package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c + mobile_scanner: 0a05256215b047af27b9495db3b77640055e8824 + package_info_plus: f5790acc797bf17c3e959e9d6cf162cc68ff7523 pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 printing: 1dd6a1fce2209ec240698e2439a4adbb9b427637 @@ -93,8 +93,8 @@ SPEC CHECKSUMS: share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 smart_auth: b38e3ab4bfe089eacb1e233aca1a2340f96c28e9 - sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec - url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 + sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13 + url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 veilid: a54f57b7bcf0e4e072fe99272d76ca126b2026d0 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 diff --git a/packages/veilid_support/example/macos/Runner/AppDelegate.swift b/packages/veilid_support/example/macos/Runner/AppDelegate.swift index d53ef64..8e02df2 100644 --- a/packages/veilid_support/example/macos/Runner/AppDelegate.swift +++ b/packages/veilid_support/example/macos/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true diff --git a/packages/veilid_support/example/pubspec.lock b/packages/veilid_support/example/pubspec.lock index 236861d..91b7eee 100644 --- a/packages/veilid_support/example/pubspec.lock +++ b/packages/veilid_support/example/pubspec.lock @@ -5,26 +5,31 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 url: "https://pub.dev" source: hosted - version: "67.0.0" + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" analyzer: dependency: transitive description: name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.7.0" args: dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.0" async: dependency: transitive description: @@ -37,10 +42,10 @@ packages: dependency: "direct dev" description: name: async_tools - sha256: "93df8b92d54d92e3323c630277e902b4ad4f05f798b55cfbc451e98c3e2fb7ba" + sha256: bbded696bfcb1437d0ca510ac047f261f9c7494fea2c488dd32ba2800e7f49e8 url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "0.1.7" bloc: dependency: transitive description: @@ -53,10 +58,10 @@ packages: dependency: transitive description: name: bloc_advanced_tools - sha256: f0b2dbe028792c97d1eb30480ed4e8035b5c70ea3bcc95a9c5255142592857f7 + sha256: d8a680d8a0469456399fb26bae9f7a1d2a1420b5bdf75e204e0fadab9edb0811 url: "https://pub.dev" source: hosted - version: "0.1.7" + version: "0.1.8" boolean_selector: dependency: transitive description: @@ -69,10 +74,10 @@ packages: dependency: transitive description: name: change_case - sha256: "47c48c36f95f20c6d0ba03efabceff261d05026cca322cc2c4c01c343371b5bb" + sha256: "99cfdf2018c627c8a3af5a23ea4c414eb69c75c31322d23b9660ebc3cf30b514" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" characters: dependency: transitive description: @@ -109,26 +114,26 @@ packages: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" coverage: dependency: transitive description: name: coverage - sha256: "3945034e86ea203af7a056d98e98e42a5518fff200d6e8e6647e1886b07e936e" + sha256: "88b0fddbe4c92910fefc09cc0248f5e7f0cd23e450ded4c28f16ab8ee8f83268" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.10.0" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.6" cupertino_icons: dependency: "direct main" description: @@ -157,18 +162,18 @@ packages: dependency: transitive description: name: fast_immutable_collections - sha256: "533806a7f0c624c2e479d05d3fdce4c87109a7cd0db39b8cc3830d3a2e8dedc7" + sha256: c3c73f4f989d3302066e4ec94e6ec73b5dc872592d02194f49f1352d64126b8c url: "https://pub.dev" source: hosted - version: "10.2.3" + version: "10.2.4" ffi: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" file: dependency: transitive description: @@ -181,10 +186,10 @@ packages: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -209,10 +214,10 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.4" frontend_server_client: dependency: transitive description: @@ -291,18 +296,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -323,10 +328,10 @@ packages: dependency: transitive description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" loggy: dependency: transitive description: @@ -335,6 +340,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" matcher: dependency: transitive description: @@ -347,26 +360,26 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "2.0.0" node_preamble: dependency: transitive description: @@ -395,18 +408,18 @@ packages: dependency: transitive description: name: path_provider - sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.2.12" path_provider_foundation: dependency: transitive description: @@ -435,18 +448,18 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" platform: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -507,18 +520,18 @@ packages: dependency: transitive description: name: shelf_static - sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -528,10 +541,10 @@ packages: dependency: transitive description: name: source_map_stack_trace - sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" source_maps: dependency: transitive description: @@ -608,34 +621,34 @@ packages: dependency: "direct dev" description: name: test - sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" + sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e" url: "https://pub.dev" source: hosted - version: "1.25.2" + version: "1.25.7" test_api: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" test_core: dependency: transitive description: name: test_core - sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" + sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.4" typed_data: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" vector_math: dependency: transitive description: @@ -669,10 +682,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.5" watcher: dependency: transitive description: @@ -685,18 +698,26 @@ packages: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "1.1.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "3.0.1" webdriver: dependency: transitive description: @@ -713,22 +734,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" - win32: - dependency: transitive - description: - name: win32 - sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 - url: "https://pub.dev" - source: hosted - version: "5.5.1" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" yaml: dependency: transitive description: @@ -738,5 +751,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.19.1" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart index 8fb1999..bba3306 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart @@ -293,11 +293,12 @@ class DHTLog implements DHTDeleteable { //////////////////////////////////////////////////////////////// // Fields - // 56 subkeys * 512 segments * 36 bytes per typedkey = - // 1032192 bytes per record + // 55 subkeys * 512 segments * 36 bytes per typedkey = + // 1013760 bytes per record + // Leaves 34816 bytes for 0th subkey as head, 56 subkeys total // 512*36 = 18432 bytes per subkey - // 28672 shortarrays * 256 elements = 7340032 elements - static const spineSubkeys = 56; + // 28160 shortarrays * 256 elements = 7208960 elements + static const spineSubkeys = 55; static const segmentsPerSubkey = 512; // Internal representation refreshed from spine record diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index 7d8c519..1ea48be 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -147,7 +147,7 @@ class _DHTLogSpine { if (!await writeSpineHead(old: (oldHead, oldTail))) { // Failed to write head means head got overwritten so write should // be considered failed - throw DHTExceptionOutdated(); + throw const DHTExceptionOutdated(); } return out; } on Exception { @@ -257,7 +257,7 @@ class _DHTLogSpine { final headDelta = _ringDistance(newHead, oldHead); final tailDelta = _ringDistance(newTail, oldTail); if (headDelta > _positionLimit ~/ 2 || tailDelta > _positionLimit ~/ 2) { - throw DHTExceptionInvalidData(); + throw const DHTExceptionInvalidData(); } } @@ -615,8 +615,9 @@ class _DHTLogSpine { await _spineRecord.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 + // xxx: check if this localChanges can be false... + // xxx: Don't watch for local changes because this class already handles + // xxx: notifying listeners and knows when it makes local changes _subscription ??= await _spineRecord.listen(localChanges: true, _onSpineChanged); } on Exception { diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index 20798e2..4262a13 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -5,34 +5,39 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 url: "https://pub.dev" source: hosted - version: "67.0.0" + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" analyzer: dependency: transitive description: name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.7.0" args: dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.0" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.12.0" async_tools: dependency: "direct main" description: @@ -101,18 +106,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "1414d6d733a85d8ad2f1dfcb3ea7945759e35a123cb99ccfac75d0758f75edfa" + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" url: "https://pub.dev" source: hosted - version: "2.4.10" + version: "2.4.13" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.3.2" built_collection: dependency: transitive description: @@ -133,10 +138,10 @@ packages: dependency: transitive description: name: change_case - sha256: "47c48c36f95f20c6d0ba03efabceff261d05026cca322cc2c4c01c343371b5bb" + sha256: "99cfdf2018c627c8a3af5a23ea4c414eb69c75c31322d23b9660ebc3cf30b514" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" characters: dependency: transitive description: @@ -181,34 +186,34 @@ packages: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" coverage: dependency: transitive description: name: coverage - sha256: "3945034e86ea203af7a056d98e98e42a5518fff200d6e8e6647e1886b07e936e" + sha256: "88b0fddbe4c92910fefc09cc0248f5e7f0cd23e450ded4c28f16ab8ee8f83268" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.10.0" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.6" dart_style: dependency: transitive description: name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.3.7" equatable: dependency: "direct main" description: @@ -221,34 +226,34 @@ packages: dependency: "direct main" description: name: fast_immutable_collections - sha256: "533806a7f0c624c2e479d05d3fdce4c87109a7cd0db39b8cc3830d3a2e8dedc7" + sha256: c3c73f4f989d3302066e4ec94e6ec73b5dc872592d02194f49f1352d64126b8c url: "https://pub.dev" source: hosted - version: "10.2.3" + version: "10.2.4" ffi: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: transitive description: flutter @@ -263,18 +268,18 @@ packages: dependency: "direct dev" description: name: freezed - sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1 + sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.7" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.4" frontend_server_client: dependency: transitive description: @@ -303,10 +308,10 @@ packages: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" http_multi_server: dependency: transitive description: @@ -367,10 +372,10 @@ packages: dependency: transitive description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" loggy: dependency: "direct main" description: @@ -379,6 +384,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" matcher: dependency: transitive description: @@ -391,26 +404,26 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: "direct main" description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "2.0.0" node_preamble: dependency: transitive description: @@ -431,26 +444,26 @@ packages: dependency: "direct main" description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_provider: dependency: "direct main" description: name: path_provider - sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.2.12" path_provider_foundation: dependency: transitive description: @@ -479,18 +492,18 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" platform: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -527,10 +540,10 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.3.0" shelf: dependency: transitive description: @@ -551,18 +564,18 @@ packages: dependency: transitive description: name: shelf_static - sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -588,10 +601,10 @@ packages: dependency: transitive description: name: source_map_stack_trace - sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" source_maps: dependency: transitive description: @@ -612,10 +625,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -636,10 +649,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "0bd04f5bb74fcd6ff0606a888a30e917af9bd52820b178eaa464beb11dca84b6" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.0" system_info2: dependency: transitive description: @@ -668,26 +681,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" + sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" url: "https://pub.dev" source: hosted - version: "1.25.2" + version: "1.25.8" test_api: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.3" test_core: dependency: transitive description: name: test_core - sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" + sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.5" timing: dependency: transitive description: @@ -700,10 +713,10 @@ packages: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" vector_math: dependency: transitive description: @@ -723,10 +736,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "7475cb4dd713d57b6f7464c0e13f06da0d535d8b2067e188962a59bac2cf280b" + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.2" + version: "14.3.0" watcher: dependency: transitive description: @@ -739,18 +752,26 @@ packages: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "1.1.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "3.0.1" webkit_inspection_protocol: dependency: transitive description: @@ -759,22 +780,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" - win32: - dependency: transitive - description: - name: win32 - sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 - url: "https://pub.dev" - source: hosted - version: "5.5.1" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" yaml: dependency: transitive description: @@ -784,5 +797,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.19.1" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/pubspec.lock b/pubspec.lock index 17cdf9b..4a839f3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,15 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 url: "https://pub.dev" source: hosted - version: "67.0.0" + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" accordion: dependency: "direct main" description: @@ -21,10 +26,10 @@ packages: dependency: transitive description: name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.7.0" animated_bottom_navigation_bar: dependency: "direct main" description: @@ -53,10 +58,10 @@ packages: dependency: "direct main" description: name: ansicolor - sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880" + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" archive: dependency: "direct main" description: @@ -69,18 +74,18 @@ packages: dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.0" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.12.0" async_tools: dependency: "direct main" description: @@ -125,10 +130,10 @@ packages: dependency: transitive description: name: bidi - sha256: "1a7d0c696324b2089f72e7671fd1f1f64fef44c980f3cebc84e803967c597b63" + sha256: "9a712c7ddf708f7c41b1923aa83648a3ed44cfd75b04f72d598c45e5be287f9d" url: "https://pub.dev" source: hosted - version: "2.0.10" + version: "2.0.12" bloc: dependency: "direct main" description: @@ -197,18 +202,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7" + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" url: "https://pub.dev" source: hosted - version: "2.4.11" + version: "2.4.13" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 url: "https://pub.dev" source: hosted - version: "7.3.1" + version: "7.3.2" built_collection: dependency: transitive description: @@ -237,10 +242,10 @@ packages: dependency: transitive description: name: cached_network_image_platform_interface - sha256: ff0c949e323d2a1b52be73acce5b4a7b04063e61414c8ca542dbba47281630a7 + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.1.1" cached_network_image_web: dependency: transitive description: @@ -261,18 +266,18 @@ packages: dependency: transitive description: name: camera_android - sha256: "134b83167cc3c83199e8d75e5bcfde677fec843e7b2ca6b754a5b0b96d00d921" + sha256: "387c9861dc026b980e5ff16c949c5bcfecafa8825b8b74e41b24405eb9d39af6" url: "https://pub.dev" source: hosted - version: "0.10.9+10" + version: "0.10.9+14" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: b5093a82537b64bb88d4244f8e00b5ba69e822a5994f47b31d11400e1db975e5 + sha256: "0d04cec8715b59fb6dc60eefb47e69024f51233c570e475b886dc9290568bca7" url: "https://pub.dev" source: hosted - version: "0.9.17+1" + version: "0.9.17+4" camera_platform_interface: dependency: transitive description: @@ -285,10 +290,10 @@ packages: dependency: transitive description: name: camera_web - sha256: b9235ec0a2ce949daec546f1f3d86f05c3921ed31c7d9ab6b7c03214d152fc2d + sha256: "595f28c89d1fb62d77c73c633193755b781c6d2e0ebcd8dc25b763b514e6ba8f" url: "https://pub.dev" source: hosted - version: "0.3.4" + version: "0.3.5" change_case: dependency: "direct main" description: @@ -373,34 +378,34 @@ packages: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" cool_dropdown: dependency: "direct main" description: name: cool_dropdown - sha256: "24400f57740b4779407586121e014bef241699ad2a52c506a7e1e7616cb68653" + sha256: "23926fd242b625bcb7ab30c1336498d60f78267518db439141ca19de403ab030" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" cross_file: dependency: transitive description: name: cross_file - sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" url: "https://pub.dev" source: hosted - version: "0.3.4+1" + version: "0.3.4+2" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.6" csslib: dependency: transitive description: @@ -421,10 +426,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.3.7" diffutil_dart: dependency: transitive description: @@ -437,18 +442,18 @@ packages: dependency: transitive description: name: dio - sha256: e17f6b3097b8c51b72c74c9f071a605c47bcc8893839bd66732457a5ebe73714 + sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" url: "https://pub.dev" source: hosted - version: "5.5.0+1" + version: "5.7.0" dio_web_adapter: dependency: transitive description: name: dio_web_adapter - sha256: "36c5b2d79eb17cdae41e974b7a8284fec631651d2a6f39a8a2ff22327e90aeac" + sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "2.0.0" equatable: dependency: "direct main" description: @@ -477,18 +482,18 @@ packages: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" file_saver: dependency: "direct main" description: @@ -501,10 +506,10 @@ packages: dependency: "direct main" description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -530,10 +535,10 @@ packages: dependency: transitive description: name: flutter_cache_manager - sha256: a77f77806a790eb9ba0118a5a3a936e81c4fea2b61533033b2b0c3d50bbde5ea + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "3.4.1" flutter_chat_types: dependency: "direct main" description: @@ -555,10 +560,10 @@ packages: dependency: "direct main" description: name: flutter_form_builder - sha256: "447f8808f68070f7df968e8063aada3c9d2e90e789b5b70f3b44e4b315212656" + sha256: c278ef69b08957d484f83413f0e77b656a39b7a7bb4eb8a295da3a820ecc6545 url: "https://pub.dev" source: hosted - version: "9.3.0" + version: "9.5.0" flutter_hooks: dependency: "direct main" description: @@ -608,18 +613,18 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "9d98bd47ef9d34e803d438f17fd32b116d31009f534a6fa5ce3a1167f189a6de" + sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398" url: "https://pub.dev" source: hosted - version: "2.0.21" + version: "2.0.23" flutter_shaders: dependency: transitive description: name: flutter_shaders - sha256: "02750b545c01ff4d8e9bbe8f27a7731aa3778402506c67daa1de7f5fc3f4befe" + sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" url: "https://pub.dev" source: hosted - version: "0.1.2" + version: "0.1.3" flutter_slidable: dependency: "direct main" description: @@ -685,10 +690,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1 + sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.7" freezed_annotation: dependency: "direct main" description: @@ -733,10 +738,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "39dd52168d6c59984454183148dc3a5776960c61083adfc708cc79a7b3ce1ba8" + sha256: "6f1b756f6e863259a99135ff3c95026c3cdca17d10ebef2bba2261a25ddc8bbc" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.3.0" graphs: dependency: transitive description: @@ -805,10 +810,10 @@ packages: dependency: "direct main" description: name: image - sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" + sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.3.0" intl: dependency: "direct main" description: @@ -869,10 +874,10 @@ packages: dependency: transitive description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" loggy: dependency: "direct main" description: @@ -881,14 +886,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -909,26 +922,26 @@ packages: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.6" mobile_scanner: dependency: "direct main" description: name: mobile_scanner - sha256: b8c0e9afcfd52534f85ec666f3d52156f560b5e6c25b1e3d4fe2087763607926 + sha256: d234581c090526676fd8fab4ada92f35c6746e3fb4f05a399665d75a399fb760 url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "5.2.3" motion_toast: dependency: "direct main" description: name: motion_toast - sha256: "8dc8af93c606d0a08f2592591164f4a761099c5470e589f25689de6c601f124e" + sha256: "5a4775bf5a89a2402456047f194df5a5d6ac717af0d7694c8b9e37f324d1efd7" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.11.0" native_device_orientation: dependency: "direct main" description: @@ -965,10 +978,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "4de6c36df77ffbcef0a5aefe04669d33f2d18397fea228277b852a2d4e58e860" + sha256: df3eb3e0aed5c1107bb0fdb80a8e82e778114958b1c5ac5644fb1ac9cae8a998 url: "https://pub.dev" source: hosted - version: "8.0.1" + version: "8.1.0" package_info_plus_platform_interface: dependency: transitive description: @@ -1013,10 +1026,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "490539678396d4c3c0b06efdaab75ae60675c3e0c66f72bc04c2e2c1e0e2abeb" + sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a url: "https://pub.dev" source: hosted - version: "2.2.9" + version: "2.2.12" path_provider_foundation: dependency: transitive description: @@ -1053,10 +1066,10 @@ packages: dependency: "direct main" description: name: pdf - sha256: "81d5522bddc1ef5c28e8f0ee40b71708761753c163e0c93a40df56fd515ea0f0" + sha256: "05df53f8791587402493ac97b9869d3824eccbc77d97855f4545cf72df3cae07" url: "https://pub.dev" source: hosted - version: "3.11.0" + version: "3.11.1" pdf_widget_wrapper: dependency: transitive description: @@ -1093,10 +1106,10 @@ packages: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -1181,10 +1194,10 @@ packages: dependency: "direct main" description: name: qr_code_dart_scan - sha256: "52912da40f5e40a197b890108af9d2a6baa0c5812b77bfb085c8ee9e3c4f1f52" + sha256: d5511d137f1ca5cb217fe79fa992616e0361a505a74b1e34499e68040a68b0c3 url: "https://pub.dev" source: hosted - version: "0.8.1" + version: "0.8.3" qr_flutter: dependency: "direct main" description: @@ -1197,10 +1210,10 @@ packages: dependency: transitive description: name: quiver - sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" radix_colors: dependency: "direct main" description: @@ -1286,34 +1299,34 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: c272f9cabca5a81adc9b0894381e9c1def363e980f960fa903c604c471b22f68 + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "041be4d9d2dc6079cf342bc8b761b03787e3b71192d658220a56cac9c04a0294" + sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.3" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "671e7a931f55a08aa45be2a13fe7247f2a41237897df434b30d2012388191833" + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.5.3" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "2ba0510d3017f91655b7543e9ee46d48619de2a2af38e5c790423f7007c7ccc1" + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: @@ -1326,18 +1339,18 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: "3a293170d4d9403c3254ee05b84e62e8a9b3c5808ebd17de6a33fe9ea6457936" + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "398084b47b7f92110683cac45c6dc4aae853db47e470e5ddcd52cab7f7196ab2" + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" shelf: dependency: transitive description: @@ -1452,26 +1465,50 @@ packages: dependency: transitive description: name: sqflite - sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d + sha256: "79a297dc3cc137e758c6a4baf83342b039e5a6d2436fcdf3f96a00adaaf2ad62" url: "https://pub.dev" source: hosted - version: "2.3.3+1" + version: "2.4.0" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3" + url: "https://pub.dev" + source: hosted + version: "2.4.0" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" + sha256: "4468b24876d673418a7b7147e5a08a715b4998a7ae69227acafaab762e0e5490" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.5.4+5" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "769733dddf94622d5541c73e4ddc6aa7b252d865285914b6fcd54a63c4b4f027" + url: "https://pub.dev" + source: hosted + version: "2.4.1-1" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" stack_trace: dependency: "direct main" description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" star_menu: dependency: "direct main" description: @@ -1500,18 +1537,18 @@ packages: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "0bd04f5bb74fcd6ff0606a888a30e917af9bd52820b178eaa464beb11dca84b6" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" synchronized: dependency: transitive description: name: synchronized - sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" url: "https://pub.dev" source: hosted - version: "3.1.0+1" + version: "3.3.0+3" system_info2: dependency: transitive description: @@ -1540,10 +1577,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.3" timing: dependency: transitive description: @@ -1564,10 +1601,10 @@ packages: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" universal_io: dependency: transitive description: @@ -1588,18 +1625,18 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "94d8ad05f44c6d4e2ffe5567ab4d741b82d62e3c8e288cc1fcea45965edf47c9" + sha256: "8fc3bae0b68c02c47c5c86fa8bfa74471d42687b0eded01b78de87872db745e2" url: "https://pub.dev" source: hosted - version: "6.3.8" + version: "6.3.12" url_launcher_ios: dependency: transitive description: @@ -1612,18 +1649,18 @@ packages: dependency: transitive description: name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.2.0" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.1" url_launcher_platform_interface: dependency: transitive description: @@ -1636,26 +1673,26 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" + sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" uuid: dependency: "direct main" description: name: uuid - sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "4.4.2" + version: "4.5.1" value_layout_builder: dependency: transitive description: @@ -1754,10 +1791,10 @@ packages: dependency: transitive description: name: win32 - sha256: "015002c060f1ae9f41a818f2d5640389cc05283e368be19dc8d77cecb43c40c9" + sha256: "2735daae5150e8b1dfeb3eb0544b4d3af0061e9e82cef063adcd583bdae4306a" url: "https://pub.dev" source: hosted - version: "5.5.3" + version: "5.7.0" window_manager: dependency: "direct main" description: @@ -1770,10 +1807,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" xml: dependency: transitive description: @@ -1823,5 +1860,5 @@ packages: source: hosted version: "1.1.2" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.22.1" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" From e4c0162e3d3d1ca6ddc470c00bf276dca70e8fac Mon Sep 17 00:00:00 2001 From: TC Johnson Date: Mon, 4 Nov 2024 10:44:45 -0600 Subject: [PATCH 200/270] Updated changelog for v0.4.5 release --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6f43c9..f7e369f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## v0.4.5 ## +- Updated veilid-core to v0.4.1 + - See Veilid changelog for specifics +- DHT speed and reliability improvements + ## v0.4.4 ## - Update beta dialog with expectations page - Temporarily disable relay selection aggressiveness From 57504c3625fe8bf1aa69b926759d75298f0ecf55 Mon Sep 17 00:00:00 2001 From: TC Johnson Date: Mon, 4 Nov 2024 10:45:58 -0600 Subject: [PATCH 201/270] =?UTF-8?q?Version=20update:=20v0.4.4=20=E2=86=92?= =?UTF-8?q?=20v0.4.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 9571b62..f122962 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.4.4+0 +current_version = 0.4.5+0 commit = False tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)\+(?P\d+) diff --git a/pubspec.yaml b/pubspec.yaml index c71d310..8b171f3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: veilidchat description: VeilidChat publish_to: 'none' -version: 0.4.4+17 +version: 0.4.5+18 environment: sdk: '>=3.2.0 <4.0.0' From ad99f703ff6340404a0e0d670b97f6721946fcb0 Mon Sep 17 00:00:00 2001 From: TC Johnson Date: Mon, 4 Nov 2024 11:38:25 -0600 Subject: [PATCH 202/270] =?UTF-8?q?Version=20update:=20v0.4.4=20=E2=86=92?= =?UTF-8?q?=20v0.4.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/Podfile.lock | 12 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 +- pubspec.lock | 123 ++++++------------ 3 files changed, 50 insertions(+), 87 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 81058ea..c92c9bd 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -77,7 +77,7 @@ PODS: - FlutterMacOS - smart_auth (0.0.1): - Flutter - - sqflite_darwin (0.0.4): + - sqflite (0.0.3): - Flutter - FlutterMacOS - system_info_plus (0.0.1): @@ -101,7 +101,7 @@ 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_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) + - 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`) @@ -148,8 +148,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" smart_auth: :path: ".symlinks/plugins/smart_auth/ios" - sqflite_darwin: - :path: ".symlinks/plugins/sqflite_darwin/darwin" + sqflite: + :path: ".symlinks/plugins/sqflite/darwin" system_info_plus: :path: ".symlinks/plugins/system_info_plus/ios" url_launcher_ios: @@ -183,10 +183,10 @@ SPEC CHECKSUMS: share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 smart_auth: 4bedbc118723912d0e45a07e8ab34039c19e04f2 - sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13 + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec system_info_plus: 5393c8da281d899950d751713575fbf91c7709aa url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe - veilid: f5c2e662f91907b30cf95762619526ac3e4512fd + veilid: 51243c25047dbc1ebbfd87d713560260d802b845 PODFILE CHECKSUM: 5d504085cd7c7a4d71ee600d7af087cb60ab75b2 diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 238bf3e..c311845 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -15,7 +15,7 @@ import screen_retriever import share_plus import shared_preferences_foundation import smart_auth -import sqflite_darwin +import sqflite import url_launcher_macos import veilid import window_manager diff --git a/pubspec.lock b/pubspec.lock index 4a839f3..b8cd074 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,15 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "72.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.2" + version: "67.0.0" accordion: dependency: "direct main" description: @@ -26,10 +21,10 @@ packages: dependency: transitive description: name: analyzer - sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "6.7.0" + version: "6.4.1" animated_bottom_navigation_bar: dependency: "direct main" description: @@ -202,18 +197,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7" url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.4.11" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe url: "https://pub.dev" source: hosted - version: "7.3.2" + version: "7.3.1" built_collection: dependency: transitive description: @@ -266,10 +261,10 @@ packages: dependency: transitive description: name: camera_android - sha256: "387c9861dc026b980e5ff16c949c5bcfecafa8825b8b74e41b24405eb9d39af6" + sha256: "32f04948a284b71d938fe275616faf4957d07f9b3aab8021bfc8c418301a289e" url: "https://pub.dev" source: hosted - version: "0.10.9+14" + version: "0.10.9+11" camera_avfoundation: dependency: transitive description: @@ -426,10 +421,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "2.3.6" diffutil_dart: dependency: transitive description: @@ -560,10 +555,10 @@ packages: dependency: "direct main" description: name: flutter_form_builder - sha256: c278ef69b08957d484f83413f0e77b656a39b7a7bb4eb8a295da3a820ecc6545 + sha256: "447f8808f68070f7df968e8063aada3c9d2e90e789b5b70f3b44e4b315212656" url: "https://pub.dev" source: hosted - version: "9.5.0" + version: "9.3.0" flutter_hooks: dependency: "direct main" description: @@ -613,10 +608,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398" + sha256: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda" url: "https://pub.dev" source: hosted - version: "2.0.23" + version: "2.0.22" flutter_shaders: dependency: transitive description: @@ -690,10 +685,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" + sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1 url: "https://pub.dev" source: hosted - version: "2.5.7" + version: "2.5.2" freezed_annotation: dependency: "direct main" description: @@ -886,14 +881,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" - macros: - dependency: transitive - description: - name: macros - sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" - url: "https://pub.dev" - source: hosted - version: "0.1.2-main.4" matcher: dependency: transitive description: @@ -906,18 +893,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.8.0" meta: dependency: "direct main" description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.12.0" mime: dependency: transitive description: @@ -1026,10 +1013,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a + sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" url: "https://pub.dev" source: hosted - version: "2.2.12" + version: "2.2.10" path_provider_foundation: dependency: transitive description: @@ -1307,10 +1294,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" + sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.3.2" shared_preferences_foundation: dependency: transitive description: @@ -1465,42 +1452,18 @@ packages: dependency: transitive description: name: sqflite - sha256: "79a297dc3cc137e758c6a4baf83342b039e5a6d2436fcdf3f96a00adaaf2ad62" + sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d url: "https://pub.dev" source: hosted - version: "2.4.0" - sqflite_android: - dependency: transitive - description: - name: sqflite_android - sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3" - url: "https://pub.dev" - source: hosted - version: "2.4.0" + version: "2.3.3+1" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "4468b24876d673418a7b7147e5a08a715b4998a7ae69227acafaab762e0e5490" + sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" url: "https://pub.dev" source: hosted - version: "2.5.4+5" - sqflite_darwin: - dependency: transitive - description: - name: sqflite_darwin - sha256: "769733dddf94622d5541c73e4ddc6aa7b252d865285914b6fcd54a63c4b4f027" - url: "https://pub.dev" - source: hosted - version: "2.4.1-1" - sqflite_platform_interface: - dependency: transitive - description: - name: sqflite_platform_interface - sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" - url: "https://pub.dev" - source: hosted - version: "2.4.0" + version: "2.5.4" stack_trace: dependency: "direct main" description: @@ -1545,10 +1508,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" url: "https://pub.dev" source: hosted - version: "3.3.0+3" + version: "3.1.0+1" system_info2: dependency: transitive description: @@ -1577,10 +1540,10 @@ packages: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.0" timing: dependency: transitive description: @@ -1601,10 +1564,10 @@ packages: dependency: transitive description: name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.3.2" universal_io: dependency: transitive description: @@ -1633,10 +1596,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "8fc3bae0b68c02c47c5c86fa8bfa74471d42687b0eded01b78de87872db745e2" + sha256: f0c73347dfcfa5b3db8bc06e1502668265d39c08f310c29bff4e28eea9699f79 url: "https://pub.dev" source: hosted - version: "6.3.12" + version: "6.3.9" url_launcher_ios: dependency: transitive description: @@ -1739,7 +1702,7 @@ packages: path: "../veilid/veilid-flutter" relative: true source: path - version: "0.3.4" + version: "0.4.1" veilid_support: dependency: "direct main" description: @@ -1791,10 +1754,10 @@ packages: dependency: transitive description: name: win32 - sha256: "2735daae5150e8b1dfeb3eb0544b4d3af0061e9e82cef063adcd583bdae4306a" + sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" url: "https://pub.dev" source: hosted - version: "5.7.0" + version: "5.5.4" window_manager: dependency: "direct main" description: @@ -1860,5 +1823,5 @@ packages: source: hosted version: "1.1.2" sdks: - dart: ">=3.5.0 <4.0.0" - flutter: ">=3.24.0" + dart: ">=3.4.1 <4.0.0" + flutter: ">=3.22.1" From c3cc1b6a6fae11883d9535f6876066c378c31058 Mon Sep 17 00:00:00 2001 From: Dumont Date: Tue, 12 Nov 2024 10:56:14 -0500 Subject: [PATCH 203/270] fix linux desktop builds Fixes the "No target file veilid-flutter-shared" error documented here: https://gitlab.com/veilid/veilid/-/issues/397 --- linux/CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index b669b11..e71febc 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -122,6 +122,12 @@ foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) COMPONENT Runtime) endforeach(bundled_library) +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") From 768c649a555164398092b685d3fce0b6dd4bb0d8 Mon Sep 17 00:00:00 2001 From: "kimmy.zip" <15420938-kimmydotzip@users.noreply.gitlab.com> Date: Mon, 10 Feb 2025 03:00:31 +0000 Subject: [PATCH 204/270] Fix compatibility issues with Flutter 3.27.0 --- macos/Flutter/GeneratedPluginRegistrant.swift | 2 +- pubspec.lock | 383 ++++++++++-------- pubspec.yaml | 2 +- windows/flutter/CMakeLists.txt | 7 +- 4 files changed, 218 insertions(+), 176 deletions(-) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index c311845..238bf3e 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -15,7 +15,7 @@ import screen_retriever import share_plus import shared_preferences_foundation import smart_auth -import sqflite +import sqflite_darwin import url_launcher_macos import veilid import window_manager diff --git a/pubspec.lock b/pubspec.lock index b8cd074..7e541f2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,15 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + sha256: "03f6da266a27a4538a69295ec142cb5717d7d4e5727b84658b63e1e1509bac9c" url: "https://pub.dev" source: hosted - version: "67.0.0" + version: "79.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.3" accordion: dependency: "direct main" description: @@ -21,10 +26,10 @@ packages: dependency: transitive description: name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + sha256: c9040fc56483c22a5e04a9f6a251313118b1a3c42423770623128fa484115643 url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "7.2.0" animated_bottom_navigation_bar: dependency: "direct main" description: @@ -109,10 +114,10 @@ packages: dependency: transitive description: name: barcode - sha256: ab180ce22c6555d77d45f0178a523669db67f95856e3378259ef2ffeb43e6003 + sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4" url: "https://pub.dev" source: hosted - version: "2.2.8" + version: "2.2.9" basic_utils: dependency: "direct main" description: @@ -157,58 +162,58 @@ packages: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" build: dependency: transitive description: name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" build_config: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.3" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7" + sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" url: "https://pub.dev" source: hosted - version: "2.4.11" + version: "2.4.14" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.3.1" + version: "8.0.0" built_collection: dependency: transitive description: @@ -221,10 +226,10 @@ packages: dependency: transitive description: name: built_value - sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2" url: "https://pub.dev" source: hosted - version: "8.9.2" + version: "8.9.3" cached_network_image: dependency: transitive description: @@ -261,26 +266,26 @@ packages: dependency: transitive description: name: camera_android - sha256: "32f04948a284b71d938fe275616faf4957d07f9b3aab8021bfc8c418301a289e" + sha256: "007c57cdcace4751014071e3d42f2eb8a64a519254abed35b714223d81d66234" url: "https://pub.dev" source: hosted - version: "0.10.9+11" + version: "0.10.10" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: "0d04cec8715b59fb6dc60eefb47e69024f51233c570e475b886dc9290568bca7" + sha256: "55eb9c216f25339a3faa55fc42826e2c4a45becefa1387fd50fce6ae9dd0c574" url: "https://pub.dev" source: hosted - version: "0.9.17+4" + version: "0.9.18+1" camera_platform_interface: dependency: transitive description: name: camera_platform_interface - sha256: b3ede1f171532e0d83111fe0980b46d17f1aa9788a07a2fbed07366bbdbb9061 + sha256: "953e7baed3a7c8fae92f7200afeb2be503ff1a17c3b4e4ed7b76f008c2810a31" url: "https://pub.dev" source: hosted - version: "2.8.0" + version: "2.9.0" camera_web: dependency: transitive description: @@ -293,10 +298,10 @@ packages: dependency: "direct main" description: name: change_case - sha256: "99cfdf2018c627c8a3af5a23ea4c414eb69c75c31322d23b9660ebc3cf30b514" + sha256: e41ef3df58521194ef8d7649928954805aeb08061917cf658322305e61568003 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" characters: dependency: transitive description: @@ -309,10 +314,10 @@ packages: dependency: "direct main" description: name: charcode - sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.4.0" charset: dependency: transitive description: @@ -357,18 +362,18 @@ packages: dependency: transitive description: name: code_builder - sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.10.1" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" convert: dependency: transitive description: @@ -405,10 +410,10 @@ packages: dependency: transitive description: name: csslib - sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.2" cupertino_icons: dependency: "direct main" description: @@ -421,10 +426,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "3.0.1" diffutil_dart: dependency: transitive description: @@ -437,26 +442,26 @@ packages: dependency: transitive description: name: dio - sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" url: "https://pub.dev" source: hosted - version: "5.7.0" + version: "5.8.0+1" dio_web_adapter: dependency: transitive description: name: dio_web_adapter - sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" + sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" equatable: dependency: "direct main" description: name: equatable - sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.7" expansion_tile_group: dependency: "direct main" description: @@ -514,10 +519,10 @@ packages: dependency: "direct main" description: name: flutter_animate - sha256: "7c8a6594a9252dad30cc2ef16e33270b6248c4dedc3b3d06c86c4f3f4dc05ae5" + sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.5.2" flutter_bloc: dependency: "direct main" description: @@ -555,10 +560,10 @@ packages: dependency: "direct main" description: name: flutter_form_builder - sha256: "447f8808f68070f7df968e8063aada3c9d2e90e789b5b70f3b44e4b315212656" + sha256: "375da52998c72f80dec9187bd93afa7ab202b89d5d066699368ff96d39fd4876" url: "https://pub.dev" source: hosted - version: "9.3.0" + version: "9.7.0" flutter_hooks: dependency: "direct main" description: @@ -592,10 +597,10 @@ packages: dependency: "direct main" description: name: flutter_native_splash - sha256: aa06fec78de2190f3db4319dd60fdc8d12b2626e93ef9828633928c2dcaea840 + sha256: "7062602e0dbd29141fb8eb19220b5871ca650be5197ab9c1f193a28b17537bc7" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.4" flutter_parsed_text: dependency: transitive description: @@ -608,10 +613,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda" + sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e" url: "https://pub.dev" source: hosted - version: "2.0.22" + version: "2.0.24" flutter_shaders: dependency: transitive description: @@ -624,10 +629,10 @@ packages: dependency: "direct main" description: name: flutter_slidable - sha256: "2c5611c0b44e20d180e4342318e1bbc28b0a44ad2c442f5df16962606fd3e8e3" + sha256: a857de7ea701f276fd6a6c4c67ae885b60729a3449e42766bb0e655171042801 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" flutter_spinkit: dependency: "direct main" description: @@ -640,18 +645,18 @@ packages: dependency: "direct main" description: name: flutter_sticky_header - sha256: "017f398fbb45a589e01491861ca20eb6570a763fd9f3888165a978e11248c709" + sha256: "7f76d24d119424ca0c95c146b8627a457e8de8169b0d584f766c2c545db8f8be" url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.7.0" flutter_svg: dependency: "direct main" description: name: flutter_svg - sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" + sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b url: "https://pub.dev" source: hosted - version: "2.0.10+1" + version: "2.0.17" flutter_translate: dependency: "direct main" description: @@ -677,18 +682,18 @@ packages: dependency: "direct main" description: name: form_builder_validators - sha256: c61ed7b1deecf0e1ebe49e2fa79e3283937c5a21c7e48e3ed9856a4a14e1191a + sha256: "517fb884183fff7a0ef3db7d375981011da26ee452f20fb3d2e788ad527ad01d" url: "https://pub.dev" source: hosted - version: "11.0.0" + version: "11.1.1" freezed: dependency: "direct dev" description: name: freezed - sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1 + sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.8" freezed_annotation: dependency: "direct main" description: @@ -717,10 +722,10 @@ packages: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" globbing: dependency: transitive description: @@ -733,10 +738,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "6f1b756f6e863259a99135ff3c95026c3cdca17d10ebef2bba2261a25ddc8bbc" + sha256: "9b736a9fa879d8ad6df7932cbdcc58237c173ab004ef90d8377923d7ad731eaa" url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "14.7.2" graphs: dependency: transitive description: @@ -757,34 +762,34 @@ packages: dependency: transitive description: name: html - sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" url: "https://pub.dev" source: hosted - version: "0.15.4" + version: "0.15.5" http: dependency: transitive description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.3.0" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" hydrated_bloc: dependency: "direct main" description: @@ -821,10 +826,10 @@ packages: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" js: dependency: transitive description: @@ -845,10 +850,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + sha256: b0a98230538fe5d0b60a22fb6bf1b6cb03471b53e3324ff6069c591679dd59c9 url: "https://pub.dev" source: hosted - version: "6.8.0" + version: "6.9.3" linkify: dependency: transitive description: @@ -881,6 +886,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + macros: + dependency: transitive + description: + name: macros + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" + url: "https://pub.dev" + source: hosted + version: "0.1.3-main.0" matcher: dependency: transitive description: @@ -893,18 +906,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: "direct main" description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: transitive description: @@ -957,26 +970,26 @@ packages: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" package_info_plus: dependency: "direct main" description: name: package_info_plus - sha256: df3eb3e0aed5c1107bb0fdb80a8e82e778114958b1c5ac5644fb1ac9cae8a998 + sha256: b15fad91c4d3d1f2b48c053dd41cb82da007c27407dc9ab5f9aa59881d0e39d4 url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "8.1.4" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 + sha256: a5ef9986efc7bf772f2696183a3992615baa76c1ffb1189318dd8803778fb05b url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" pasteboard: dependency: "direct main" description: @@ -997,34 +1010,34 @@ packages: dependency: transitive description: name: path_parsing - sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" path_provider: dependency: "direct main" description: name: path_provider - sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" url: "https://pub.dev" source: hosted - version: "2.2.10" + version: "2.2.15" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -1053,10 +1066,10 @@ packages: dependency: "direct main" description: name: pdf - sha256: "05df53f8791587402493ac97b9869d3824eccbc77d97855f4545cf72df3cae07" + sha256: adbdec5bc84d20e6c8d67f9c64270aa64d1e9e1ed529f0fef7e7bc7e9400f894 url: "https://pub.dev" source: hosted - version: "3.11.1" + version: "3.11.2" pdf_widget_wrapper: dependency: transitive description: @@ -1157,18 +1170,18 @@ packages: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" qr: dependency: transitive description: @@ -1286,26 +1299,26 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + sha256: "688ee90fbfb6989c980254a56cb26ebe9bb30a3a2dff439a78894211f73de67a" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.5.1" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" + sha256: "650584dcc0a39856f369782874e562efd002a9c94aec032412c9eb81419cce1f" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.4" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.4" shared_preferences_linux: dependency: transitive description: @@ -1342,18 +1355,18 @@ packages: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" signal_strength_indicator: dependency: "direct main" description: @@ -1366,7 +1379,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" sliver_expandable: dependency: "direct main" description: @@ -1412,26 +1425,26 @@ packages: dependency: transitive description: name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "2.0.0" source_helper: dependency: transitive description: name: source_helper - sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" url: "https://pub.dev" source: hosted - version: "1.3.4" + version: "1.3.5" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" split_view: dependency: "direct main" description: @@ -1452,26 +1465,50 @@ packages: dependency: transitive description: name: sqflite - sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d + sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" url: "https://pub.dev" source: hosted - version: "2.3.3+1" + version: "2.4.1" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3" + url: "https://pub.dev" + source: hosted + version: "2.4.0" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" + sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.5.4+6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c" + url: "https://pub.dev" + source: hosted + version: "2.4.1+1" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" stack_trace: dependency: "direct main" description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" star_menu: dependency: "direct main" description: @@ -1484,34 +1521,34 @@ packages: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: "direct main" description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - sha256: "0bd04f5bb74fcd6ff0606a888a30e917af9bd52820b178eaa464beb11dca84b6" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" synchronized: dependency: transitive description: name: synchronized - sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" url: "https://pub.dev" source: hosted - version: "3.1.0+1" + version: "3.3.0+3" system_info2: dependency: transitive description: @@ -1532,26 +1569,26 @@ packages: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.4" timing: dependency: transitive description: name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" transitioned_indexed_stack: dependency: "direct main" description: @@ -1564,10 +1601,10 @@ packages: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" universal_io: dependency: transitive description: @@ -1596,34 +1633,34 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: f0c73347dfcfa5b3db8bc06e1502668265d39c08f310c29bff4e28eea9699f79 + sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" url: "https://pub.dev" source: hosted - version: "6.3.9" + version: "6.3.14" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e + sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.2" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: @@ -1636,18 +1673,18 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.0" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" uuid: dependency: "direct main" description: @@ -1660,34 +1697,34 @@ packages: dependency: transitive description: name: value_layout_builder - sha256: "98202ec1807e94ac72725b7f0d15027afde513c55c69ff3f41bcfccb950831bc" + sha256: c02511ea91ca5c643b514a33a38fa52536f74aa939ec367d02938b5ede6807fa url: "https://pub.dev" source: hosted - version: "0.3.1" + version: "0.4.0" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" + sha256: "7ed22c21d7fdcc88dd6ba7860384af438cd220b251ad65dfc142ab722fabef61" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.16" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.13" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" + sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.16" vector_math: dependency: transitive description: @@ -1722,10 +1759,10 @@ packages: dependency: transitive description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web: dependency: transitive description: @@ -1746,18 +1783,18 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" win32: dependency: transitive description: name: win32 - sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" + sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e url: "https://pub.dev" source: hosted - version: "5.5.4" + version: "5.10.1" window_manager: dependency: "direct main" description: @@ -1794,10 +1831,10 @@ packages: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" zmodem: dependency: transitive description: @@ -1823,5 +1860,5 @@ packages: source: hosted version: "1.1.2" sdks: - dart: ">=3.4.1 <4.0.0" - flutter: ">=3.22.1" + dart: ">=3.6.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index 8b171f3..6027eee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,7 +48,7 @@ dependencies: flutter_native_splash: ^2.4.0 flutter_slidable: ^3.1.0 flutter_spinkit: ^5.2.1 - flutter_sticky_header: ^0.6.5 + flutter_sticky_header: ^0.7.0 flutter_svg: ^2.0.10+1 flutter_translate: ^4.1.0 flutter_zoom_drawer: ^3.2.0 diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt index 930d207..903f489 100644 --- a/windows/flutter/CMakeLists.txt +++ b/windows/flutter/CMakeLists.txt @@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -92,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS From 39b0262d0e636dffa9964f90a44e192efd08fabf Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 18 Feb 2025 21:37:46 -0500 Subject: [PATCH 205/270] more log work --- .gitignore | 2 + android/app/build.gradle | 33 +- android/build.gradle | 13 - .../gradle/wrapper/gradle-wrapper.properties | 2 +- android/settings.gradle | 30 +- dev-setup/flutter_config.sh | 10 +- ios/Podfile | 2 +- ios/Podfile.lock | 134 +++---- ios/Runner.xcodeproj/project.pbxproj | 3 + .../chat_single_contact_item_widget.dart | 27 +- lib/theme/models/contrast_generator.dart | 16 +- lib/theme/models/models.dart | 1 + lib/theme/models/radix_generator.dart | 25 +- lib/theme/models/scale_scheme.dart | 30 -- lib/theme/models/scale_theme.dart | 88 +++++ lib/theme/models/slider_tile.dart | 95 ++--- lib/tools/loggy.dart | 3 +- linux/flutter/generated_plugin_registrant.cc | 12 +- linux/flutter/generated_plugins.cmake | 3 +- macos/Flutter/GeneratedPluginRegistrant.swift | 6 +- macos/Podfile.lock | 44 +-- macos/Runner/AppDelegate.swift | 4 + .../example/android/app/build.gradle | 10 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../example/dev-setup/flutter_config.sh | 10 +- packages/veilid_support/example/pubspec.lock | 142 +++---- packages/veilid_support/example/pubspec.yaml | 2 +- .../veilid_support/lib/src/veilid_log.dart | 5 - packages/veilid_support/pubspec.lock | 235 ++++++------ packages/veilid_support/pubspec.yaml | 2 +- pubspec.lock | 355 +++++++++++++----- pubspec.yaml | 22 +- web/index.html | 132 ++++++- .../flutter/generated_plugin_registrant.cc | 9 +- windows/flutter/generated_plugins.cmake | 3 +- 35 files changed, 906 insertions(+), 606 deletions(-) create mode 100644 lib/theme/models/scale_theme.dart diff --git a/.gitignore b/.gitignore index 46dd51f..79e7a9c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related diff --git a/android/app/build.gradle b/android/app/build.gradle index f5e4e3d..9d84346 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,3 +1,9 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -21,10 +22,6 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - def buildConfig = 'debug' def keystoreProperties = new Properties() @@ -35,16 +32,16 @@ if (keystorePropertiesFile.exists()) { } android { - ndkVersion "26.3.11579264" + ndkVersion "27.0.12077973" compileSdkVersion flutter.compileSdkVersion - + compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } sourceSets { @@ -70,7 +67,7 @@ android { storePassword keystoreProperties['storePassword'] } } - + buildTypes { release { shrinkResources false @@ -82,7 +79,7 @@ android { } } } - + namespace 'com.veilid.veilidchat' } @@ -90,6 +87,4 @@ flutter { source '../..' } -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" -} +dependencies {} diff --git a/android/build.gradle b/android/build.gradle index dbb249e..bc157bd 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,16 +1,3 @@ -buildscript { - ext.kotlin_version = '1.9.10' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index e1ca574..afa1e8e 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index 44e62bc..b1ae36a 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,11 +1,25 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.8.0" apply false + id "org.jetbrains.kotlin.android" version "1.9.25" apply false +} + +include ":app" \ No newline at end of file diff --git a/dev-setup/flutter_config.sh b/dev-setup/flutter_config.sh index 1168d74..a1b8c8d 100755 --- a/dev-setup/flutter_config.sh +++ b/dev-setup/flutter_config.sh @@ -14,13 +14,13 @@ sed -i '' 's/MACOSX_DEPLOYMENT_TARGET = [^;]*/MACOSX_DEPLOYMENT_TARGET = 10.14.6 sed -i '' "s/platform :osx, '[^']*'/platform :osx, '10.14.6'/g" $APPDIR/macos/Podfile # Android: Set NDK version -if [[ "$TMPDIR" != "" ]]; then +if [[ "$TMPDIR" != "" ]]; then ANDTMP=$TMPDIR/andtmp_$(date +%s) -else +else ANDTMP=/tmp/andtmp_$(date +%s) fi cat < $ANDTMP - ndkVersion '26.3.11579264' + ndkVersion '27.0.12077973' EOF sed -i '' -e "/android {/r $ANDTMP" $APPDIR/android/app/build.gradle rm -- $ANDTMP @@ -29,7 +29,7 @@ rm -- $ANDTMP sed -i '' 's/minSdkVersion .*/minSdkVersion Math.max(flutter.minSdkVersion, 24)/g' $APPDIR/android/app/build.gradle # Android: Set gradle plugin version -sed -i '' "s/classpath \'com.android.tools.build:gradle:[^\']*\'/classpath 'com.android.tools.build:gradle:7.2.0'/g" $APPDIR/android/build.gradle +sed -i '' "s/classpath \'com.android.tools.build:gradle:[^\']*\'/classpath 'com.android.tools.build:gradle:8.8.0'/g" $APPDIR/android/build.gradle # Android: Set gradle version -sed -i '' 's/distributionUrl=.*/distributionUrl=https:\/\/services.gradle.org\/distributions\/gradle-7.6.3-all.zip/g' $APPDIR/android/gradle/wrapper/gradle-wrapper.properties +sed -i '' 's/distributionUrl=.*/distributionUrl=https:\/\/services.gradle.org\/distributions\/gradle-8.10.2-all.zip/g' $APPDIR/android/gradle/wrapper/gradle-wrapper.properties diff --git a/ios/Podfile b/ios/Podfile index 2cbcaa2..572283f 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.4' +platform :ios, '15.6' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c92c9bd..c00efec 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -4,60 +4,56 @@ PODS: - file_saver (0.0.1): - Flutter - Flutter (1.0.0) - - flutter_native_splash (0.0.1): + - flutter_native_splash (2.4.3): - Flutter - - GoogleDataTransport (9.4.1): - - GoogleUtilities/Environment (~> 7.7) - - nanopb (< 2.30911.0, >= 2.30908.0) - - PromisesObjC (< 3.0, >= 1.2) - - GoogleMLKit/BarcodeScanning (6.0.0): + - GoogleDataTransport (10.1.0): + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - GoogleMLKit/BarcodeScanning (7.0.0): - GoogleMLKit/MLKitCore - - MLKitBarcodeScanning (~> 5.0.0) - - GoogleMLKit/MLKitCore (6.0.0): - - MLKitCommon (~> 11.0.0) + - MLKitBarcodeScanning (~> 6.0.0) + - GoogleMLKit/MLKitCore (7.0.0): + - MLKitCommon (~> 12.0.0) - GoogleToolboxForMac/Defines (4.2.1) - GoogleToolboxForMac/Logger (4.2.1): - GoogleToolboxForMac/Defines (= 4.2.1) - "GoogleToolboxForMac/NSData+zlib (4.2.1)": - GoogleToolboxForMac/Defines (= 4.2.1) - - GoogleUtilities/Environment (7.13.3): + - GoogleUtilities/Environment (8.0.2): - GoogleUtilities/Privacy - - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.13.3): + - GoogleUtilities/Logger (8.0.2): - GoogleUtilities/Environment - GoogleUtilities/Privacy - - GoogleUtilities/Privacy (7.13.3) - - GoogleUtilities/UserDefaults (7.13.3): + - GoogleUtilities/Privacy (8.0.2) + - GoogleUtilities/UserDefaults (8.0.2): - GoogleUtilities/Logger - GoogleUtilities/Privacy - - GoogleUtilitiesComponents (1.1.0): - - GoogleUtilities/Logger - - GTMSessionFetcher/Core (3.4.1) - - MLImage (1.0.0-beta5) - - MLKitBarcodeScanning (5.0.0): - - MLKitCommon (~> 11.0) - - MLKitVision (~> 7.0) - - MLKitCommon (11.0.0): - - GoogleDataTransport (< 10.0, >= 9.4.1) + - GTMSessionFetcher/Core (3.5.0) + - MLImage (1.0.0-beta6) + - MLKitBarcodeScanning (6.0.0): + - MLKitCommon (~> 12.0) + - MLKitVision (~> 8.0) + - MLKitCommon (12.0.0): + - GoogleDataTransport (~> 10.0) - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" - - GoogleUtilities/UserDefaults (< 8.0, >= 7.13.0) - - GoogleUtilitiesComponents (~> 1.0) + - GoogleUtilities/Logger (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) - - MLKitVision (7.0.0): + - MLKitVision (8.0.0): - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) - - MLImage (= 1.0.0-beta5) - - MLKitCommon (~> 11.0) - - mobile_scanner (5.2.3): + - MLImage (= 1.0.0-beta6) + - MLKitCommon (~> 12.0) + - mobile_scanner (6.0.2): - Flutter - - GoogleMLKit/BarcodeScanning (~> 6.0.0) - - nanopb (2.30910.0): - - nanopb/decode (= 2.30910.0) - - nanopb/encode (= 2.30910.0) - - nanopb/decode (2.30910.0) - - nanopb/encode (2.30910.0) + - GoogleMLKit/BarcodeScanning (~> 7.0.0) + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) - native_device_orientation (0.0.1): - Flutter - package_info_plus (0.4.5): @@ -75,9 +71,7 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - smart_auth (0.0.1): - - Flutter - - sqflite (0.0.3): + - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS - system_info_plus (0.0.1): @@ -100,8 +94,7 @@ DEPENDENCIES: - printing (from `.symlinks/plugins/printing/ios`) - 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/darwin`) + - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/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`) @@ -112,7 +105,6 @@ SPEC REPOS: - GoogleMLKit - GoogleToolboxForMac - GoogleUtilities - - GoogleUtilitiesComponents - GTMSessionFetcher - MLImage - MLKitBarcodeScanning @@ -146,10 +138,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" - smart_auth: - :path: ".symlinks/plugins/smart_auth/ios" - sqflite: - :path: ".symlinks/plugins/sqflite/darwin" + sqflite_darwin: + :path: ".symlinks/plugins/sqflite_darwin/darwin" system_info_plus: :path: ".symlinks/plugins/system_info_plus/ios" url_launcher_ios: @@ -158,36 +148,34 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/veilid/ios" SPEC CHECKSUMS: - camera_avfoundation: dd002b0330f4981e1bbcb46ae9b62829237459a4 - file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 + camera_avfoundation: 04b44aeb14070126c6529e5ab82cc7c9fca107cf + file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 - GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a - GoogleMLKit: 97ac7af399057e99182ee8edfa8249e3226a4065 + flutter_native_splash: 6cad9122ea0fad137d23137dd14b937f3e90b145 + GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 + GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318 GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 - GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 - GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe - GTMSessionFetcher: 8000756fc1c19d2e5697b90311f7832d2e33f6cd - MLImage: 1824212150da33ef225fbd3dc49f184cf611046c - MLKitBarcodeScanning: 10ca0845a6d15f2f6e911f682a1998b68b973e8b - MLKitCommon: afec63980417d29ffbb4790529a1b0a2291699e1 - MLKitVision: e858c5f125ecc288e4a31127928301eaba9ae0c1 - mobile_scanner: 96e91f2e1fb396bb7df8da40429ba8dfad664740 - nanopb: 438bc412db1928dac798aa6fd75726007be04262 - native_device_orientation: 348b10c346a60ebbc62fb235a4fdb5d1b61a8f55 - package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 - pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - printing: 233e1b73bd1f4a05615548e9b5a324c98588640b + GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d + GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 + MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56 + MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2 + MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d + MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e + mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036 + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 + native_device_orientation: e3580675687d5034770da198f6839ebf2122ef94 + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + printing: 54ff03f28fe9ba3aa93358afb80a8595a071dd07 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 - share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - smart_auth: 4bedbc118723912d0e45a07e8ab34039c19e04f2 - sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec - system_info_plus: 5393c8da281d899950d751713575fbf91c7709aa - url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe - veilid: 51243c25047dbc1ebbfd87d713560260d802b845 + share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + system_info_plus: 555ce7047fbbf29154726db942ae785c29211740 + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + veilid: 3ce560a4f2b568a77a9fd5e23090f2fa97581019 -PODFILE CHECKSUM: 5d504085cd7c7a4d71ee600d7af087cb60ab75b2 +PODFILE CHECKSUM: c8bf5b16c34712d5790b0b8d2472cc66ac0a8487 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 417e34c..06556a5 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -380,6 +380,7 @@ INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = VeilidChat; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -510,6 +511,7 @@ INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = VeilidChat; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -534,6 +536,7 @@ INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = VeilidChat; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", 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 d3db153..826fe37 100644 --- a/lib/chat_list/views/chat_single_contact_item_widget.dart +++ b/lib/chat_list/views/chat_single_contact_item_widget.dart @@ -29,8 +29,7 @@ class ChatSingleContactItemWidget extends StatelessWidget { BuildContext context, ) { final theme = Theme.of(context); - final scale = theme.extension()!; - final scaleConfig = theme.extension()!; + final scaleTheme = Theme.of(context).extension()!; final activeChatCubit = context.watch(); final selected = activeChatCubit.state == _localConversationRecordKey; @@ -44,18 +43,22 @@ class ChatSingleContactItemWidget extends StatelessWidget { ? proto.Availability.AVAILABILITY_UNSPECIFIED : _contact.profile.availability; + final scaleTileTheme = scaleTheme.tileTheme( + disabled: _disabled, + selected: selected, + scaleKind: ScaleKind.secondary); + final avatar = AvatarWidget( name: name, size: 34, - borderColor: _disabled - ? scale.grayScale.primaryText - : scale.secondaryScale.primaryText, + borderColor: scaleTileTheme.borderColor, foregroundColor: _disabled - ? scale.grayScale.primaryText - : scale.secondaryScale.primaryText, - backgroundColor: - _disabled ? scale.grayScale.primary : scale.secondaryScale.primary, - scaleConfig: scaleConfig, + ? scaleTheme.scheme.grayScale.primaryText + : scaleTheme.scheme.secondaryScale.primaryText, + backgroundColor: _disabled + ? scaleTheme.scheme.grayScale.primary + : scaleTheme.scheme.secondaryScale.primary, + scaleConfig: scaleTheme.config, textStyle: theme.textTheme.titleLarge!, ); @@ -69,9 +72,7 @@ class ChatSingleContactItemWidget extends StatelessWidget { leading: avatar, trailing: AvailabilityWidget( availability: availability, - color: _disabled - ? scale.grayScale.primaryText - : scale.secondaryScale.primaryText, + color: scaleTileTheme.textColor, ), onTap: () { singleFuture(activeChatCubit, () async { diff --git a/lib/theme/models/contrast_generator.dart b/lib/theme/models/contrast_generator.dart index 308b6b3..4271c1a 100644 --- a/lib/theme/models/contrast_generator.dart +++ b/lib/theme/models/contrast_generator.dart @@ -4,6 +4,7 @@ import 'radix_generator.dart'; import 'scale_color.dart'; import 'scale_input_decorator_theme.dart'; import 'scale_scheme.dart'; +import 'scale_theme.dart'; ScaleColor _contrastScaleColor( {required Brightness brightness, @@ -261,14 +262,16 @@ ThemeData contrastGenerator({ final colorScheme = scaleScheme.toColorScheme( brightness, ); + final scaleTheme = ScaleTheme( + textTheme: textTheme, scheme: scaleScheme, config: scaleConfig); - final themeData = ThemeData.from( + final baseThemeData = ThemeData.from( colorScheme: colorScheme, textTheme: textTheme, useMaterial3: true); - return themeData.copyWith( - appBarTheme: themeData.appBarTheme.copyWith( + final themeData = baseThemeData.copyWith( + appBarTheme: baseThemeData.appBarTheme.copyWith( backgroundColor: scaleScheme.primaryScale.border, foregroundColor: scaleScheme.primaryScale.borderText), - bottomSheetTheme: themeData.bottomSheetTheme.copyWith( + bottomSheetTheme: baseThemeData.bottomSheetTheme.copyWith( elevation: 0, modalElevation: 0, shape: RoundedRectangleBorder( @@ -277,7 +280,7 @@ ThemeData contrastGenerator({ topRight: Radius.circular(16 * scaleConfig.borderRadiusScale)))), canvasColor: scaleScheme.primaryScale.subtleBackground, - chipTheme: themeData.chipTheme.copyWith( + chipTheme: baseThemeData.chipTheme.copyWith( backgroundColor: scaleScheme.primaryScale.elementBackground, selectedColor: scaleScheme.primaryScale.activeElementBackground, surfaceTintColor: scaleScheme.primaryScale.hoverElementBackground, @@ -303,5 +306,8 @@ ThemeData contrastGenerator({ extensions: >[ scaleScheme, scaleConfig, + scaleTheme ]); + + return themeData; } diff --git a/lib/theme/models/models.dart b/lib/theme/models/models.dart index e0ba490..45b54f9 100644 --- a/lib/theme/models/models.dart +++ b/lib/theme/models/models.dart @@ -2,5 +2,6 @@ export 'chat_theme.dart'; export 'radix_generator.dart'; export 'scale_color.dart'; export 'scale_scheme.dart'; +export 'scale_theme.dart'; export 'slider_tile.dart'; export 'theme_preference.dart'; diff --git a/lib/theme/models/radix_generator.dart b/lib/theme/models/radix_generator.dart index c3802e6..a2bdbb1 100644 --- a/lib/theme/models/radix_generator.dart +++ b/lib/theme/models/radix_generator.dart @@ -7,6 +7,7 @@ import '../../tools/tools.dart'; import 'scale_color.dart'; import 'scale_input_decorator_theme.dart'; import 'scale_scheme.dart'; +import 'scale_theme.dart'; enum RadixThemeColor { scarlet, // red + violet + tomato @@ -610,10 +611,14 @@ ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) { borderRadiusScale: 1, ); - final themeData = ThemeData.from( + final scaleTheme = ScaleTheme( + textTheme: textTheme, scheme: scaleScheme, config: scaleConfig); + + final baseThemeData = ThemeData.from( colorScheme: colorScheme, textTheme: textTheme, useMaterial3: true); - return themeData.copyWith( - scrollbarTheme: themeData.scrollbarTheme.copyWith( + + final themeData = baseThemeData.copyWith( + scrollbarTheme: baseThemeData.scrollbarTheme.copyWith( thumbColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.pressed)) { return scaleScheme.primaryScale.border; @@ -636,10 +641,10 @@ ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) { } return scaleScheme.primaryScale.subtleBorder; })), - appBarTheme: themeData.appBarTheme.copyWith( + appBarTheme: baseThemeData.appBarTheme.copyWith( backgroundColor: scaleScheme.primaryScale.border, foregroundColor: scaleScheme.primaryScale.borderText), - bottomSheetTheme: themeData.bottomSheetTheme.copyWith( + bottomSheetTheme: baseThemeData.bottomSheetTheme.copyWith( elevation: 0, modalElevation: 0, shape: const RoundedRectangleBorder( @@ -647,7 +652,7 @@ ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) { topLeft: Radius.circular(16), topRight: Radius.circular(16)))), canvasColor: scaleScheme.primaryScale.subtleBackground, - chipTheme: themeData.chipTheme.copyWith( + chipTheme: baseThemeData.chipTheme.copyWith( backgroundColor: scaleScheme.primaryScale.elementBackground, selectedColor: scaleScheme.primaryScale.activeElementBackground, surfaceTintColor: scaleScheme.primaryScale.hoverElementBackground, @@ -666,5 +671,11 @@ ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) { ), inputDecorationTheme: ScaleInputDecoratorTheme(scaleScheme, scaleConfig, textTheme), - extensions: >[scaleScheme, scaleConfig]); + extensions: >[ + scaleScheme, + scaleConfig, + scaleTheme + ]); + + return themeData; } diff --git a/lib/theme/models/scale_scheme.dart b/lib/theme/models/scale_scheme.dart index 6dbc248..ac266bc 100644 --- a/lib/theme/models/scale_scheme.dart +++ b/lib/theme/models/scale_scheme.dart @@ -145,33 +145,3 @@ class ScaleConfig extends ThemeExtension { lerpDouble(borderRadiusScale, other.borderRadiusScale, t) ?? 1); } } - -class ScaleTheme extends ThemeExtension { - ScaleTheme({ - required this.scheme, - required this.config, - }); - - final ScaleScheme scheme; - final ScaleConfig config; - - @override - ScaleTheme copyWith({ - ScaleScheme? scheme, - ScaleConfig? config, - }) => - ScaleTheme( - scheme: scheme ?? this.scheme, - config: config ?? this.config, - ); - - @override - ScaleTheme lerp(ScaleTheme? other, double t) { - if (other is! ScaleTheme) { - return this; - } - return ScaleTheme( - scheme: scheme.lerp(other.scheme, t), - config: config.lerp(other.config, t)); - } -} diff --git a/lib/theme/models/scale_theme.dart b/lib/theme/models/scale_theme.dart new file mode 100644 index 0000000..95f7db9 --- /dev/null +++ b/lib/theme/models/scale_theme.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; + +import 'scale_scheme.dart'; + +class ScaleTileTheme { + ScaleTileTheme( + {required this.textColor, + required this.backgroundColor, + required this.borderColor, + required this.shapeBorder, + required this.largeTextStyle, + required this.smallTextStyle}); + + final Color textColor; + final Color backgroundColor; + final Color borderColor; + final ShapeBorder shapeBorder; + final TextStyle largeTextStyle; + final TextStyle smallTextStyle; +} + +class ScaleTheme extends ThemeExtension { + ScaleTheme({ + required this.textTheme, + required this.scheme, + required this.config, + }); + + final TextTheme textTheme; + final ScaleScheme scheme; + final ScaleConfig config; + + @override + ScaleTheme copyWith({ + TextTheme? textTheme, + ScaleScheme? scheme, + ScaleConfig? config, + }) => + ScaleTheme( + textTheme: textTheme ?? this.textTheme, + scheme: scheme ?? this.scheme, + config: config ?? this.config, + ); + + @override + ScaleTheme lerp(ScaleTheme? other, double t) { + if (other is! ScaleTheme) { + return this; + } + return ScaleTheme( + textTheme: TextTheme.lerp(textTheme, other.textTheme, t), + scheme: scheme.lerp(other.scheme, t), + config: config.lerp(other.config, t)); + } + + ScaleTileTheme tileTheme( + {bool disabled = false, + bool selected = false, + ScaleKind scaleKind = ScaleKind.primary}) { + final tileColor = scheme.scale(!disabled ? scaleKind : ScaleKind.gray); + + final borderColor = selected ? tileColor.hoverBorder : tileColor.border; + final backgroundColor = config.useVisualIndicators && !selected + ? tileColor.borderText + : borderColor; + final textColor = config.useVisualIndicators && !selected + ? borderColor + : tileColor.borderText; + + final largeTextStyle = textTheme.labelMedium!.copyWith(color: textColor); + final smallTextStyle = textTheme.labelSmall!.copyWith(color: textColor); + + final shapeBorder = RoundedRectangleBorder( + side: config.useVisualIndicators + ? BorderSide(width: 2, color: borderColor, strokeAlign: 0) + : BorderSide.none, + borderRadius: BorderRadius.circular(8 * config.borderRadiusScale)); + + return ScaleTileTheme( + textColor: textColor, + backgroundColor: backgroundColor, + borderColor: borderColor, + shapeBorder: shapeBorder, + largeTextStyle: largeTextStyle, + smallTextStyle: smallTextStyle, + ); + } +} diff --git a/lib/theme/models/slider_tile.dart b/lib/theme/models/slider_tile.dart index 2ce81c9..9b6957a 100644 --- a/lib/theme/models/slider_tile.dart +++ b/lib/theme/models/slider_tile.dart @@ -69,29 +69,15 @@ class SliderTile extends StatelessWidget { // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { final theme = Theme.of(context); - final scale = theme.extension()!; - final tileColor = scale.scale(!disabled ? tileScale : ScaleKind.gray); - final scaleConfig = theme.extension()!; - - final borderColor = selected ? tileColor.hoverBorder : tileColor.border; - final backgroundColor = scaleConfig.useVisualIndicators && !selected - ? tileColor.borderText - : borderColor; - final textColor = scaleConfig.useVisualIndicators && !selected - ? borderColor - : tileColor.borderText; + final scaleTheme = theme.extension()!; + final scaleTileTheme = scaleTheme.tileTheme( + disabled: disabled, selected: selected, scaleKind: tileScale); return Container( clipBehavior: Clip.antiAlias, decoration: ShapeDecoration( - color: backgroundColor, - shape: RoundedRectangleBorder( - side: scaleConfig.useVisualIndicators - ? BorderSide(width: 2, color: borderColor, strokeAlign: 0) - : BorderSide.none, - borderRadius: - BorderRadius.circular(8 * scaleConfig.borderRadiusScale), - )), + color: scaleTileTheme.backgroundColor, + shape: scaleTileTheme.shapeBorder), child: Slidable( // Specify a key if the Slidable is dismissible. key: key, @@ -99,50 +85,39 @@ class SliderTile extends StatelessWidget { ? null : ActionPane( motion: const DrawerMotion(), - children: endActions - .map( - (a) => SlidableAction( - onPressed: disabled ? null : a.onPressed, - backgroundColor: scaleConfig.useVisualIndicators - ? (selected - ? tileColor.borderText - : tileColor.border) - : scale.scale(a.actionScale).primary, - foregroundColor: scaleConfig.useVisualIndicators - ? (selected - ? tileColor.border - : tileColor.borderText) - : scale.scale(a.actionScale).primaryText, - icon: subtitle.isEmpty ? a.icon : null, - label: a.label, - padding: const EdgeInsets.all(2)), - ) - .toList()), + children: endActions.map((a) { + final scaleActionTheme = scaleTheme.tileTheme( + disabled: disabled, + selected: true, + scaleKind: a.actionScale); + return SlidableAction( + onPressed: disabled ? null : a.onPressed, + backgroundColor: scaleActionTheme.backgroundColor, + foregroundColor: scaleActionTheme.textColor, + icon: subtitle.isEmpty ? a.icon : null, + label: a.label, + padding: const EdgeInsets.all(2)); + }).toList()), startActionPane: startActions.isEmpty ? null : ActionPane( motion: const DrawerMotion(), - children: startActions - .map( - (a) => SlidableAction( - onPressed: disabled ? null : a.onPressed, - backgroundColor: scaleConfig.useVisualIndicators - ? (selected - ? tileColor.borderText - : tileColor.border) - : scale.scale(a.actionScale).primary, - foregroundColor: scaleConfig.useVisualIndicators - ? (selected - ? tileColor.border - : tileColor.borderText) - : scale.scale(a.actionScale).primaryText, - icon: subtitle.isEmpty ? a.icon : null, - label: a.label, - padding: const EdgeInsets.all(2)), - ) - .toList()), + children: startActions.map((a) { + final scaleActionTheme = scaleTheme.tileTheme( + disabled: disabled, + selected: true, + scaleKind: a.actionScale); + + return SlidableAction( + onPressed: disabled ? null : a.onPressed, + backgroundColor: scaleActionTheme.backgroundColor, + foregroundColor: scaleActionTheme.textColor, + icon: subtitle.isEmpty ? a.icon : null, + label: a.label, + padding: const EdgeInsets.all(2)); + }).toList()), child: Padding( - padding: scaleConfig.useVisualIndicators + padding: scaleTheme.config.useVisualIndicators ? EdgeInsets.zero : const EdgeInsets.fromLTRB(0, 2, 0, 2), child: GestureDetector( @@ -157,8 +132,8 @@ class SliderTile extends StatelessWidget { softWrap: false, ), subtitle: subtitle.isNotEmpty ? Text(subtitle) : null, - iconColor: textColor, - textColor: textColor, + iconColor: scaleTileTheme.textColor, + textColor: scaleTileTheme.textColor, leading: FittedBox(child: leading), trailing: FittedBox(child: trailing)))))); } diff --git a/lib/tools/loggy.dart b/lib/tools/loggy.dart index 0bb259c..d8d4880 100644 --- a/lib/tools/loggy.dart +++ b/lib/tools/loggy.dart @@ -110,7 +110,8 @@ class CallbackPrinter extends LoggyPrinter { @override void onLog(LogRecord record) { - final out = record.pretty(); + final out = record.pretty().replaceAll('\uFFFD', ''); + if (Platform.isAndroid) { debugPrint(out); } else { diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 3acb238..0ac222b 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -9,8 +9,7 @@ #include #include #include -#include -#include +#include #include #include #include @@ -25,12 +24,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) printing_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin"); printing_plugin_register_with_registrar(printing_registrar); - g_autoptr(FlPluginRegistrar) screen_retriever_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); - screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); - g_autoptr(FlPluginRegistrar) smart_auth_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "SmartAuthPlugin"); - smart_auth_plugin_register_with_registrar(smart_auth_registrar); + g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); + screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index d09262f..a48f10f 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -6,8 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_saver pasteboard printing - screen_retriever - smart_auth + screen_retriever_linux url_launcher_linux veilid window_manager diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 238bf3e..a599497 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -11,10 +11,9 @@ import package_info_plus import pasteboard import path_provider_foundation import printing -import screen_retriever +import screen_retriever_macos import share_plus import shared_preferences_foundation -import smart_auth import sqflite_darwin import url_launcher_macos import veilid @@ -27,10 +26,9 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin")) - ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) + ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) - SmartAuthPlugin.register(with: registry.registrar(forPlugin: "SmartAuthPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) VeilidPlugin.register(with: registry.registrar(forPlugin: "VeilidPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 9078901..2d40a21 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -2,7 +2,7 @@ PODS: - file_saver (0.0.1): - FlutterMacOS - FlutterMacOS (1.0.0) - - mobile_scanner (5.2.3): + - mobile_scanner (6.0.2): - FlutterMacOS - package_info_plus (0.0.1): - FlutterMacOS @@ -13,15 +13,13 @@ PODS: - FlutterMacOS - printing (1.0.0): - FlutterMacOS - - screen_retriever (0.0.1): + - screen_retriever_macos (0.0.1): - FlutterMacOS - share_plus (0.0.1): - FlutterMacOS - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - smart_auth (0.0.1): - - FlutterMacOS - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS @@ -40,10 +38,9 @@ DEPENDENCIES: - pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - printing (from `Flutter/ephemeral/.symlinks/plugins/printing/macos`) - - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) + - screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - - smart_auth (from `Flutter/ephemeral/.symlinks/plugins/smart_auth/macos`) - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - veilid (from `Flutter/ephemeral/.symlinks/plugins/veilid/macos`) @@ -64,14 +61,12 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin printing: :path: Flutter/ephemeral/.symlinks/plugins/printing/macos - screen_retriever: - :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos + screen_retriever_macos: + :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos share_plus: :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin - smart_auth: - :path: Flutter/ephemeral/.symlinks/plugins/smart_auth/macos sqflite_darwin: :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin url_launcher_macos: @@ -82,22 +77,21 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - file_saver: 44e6fbf666677faf097302460e214e977fdd977b + file_saver: e35bd97de451dde55ff8c38862ed7ad0f3418d0f FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - mobile_scanner: 0a05256215b047af27b9495db3b77640055e8824 - package_info_plus: f5790acc797bf17c3e959e9d6cf162cc68ff7523 - pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - printing: 1dd6a1fce2209ec240698e2439a4adbb9b427637 - screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 - share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - smart_auth: b38e3ab4bfe089eacb1e233aca1a2340f96c28e9 - sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13 - url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 - veilid: a54f57b7bcf0e4e072fe99272d76ca126b2026d0 - window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 + mobile_scanner: 0e365ed56cad24f28c0fd858ca04edefb40dfac3 + package_info_plus: f0052d280d17aa382b932f399edf32507174e870 + pasteboard: 278d8100149f940fb795d6b3a74f0720c890ecb7 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + printing: c4cf83c78fd684f9bc318e6aadc18972aa48f617 + screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f + share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 + veilid: 319e2e78836d7b3d08203596d0b4a0e244b68d29 + window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c PODFILE CHECKSUM: ff0a9a3ce75ee73f200ca7e2f47745698c917ef9 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index 8e02df2..b3c1761 100644 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -6,4 +6,8 @@ class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } } diff --git a/packages/veilid_support/example/android/app/build.gradle b/packages/veilid_support/example/android/app/build.gradle index 033f4ac..2ba6503 100644 --- a/packages/veilid_support/example/android/app/build.gradle +++ b/packages/veilid_support/example/android/app/build.gradle @@ -23,19 +23,19 @@ if (flutterVersionName == null) { } android { - ndkVersion '26.3.11579264' - ndkVersion '26.3.11579264' + ndkVersion '27.0.12077973' + ndkVersion '27.0.12077973' namespace "com.example.example" compileSdk flutter.compileSdkVersion ndkVersion flutter.ndkVersion compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } sourceSets { diff --git a/packages/veilid_support/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/veilid_support/example/android/gradle/wrapper/gradle-wrapper.properties index c3433f7..6f8524c 100644 --- a/packages/veilid_support/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/veilid_support/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https://services.gradle.org/distributions/gradle-7.3.3-all.zip +distributionUrl=https://services.gradle.org/distributions/gradle-8.10.2-all.zip diff --git a/packages/veilid_support/example/dev-setup/flutter_config.sh b/packages/veilid_support/example/dev-setup/flutter_config.sh index 9cb53c7..a1b8c8d 100755 --- a/packages/veilid_support/example/dev-setup/flutter_config.sh +++ b/packages/veilid_support/example/dev-setup/flutter_config.sh @@ -14,13 +14,13 @@ sed -i '' 's/MACOSX_DEPLOYMENT_TARGET = [^;]*/MACOSX_DEPLOYMENT_TARGET = 10.14.6 sed -i '' "s/platform :osx, '[^']*'/platform :osx, '10.14.6'/g" $APPDIR/macos/Podfile # Android: Set NDK version -if [[ "$TMPDIR" != "" ]]; then +if [[ "$TMPDIR" != "" ]]; then ANDTMP=$TMPDIR/andtmp_$(date +%s) -else +else ANDTMP=/tmp/andtmp_$(date +%s) fi cat < $ANDTMP - ndkVersion '26.3.11579264' + ndkVersion '27.0.12077973' EOF sed -i '' -e "/android {/r $ANDTMP" $APPDIR/android/app/build.gradle rm -- $ANDTMP @@ -29,7 +29,7 @@ rm -- $ANDTMP sed -i '' 's/minSdkVersion .*/minSdkVersion Math.max(flutter.minSdkVersion, 24)/g' $APPDIR/android/app/build.gradle # Android: Set gradle plugin version -sed -i '' "s/classpath \'com.android.tools.build:gradle:[^\']*\'/classpath 'com.android.tools.build:gradle:7.2.0'/g" $APPDIR/android/build.gradle +sed -i '' "s/classpath \'com.android.tools.build:gradle:[^\']*\'/classpath 'com.android.tools.build:gradle:8.8.0'/g" $APPDIR/android/build.gradle # Android: Set gradle version -sed -i '' 's/distributionUrl=.*/distributionUrl=https:\/\/services.gradle.org\/distributions\/gradle-7.3.3-all.zip/g' $APPDIR/android/gradle/wrapper/gradle-wrapper.properties +sed -i '' 's/distributionUrl=.*/distributionUrl=https:\/\/services.gradle.org\/distributions\/gradle-8.10.2-all.zip/g' $APPDIR/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/veilid_support/example/pubspec.lock b/packages/veilid_support/example/pubspec.lock index 91b7eee..ade4030 100644 --- a/packages/veilid_support/example/pubspec.lock +++ b/packages/veilid_support/example/pubspec.lock @@ -5,23 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" url: "https://pub.dev" source: hosted - version: "72.0.0" + version: "76.0.0" _macros: dependency: transitive description: dart source: sdk - version: "0.3.2" + version: "0.3.3" analyzer: dependency: transitive description: name: analyzer - sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" url: "https://pub.dev" source: hosted - version: "6.7.0" + version: "6.11.0" args: dependency: transitive description: @@ -74,10 +74,10 @@ packages: dependency: transitive description: name: change_case - sha256: "99cfdf2018c627c8a3af5a23ea4c414eb69c75c31322d23b9660ebc3cf30b514" + sha256: e41ef3df58521194ef8d7649928954805aeb08061917cf658322305e61568003 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" characters: dependency: transitive description: @@ -90,10 +90,10 @@ packages: dependency: transitive description: name: charcode - sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.4.0" clock: dependency: transitive description: @@ -106,10 +106,10 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" convert: dependency: transitive description: @@ -122,10 +122,10 @@ packages: dependency: transitive description: name: coverage - sha256: "88b0fddbe4c92910fefc09cc0248f5e7f0cd23e450ded4c28f16ab8ee8f83268" + sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.1" crypto: dependency: transitive description: @@ -146,10 +146,10 @@ packages: dependency: transitive description: name: equatable - sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.7" fake_async: dependency: transitive description: @@ -235,10 +235,10 @@ packages: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" globbing: dependency: transitive description: @@ -251,18 +251,18 @@ packages: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" integration_test: dependency: "direct dev" description: flutter @@ -272,10 +272,10 @@ packages: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" js: dependency: transitive description: @@ -296,18 +296,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -320,10 +320,10 @@ packages: dependency: "direct dev" description: name: lint_hard - sha256: "44d15ec309b1a8e1aff99069df9dcb1597f49d5f588f32811ca28fb7b38c32fe" + sha256: "638d2cce6d3d5499826be71311d18cded797a51351eaa1aee7a35a2f0f9bc46e" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "5.0.0" logging: dependency: transitive description: @@ -344,10 +344,10 @@ packages: dependency: transitive description: name: macros - sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" url: "https://pub.dev" source: hosted - version: "0.1.2-main.4" + version: "0.1.3-main.0" matcher: dependency: transitive description: @@ -392,10 +392,10 @@ packages: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" path: dependency: transitive description: @@ -408,26 +408,26 @@ packages: dependency: transitive description: name: path_provider - sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" url: "https://pub.dev" source: hosted - version: "2.2.12" + version: "2.2.15" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -496,18 +496,18 @@ packages: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" shelf: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_packages_handler: dependency: transitive description: @@ -528,15 +528,15 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_map_stack_trace: dependency: transitive description: @@ -549,10 +549,10 @@ packages: dependency: transitive description: name: source_maps - sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" url: "https://pub.dev" source: hosted - version: "0.10.12" + version: "0.10.13" source_span: dependency: transitive description: @@ -565,10 +565,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -581,10 +581,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" sync_http: dependency: transitive description: @@ -605,10 +605,10 @@ packages: dependency: transitive description: name: system_info_plus - sha256: b915c811c6605b802f3988859bc2bb79c95f735762a75b5451741f7a2b949d1b + sha256: df94187e95527f9cb459e6a9f6e0b1ea20c157d8029bc233de34b3c1e17e1c48 url: "https://pub.dev" source: hosted - version: "0.0.5" + version: "0.0.6" term_glyph: dependency: transitive description: @@ -621,26 +621,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e" + sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" url: "https://pub.dev" source: hosted - version: "1.25.7" + version: "1.25.8" test_api: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.3" test_core: dependency: transitive description: name: test_core - sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696" + sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.6.5" typed_data: dependency: transitive description: @@ -663,7 +663,7 @@ packages: path: "../../../../veilid/veilid-flutter" relative: true source: path - version: "0.3.4" + version: "0.4.1" veilid_support: dependency: "direct main" description: @@ -682,18 +682,18 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.3.0" watcher: dependency: transitive description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web: dependency: transitive description: @@ -714,18 +714,18 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" webdriver: dependency: transitive description: name: webdriver - sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" + sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.4" webkit_inspection_protocol: dependency: transitive description: @@ -746,10 +746,10 @@ packages: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" sdks: dart: ">=3.5.0 <4.0.0" flutter: ">=3.24.0" diff --git a/packages/veilid_support/example/pubspec.yaml b/packages/veilid_support/example/pubspec.yaml index 17d17f4..c043a1b 100644 --- a/packages/veilid_support/example/pubspec.yaml +++ b/packages/veilid_support/example/pubspec.yaml @@ -17,7 +17,7 @@ dev_dependencies: async_tools: ^0.1.6 integration_test: sdk: flutter - lint_hard: ^4.0.0 + lint_hard: ^5.0.0 test: ^1.25.2 veilid_test: path: ../../../../veilid/veilid-flutter/packages/veilid_test diff --git a/packages/veilid_support/lib/src/veilid_log.dart b/packages/veilid_support/lib/src/veilid_log.dart index 0007754..3b35b5d 100644 --- a/packages/veilid_support/lib/src/veilid_log.dart +++ b/packages/veilid_support/lib/src/veilid_log.dart @@ -57,19 +57,14 @@ void processLog(VeilidLog log) { switch (log.logLevel) { case VeilidLogLevel.error: veilidLoggy.error(log.message, error, stackTrace); - break; case VeilidLogLevel.warn: veilidLoggy.warning(log.message, error, stackTrace); - break; case VeilidLogLevel.info: veilidLoggy.info(log.message, error, stackTrace); - break; case VeilidLogLevel.debug: veilidLoggy.debug(log.message, error, stackTrace); - break; case VeilidLogLevel.trace: veilidLoggy.trace(log.message, error, stackTrace); - break; } } diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index 4262a13..260c991 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -5,23 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 url: "https://pub.dev" source: hosted - version: "72.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.2" + version: "80.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" url: "https://pub.dev" source: hosted - version: "6.7.0" + version: "7.3.0" args: dependency: transitive description: @@ -34,10 +29,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" async_tools: dependency: "direct main" description: @@ -66,58 +61,58 @@ packages: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" build: dependency: transitive description: name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" build_config: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.4" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.4.15" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.3.2" + version: "8.0.0" built_collection: dependency: transitive description: @@ -130,18 +125,18 @@ packages: dependency: transitive description: name: built_value - sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2" url: "https://pub.dev" source: hosted - version: "8.9.2" + version: "8.9.3" change_case: dependency: transitive description: name: change_case - sha256: "99cfdf2018c627c8a3af5a23ea4c414eb69c75c31322d23b9660ebc3cf30b514" + sha256: e41ef3df58521194ef8d7649928954805aeb08061917cf658322305e61568003 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" characters: dependency: transitive description: @@ -154,10 +149,10 @@ packages: dependency: "direct main" description: name: charcode - sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -170,18 +165,18 @@ packages: dependency: transitive description: name: code_builder - sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.10.1" collection: dependency: "direct main" description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" convert: dependency: transitive description: @@ -194,10 +189,10 @@ packages: dependency: transitive description: name: coverage - sha256: "88b0fddbe4c92910fefc09cc0248f5e7f0cd23e450ded4c28f16ab8ee8f83268" + sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.1" crypto: dependency: transitive description: @@ -210,18 +205,18 @@ packages: dependency: transitive description: name: dart_style - sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "3.0.1" equatable: dependency: "direct main" description: name: equatable - sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.7" fast_immutable_collections: dependency: "direct main" description: @@ -268,10 +263,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" + sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c" url: "https://pub.dev" source: hosted - version: "2.5.7" + version: "2.5.8" freezed_annotation: dependency: "direct main" description: @@ -292,10 +287,10 @@ packages: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" globbing: dependency: transitive description: @@ -312,30 +307,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + http: + dependency: transitive + description: + name: http + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + url: "https://pub.dev" + source: hosted + version: "1.3.0" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" io: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" js: dependency: transitive description: @@ -356,18 +359,18 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + sha256: "81f04dee10969f89f604e1249382d46b97a1ccad53872875369622b5bfc9e58a" url: "https://pub.dev" source: hosted - version: "6.8.0" + version: "6.9.4" lint_hard: dependency: "direct dev" description: name: lint_hard - sha256: "44d15ec309b1a8e1aff99069df9dcb1597f49d5f588f32811ca28fb7b38c32fe" + sha256: "638d2cce6d3d5499826be71311d18cded797a51351eaa1aee7a35a2f0f9bc46e" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "5.0.0" logging: dependency: transitive description: @@ -384,22 +387,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" - macros: - dependency: transitive - description: - name: macros - sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" - url: "https://pub.dev" - source: hosted - version: "0.1.2-main.4" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -436,10 +431,10 @@ packages: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" path: dependency: "direct main" description: @@ -452,26 +447,26 @@ packages: dependency: "direct main" description: name: path_provider - sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" url: "https://pub.dev" source: hosted - version: "2.2.12" + version: "2.2.15" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -532,26 +527,26 @@ packages: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" shelf: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_packages_handler: dependency: transitive description: @@ -572,31 +567,31 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "3.0.0" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_gen: dependency: transitive description: name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "2.0.0" source_helper: dependency: transitive description: name: source_helper - sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" url: "https://pub.dev" source: hosted - version: "1.3.4" + version: "1.3.5" source_map_stack_trace: dependency: transitive description: @@ -609,50 +604,50 @@ packages: dependency: transitive description: name: source_maps - sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" url: "https://pub.dev" source: hosted - version: "0.10.12" + version: "0.10.13" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - sha256: "0bd04f5bb74fcd6ff0606a888a30e917af9bd52820b178eaa464beb11dca84b6" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" system_info2: dependency: transitive description: @@ -665,50 +660,50 @@ packages: dependency: transitive description: name: system_info_plus - sha256: b915c811c6605b802f3988859bc2bb79c95f735762a75b5451741f7a2b949d1b + sha256: df94187e95527f9cb459e6a9f6e0b1ea20c157d8029bc233de34b3c1e17e1c48 url: "https://pub.dev" source: hosted - version: "0.0.5" + version: "0.0.6" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test: dependency: "direct dev" description: name: test - sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" + sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e" url: "https://pub.dev" source: hosted - version: "1.25.8" + version: "1.25.15" test_api: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.4" test_core: dependency: transitive description: name: test_core - sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" + sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.8" timing: dependency: transitive description: name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" typed_data: dependency: transitive description: @@ -731,23 +726,23 @@ packages: path: "../../../veilid/veilid-flutter" relative: true source: path - version: "0.3.4" + version: "0.4.1" vm_service: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "15.0.0" watcher: dependency: transitive description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web: dependency: transitive description: @@ -768,10 +763,10 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" webkit_inspection_protocol: dependency: transitive description: @@ -792,10 +787,10 @@ packages: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.6.0 <4.0.0" flutter: ">=3.24.0" diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index 2634603..8ed1f58 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -36,5 +36,5 @@ dev_dependencies: build_runner: ^2.4.10 freezed: ^2.5.2 json_serializable: ^6.8.0 - lint_hard: ^4.0.0 + lint_hard: ^5.0.0 test: ^1.25.2 diff --git a/pubspec.lock b/pubspec.lock index 7e541f2..d6492c0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,15 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "03f6da266a27a4538a69295ec142cb5717d7d4e5727b84658b63e1e1509bac9c" + sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 url: "https://pub.dev" source: hosted - version: "79.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.3" + version: "80.0.0" accordion: dependency: "direct main" description: @@ -26,18 +21,18 @@ packages: dependency: transitive description: name: analyzer - sha256: c9040fc56483c22a5e04a9f6a251313118b1a3c42423770623128fa484115643 + sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" url: "https://pub.dev" source: hosted - version: "7.2.0" + version: "7.3.0" animated_bottom_navigation_bar: dependency: "direct main" description: name: animated_bottom_navigation_bar - sha256: "2b04a2ae4b0742669e60ddf309467d6a354cefd2d0cd20f4737b1efaf9834cda" + sha256: "94971fdfd53acd443acd0d17ce1cb5219ad833f20c75b50c55b205e54a5d6117" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.4.0" animated_switcher_transitions: dependency: "direct main" description: @@ -66,10 +61,10 @@ packages: dependency: "direct main" description: name: archive - sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + sha256: "6199c74e3db4fbfbd04f66d739e72fe11c8a8957d5f219f1f4482dbde6420b5a" url: "https://pub.dev" source: hosted - version: "3.6.1" + version: "4.0.2" args: dependency: transitive description: @@ -82,10 +77,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" async_tools: dependency: "direct main" description: @@ -98,10 +93,10 @@ packages: dependency: "direct main" description: name: awesome_extensions - sha256: "07e52221467e651cab9219a26286245760831c3852ea2c54883a48a54f120d7c" + sha256: "91dc128e8cf01fbd3d3567b8f1dd1e3183cbf9fd6b1850e8b0fafce9a7eee0da" url: "https://pub.dev" source: hosted - version: "2.0.16" + version: "2.0.20" badges: dependency: "direct main" description: @@ -115,9 +110,11 @@ packages: description: name: barcode sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4" + sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4" url: "https://pub.dev" source: hosted version: "2.2.9" + version: "2.2.9" basic_utils: dependency: "direct main" description: @@ -163,57 +160,67 @@ packages: description: name: boolean_selector sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted version: "2.1.2" + version: "2.1.2" build: dependency: transitive description: name: build sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 + sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 url: "https://pub.dev" source: hosted version: "2.4.2" + version: "2.4.2" build_config: dependency: transitive description: name: build_config sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted version: "1.1.2" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948" + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" url: "https://pub.dev" source: hosted - version: "4.0.3" + version: "4.0.4" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e" + sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.4.4" build_runner: dependency: "direct dev" description: name: build_runner sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" + sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" url: "https://pub.dev" source: hosted version: "2.4.14" + version: "2.4.14" build_runner_core: dependency: transitive description: name: build_runner_core sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted version: "8.0.0" + version: "8.0.0" built_collection: dependency: transitive description: @@ -227,9 +234,11 @@ packages: description: name: built_value sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2" + sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2" url: "https://pub.dev" source: hosted version: "8.9.3" + version: "8.9.3" cached_network_image: dependency: transitive description: @@ -258,34 +267,36 @@ packages: dependency: transitive description: name: camera - sha256: dfa8fc5a1adaeb95e7a54d86a5bd56f4bb0e035515354c8ac6d262e35cec2ec8 + sha256: "413d2b34fe28496c35c69ede5b232fb9dd5ca2c3a4cb606b14efc1c7546cc8cb" url: "https://pub.dev" source: hosted - version: "0.10.6" - camera_android: + version: "0.11.1" + camera_android_camerax: dependency: transitive description: - name: camera_android - sha256: "007c57cdcace4751014071e3d42f2eb8a64a519254abed35b714223d81d66234" + name: camera_android_camerax + sha256: "7cc6adf1868bdcf4e63a56b24b41692dfbad2bec1cdceea451c77798f6a605c3" url: "https://pub.dev" source: hosted - version: "0.10.10" + version: "0.6.13" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: "55eb9c216f25339a3faa55fc42826e2c4a45becefa1387fd50fce6ae9dd0c574" + sha256: "1eeb9ce7c9a397e312343fd7db337d95f35c3e65ad5a62ff637c8abce5102b98" url: "https://pub.dev" source: hosted - version: "0.9.18+1" + version: "0.9.18+8" camera_platform_interface: dependency: transitive description: name: camera_platform_interface sha256: "953e7baed3a7c8fae92f7200afeb2be503ff1a17c3b4e4ed7b76f008c2810a31" + sha256: "953e7baed3a7c8fae92f7200afeb2be503ff1a17c3b4e4ed7b76f008c2810a31" url: "https://pub.dev" source: hosted version: "2.9.0" + version: "2.9.0" camera_web: dependency: transitive description: @@ -299,9 +310,11 @@ packages: description: name: change_case sha256: e41ef3df58521194ef8d7649928954805aeb08061917cf658322305e61568003 + sha256: e41ef3df58521194ef8d7649928954805aeb08061917cf658322305e61568003 url: "https://pub.dev" source: hosted version: "2.2.0" + version: "2.2.0" characters: dependency: transitive description: @@ -315,9 +328,11 @@ packages: description: name: charcode sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a url: "https://pub.dev" source: hosted version: "1.4.0" + version: "1.4.0" charset: dependency: transitive description: @@ -363,17 +378,21 @@ packages: description: name: code_builder sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" url: "https://pub.dev" source: hosted version: "4.10.1" + version: "4.10.1" collection: dependency: transitive description: name: collection sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted version: "1.19.0" + version: "1.19.0" convert: dependency: transitive description: @@ -411,9 +430,11 @@ packages: description: name: csslib sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" url: "https://pub.dev" source: hosted version: "1.0.2" + version: "1.0.2" cupertino_icons: dependency: "direct main" description: @@ -427,9 +448,11 @@ packages: description: name: dart_style sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac" + sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac" url: "https://pub.dev" source: hosted version: "3.0.1" + version: "3.0.1" diffutil_dart: dependency: transitive description: @@ -443,33 +466,39 @@ packages: description: name: dio sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" url: "https://pub.dev" source: hosted version: "5.8.0+1" + version: "5.8.0+1" dio_web_adapter: dependency: transitive description: name: dio_web_adapter sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a + sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a url: "https://pub.dev" source: hosted version: "2.1.0" + version: "2.1.0" equatable: dependency: "direct main" description: name: equatable sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" url: "https://pub.dev" source: hosted version: "2.0.7" + version: "2.0.7" expansion_tile_group: dependency: "direct main" description: name: expansion_tile_group - sha256: "47615665d4e610dee0b6362de9e81003b56b150b5765ea5444a091762b5dc7d5" + sha256: "3be10b81d6d99d1213fe76a285993be0ea6092565ac100152deb6cdf9f5521dc" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "2.2.0" fast_immutable_collections: dependency: "direct main" description: @@ -498,10 +527,10 @@ packages: dependency: "direct main" description: name: file_saver - sha256: d375b351e3331663abbaf99747abd72f159260c58fbbdbca9f926f02c01bdc48 + sha256: "017a127de686af2d2fbbd64afea97052d95f2a0f87d19d25b87e097407bf9c1e" url: "https://pub.dev" source: hosted - version: "0.2.13" + version: "0.2.14" fixnum: dependency: "direct main" description: @@ -520,9 +549,11 @@ packages: description: name: flutter_animate sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" + sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" url: "https://pub.dev" source: hosted version: "4.5.2" + version: "4.5.2" flutter_bloc: dependency: "direct main" description: @@ -561,9 +592,11 @@ packages: description: name: flutter_form_builder sha256: "375da52998c72f80dec9187bd93afa7ab202b89d5d066699368ff96d39fd4876" + sha256: "375da52998c72f80dec9187bd93afa7ab202b89d5d066699368ff96d39fd4876" url: "https://pub.dev" source: hosted version: "9.7.0" + version: "9.7.0" flutter_hooks: dependency: "direct main" description: @@ -598,9 +631,11 @@ packages: description: name: flutter_native_splash sha256: "7062602e0dbd29141fb8eb19220b5871ca650be5197ab9c1f193a28b17537bc7" + sha256: "7062602e0dbd29141fb8eb19220b5871ca650be5197ab9c1f193a28b17537bc7" url: "https://pub.dev" source: hosted version: "2.4.4" + version: "2.4.4" flutter_parsed_text: dependency: transitive description: @@ -614,9 +649,11 @@ packages: description: name: flutter_plugin_android_lifecycle sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e" + sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e" url: "https://pub.dev" source: hosted version: "2.0.24" + version: "2.0.24" flutter_shaders: dependency: transitive description: @@ -629,10 +666,10 @@ packages: dependency: "direct main" description: name: flutter_slidable - sha256: a857de7ea701f276fd6a6c4c67ae885b60729a3449e42766bb0e655171042801 + sha256: ab7dbb16f783307c9d7762ede2593ce32c220ba2ba0fd540a3db8e9a3acba71a url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "4.0.0" flutter_spinkit: dependency: "direct main" description: @@ -646,17 +683,21 @@ packages: description: name: flutter_sticky_header sha256: "7f76d24d119424ca0c95c146b8627a457e8de8169b0d584f766c2c545db8f8be" + sha256: "7f76d24d119424ca0c95c146b8627a457e8de8169b0d584f766c2c545db8f8be" url: "https://pub.dev" source: hosted version: "0.7.0" + version: "0.7.0" flutter_svg: dependency: "direct main" description: name: flutter_svg sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b + sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b url: "https://pub.dev" source: hosted version: "2.0.17" + version: "2.0.17" flutter_translate: dependency: "direct main" description: @@ -683,17 +724,21 @@ packages: description: name: form_builder_validators sha256: "517fb884183fff7a0ef3db7d375981011da26ee452f20fb3d2e788ad527ad01d" + sha256: "517fb884183fff7a0ef3db7d375981011da26ee452f20fb3d2e788ad527ad01d" url: "https://pub.dev" source: hosted version: "11.1.1" + version: "11.1.1" freezed: dependency: "direct dev" description: name: freezed sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c" + sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c" url: "https://pub.dev" source: hosted version: "2.5.8" + version: "2.5.8" freezed_annotation: dependency: "direct main" description: @@ -714,18 +759,20 @@ packages: dependency: transitive description: name: get - sha256: e4e7335ede17452b391ed3b2ede016545706c01a02292a6c97619705e7d2a85e + sha256: c79eeb4339f1f3deffd9ec912f8a923834bec55f7b49c9e882b8fef2c139d425 url: "https://pub.dev" source: hosted - version: "4.6.6" + version: "4.7.2" glob: dependency: transitive description: name: glob sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted version: "2.1.3" + version: "2.1.3" globbing: dependency: transitive description: @@ -738,10 +785,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "9b736a9fa879d8ad6df7932cbdcc58237c173ab004ef90d8377923d7ad731eaa" + sha256: "04539267a740931c6d4479a10d466717ca5901c6fdfd3fcda09391bbb8ebd651" url: "https://pub.dev" source: hosted - version: "14.7.2" + version: "14.8.0" graphs: dependency: transitive description: @@ -763,33 +810,41 @@ packages: description: name: html sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" + sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" url: "https://pub.dev" source: hosted version: "0.15.5" + version: "0.15.5" http: dependency: transitive description: name: http sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f url: "https://pub.dev" source: hosted version: "1.3.0" + version: "1.3.0" http_multi_server: dependency: transitive description: name: http_multi_server sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted version: "3.2.2" + version: "3.2.2" http_parser: dependency: transitive description: name: http_parser sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted version: "4.1.2" + version: "4.1.2" hydrated_bloc: dependency: "direct main" description: @@ -802,18 +857,18 @@ packages: dependency: "direct dev" description: name: icons_launcher - sha256: "9b514ffed6ed69b232fd2bf34c44878c8526be71fc74129a658f35c04c9d4a9d" + sha256: a7c83fbc837dc6f81944ef35c3756f533bb2aba32fcca5cbcdb2dbcd877d5ae9 url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "3.0.0" image: dependency: "direct main" description: name: image - sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d + sha256: "8346ad4b5173924b5ddddab782fc7d8a6300178c8b1dc427775405a01701c4a6" url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "4.5.2" intl: dependency: "direct main" description: @@ -827,9 +882,11 @@ packages: description: name: io sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted version: "1.0.5" + version: "1.0.5" js: dependency: transitive description: @@ -850,10 +907,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: b0a98230538fe5d0b60a22fb6bf1b6cb03471b53e3324ff6069c591679dd59c9 + sha256: "81f04dee10969f89f604e1249382d46b97a1ccad53872875369622b5bfc9e58a" url: "https://pub.dev" source: hosted - version: "6.9.3" + version: "6.9.4" linkify: dependency: transitive description: @@ -866,10 +923,10 @@ packages: dependency: "direct dev" description: name: lint_hard - sha256: "44d15ec309b1a8e1aff99069df9dcb1597f49d5f588f32811ca28fb7b38c32fe" + sha256: "638d2cce6d3d5499826be71311d18cded797a51351eaa1aee7a35a2f0f9bc46e" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "5.0.0" logging: dependency: transitive description: @@ -886,14 +943,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" - macros: - dependency: transitive - description: - name: macros - sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" - url: "https://pub.dev" - source: hosted - version: "0.1.3-main.0" matcher: dependency: transitive description: @@ -930,10 +979,10 @@ packages: dependency: "direct main" description: name: mobile_scanner - sha256: d234581c090526676fd8fab4ada92f35c6746e3fb4f05a399665d75a399fb760 + sha256: "91d28b825784e15572fdc39165c5733099ce0e69c6f6f0964ebdbf98a62130fd" url: "https://pub.dev" source: hosted - version: "5.2.3" + version: "6.0.6" motion_toast: dependency: "direct main" description: @@ -971,33 +1020,35 @@ packages: description: name: package_config sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" url: "https://pub.dev" source: hosted version: "2.1.1" + version: "2.1.1" package_info_plus: dependency: "direct main" description: name: package_info_plus - sha256: b15fad91c4d3d1f2b48c053dd41cb82da007c27407dc9ab5f9aa59881d0e39d4 + sha256: "67eae327b1b0faf761964a1d2e5d323c797f3799db0e85aa232db8d9e922bc35" url: "https://pub.dev" source: hosted - version: "8.1.4" + version: "8.2.1" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: a5ef9986efc7bf772f2696183a3992615baa76c1ffb1189318dd8803778fb05b + sha256: "205ec83335c2ab9107bbba3f8997f9356d72ca3c715d2f038fc773d0366b4c76" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.1.0" pasteboard: dependency: "direct main" description: name: pasteboard - sha256: "1c8b6a8b3f1d12e55d4e9404433cda1b4abe66db6b17bc2d2fb5965772c04674" + sha256: "7bf733f3a00c7188ec1f2c6f0612854248b302cf91ef3611a2b7bb141c0f9d55" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.3.0" path: dependency: "direct main" description: @@ -1011,33 +1062,41 @@ packages: description: name: path_parsing sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" url: "https://pub.dev" source: hosted version: "1.1.0" + version: "1.1.0" path_provider: dependency: "direct main" description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted version: "2.1.5" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" url: "https://pub.dev" source: hosted version: "2.2.15" + version: "2.2.15" path_provider_foundation: dependency: transitive description: name: path_provider_foundation sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted version: "2.4.1" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -1066,10 +1125,10 @@ packages: dependency: "direct main" description: name: pdf - sha256: adbdec5bc84d20e6c8d67f9c64270aa64d1e9e1ed529f0fef7e7bc7e9400f894 + sha256: "28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416" url: "https://pub.dev" source: hosted - version: "3.11.2" + version: "3.11.3" pdf_widget_wrapper: dependency: transitive description: @@ -1098,10 +1157,10 @@ packages: dependency: "direct main" description: name: pinput - sha256: "6d571e38a484f7515a52e89024ef416f11fa6171ac6f32303701374ab9890efa" + sha256: "8a73be426a91fefec90a7f130763ca39772d547e92f19a827cf4aa02e323d35a" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "5.0.1" platform: dependency: transitive description: @@ -1134,6 +1193,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" + posix: + dependency: transitive + description: + name: posix + sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a + url: "https://pub.dev" + source: hosted + version: "6.0.1" preload_page_view: dependency: "direct main" description: @@ -1146,10 +1213,10 @@ packages: dependency: "direct main" description: name: printing - sha256: cc4b256a5a89d5345488e3318897b595867f5181b8c5ed6fc63bfa5f2044aec3 + sha256: "482cd5a5196008f984bb43ed0e47cbfdca7373490b62f3b27b3299275bf22a93" url: "https://pub.dev" source: hosted - version: "5.13.1" + version: "5.14.2" protobuf: dependency: "direct main" description: @@ -1171,17 +1238,21 @@ packages: description: name: pub_semver sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" + sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" url: "https://pub.dev" source: hosted version: "2.1.5" + version: "2.1.5" pubspec_parse: dependency: transitive description: name: pubspec_parse sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted version: "1.5.0" + version: "1.5.0" qr: dependency: transitive description: @@ -1194,10 +1265,10 @@ packages: dependency: "direct main" description: name: qr_code_dart_scan - sha256: d5511d137f1ca5cb217fe79fa992616e0361a505a74b1e34499e68040a68b0c3 + sha256: a21340c4a2ca14e2e114915940fcad166f15c1a065fed8b4fede4a4aba5bc4ff url: "https://pub.dev" source: hosted - version: "0.8.3" + version: "0.9.11" qr_flutter: dependency: "direct main" description: @@ -1250,10 +1321,42 @@ packages: dependency: transitive description: name: screen_retriever - sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90" + sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" url: "https://pub.dev" source: hosted - version: "0.1.9" + version: "0.2.0" + screen_retriever_linux: + dependency: transitive + description: + name: screen_retriever_linux + sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_macos: + dependency: transitive + description: + name: screen_retriever_macos + sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_platform_interface: + dependency: transitive + description: + name: screen_retriever_platform_interface + sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_windows: + dependency: transitive + description: + name: screen_retriever_windows + sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13" + url: "https://pub.dev" + source: hosted + version: "0.2.0" screenshot: dependency: "direct main" description: @@ -1283,42 +1386,44 @@ packages: dependency: "direct main" description: name: share_plus - sha256: ef3489a969683c4f3d0239010cc8b7a2a46543a8d139e111c06c558875083544 + sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "10.1.4" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "0f9e4418835d1b2c3ae78fdb918251959106cefdbc4dd43526e182f80e82f6d4" + sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "5.0.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "688ee90fbfb6989c980254a56cb26ebe9bb30a3a2dff439a78894211f73de67a" + sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" url: "https://pub.dev" source: hosted - version: "2.5.1" + version: "2.5.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "650584dcc0a39856f369782874e562efd002a9c94aec032412c9eb81419cce1f" + sha256: ea86be7b7114f9e94fddfbb52649e59a03d6627ccd2387ebddcd6624719e9f16 url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.4.5" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted version: "2.5.4" + version: "2.5.4" shared_preferences_linux: dependency: transitive description: @@ -1339,10 +1444,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" shared_preferences_windows: dependency: transitive description: @@ -1356,17 +1461,21 @@ packages: description: name: shelf sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted version: "1.4.2" + version: "1.4.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted version: "2.0.1" + version: "2.0.1" signal_strength_indicator: dependency: "direct main" description: @@ -1380,6 +1489,7 @@ packages: description: flutter source: sdk version: "0.0.0" + version: "0.0.0" sliver_expandable: dependency: "direct main" description: @@ -1404,14 +1514,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.12" - smart_auth: - dependency: transitive - description: - name: smart_auth - sha256: "88aa8fe66e951c78a307f26d1c29672dce2e9eb3da2e12e853864d0e615a73ad" - url: "https://pub.dev" - source: hosted - version: "2.0.0" sorted_list: dependency: "direct main" description: @@ -1426,25 +1528,31 @@ packages: description: name: source_gen sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" url: "https://pub.dev" source: hosted version: "2.0.0" + version: "2.0.0" source_helper: dependency: transitive description: name: source_helper sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" url: "https://pub.dev" source: hosted version: "1.3.5" + version: "1.3.5" source_span: dependency: transitive description: name: source_span sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted version: "1.10.1" + version: "1.10.1" split_view: dependency: "direct main" description: @@ -1466,9 +1574,11 @@ packages: description: name: sqflite sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" + sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" url: "https://pub.dev" source: hosted version: "2.4.1" + version: "2.4.1" sqflite_android: dependency: transitive description: @@ -1482,17 +1592,21 @@ packages: description: name: sqflite_common sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" + sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" url: "https://pub.dev" source: hosted version: "2.5.4+6" + version: "2.5.4+6" sqflite_darwin: dependency: transitive description: name: sqflite_darwin sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c" + sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c" url: "https://pub.dev" source: hosted version: "2.4.1+1" + version: "2.4.1+1" sqflite_platform_interface: dependency: transitive description: @@ -1506,9 +1620,11 @@ packages: description: name: stack_trace sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted version: "1.12.1" + version: "1.12.1" star_menu: dependency: "direct main" description: @@ -1522,25 +1638,31 @@ packages: description: name: stream_channel sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted version: "2.1.4" + version: "2.1.4" stream_transform: dependency: "direct main" description: name: stream_transform sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted version: "2.1.1" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted version: "1.4.1" + version: "1.4.1" synchronized: dependency: transitive description: @@ -1561,34 +1683,40 @@ packages: dependency: transitive description: name: system_info_plus - sha256: b915c811c6605b802f3988859bc2bb79c95f735762a75b5451741f7a2b949d1b + sha256: df94187e95527f9cb459e6a9f6e0b1ea20c157d8029bc233de34b3c1e17e1c48 url: "https://pub.dev" source: hosted - version: "0.0.5" + version: "0.0.6" term_glyph: dependency: transitive description: name: term_glyph sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted version: "1.2.2" + version: "1.2.2" test_api: dependency: transitive description: name: test_api sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted version: "0.7.4" + version: "0.7.4" timing: dependency: transitive description: name: timing sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted version: "1.0.2" + version: "1.0.2" transitioned_indexed_stack: dependency: "direct main" description: @@ -1634,33 +1762,41 @@ packages: description: name: url_launcher_android sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" + sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" url: "https://pub.dev" source: hosted version: "6.3.14" + version: "6.3.14" url_launcher_ios: dependency: transitive description: name: url_launcher_ios sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" + sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" url: "https://pub.dev" source: hosted version: "6.3.2" + version: "6.3.2" url_launcher_linux: dependency: transitive description: name: url_launcher_linux sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted version: "3.2.1" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted version: "3.2.2" + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: @@ -1674,17 +1810,21 @@ packages: description: name: url_launcher_web sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" + sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" url: "https://pub.dev" source: hosted version: "2.4.0" + version: "2.4.0" url_launcher_windows: dependency: transitive description: name: url_launcher_windows sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted version: "3.1.4" + version: "3.1.4" uuid: dependency: "direct main" description: @@ -1698,33 +1838,39 @@ packages: description: name: value_layout_builder sha256: c02511ea91ca5c643b514a33a38fa52536f74aa939ec367d02938b5ede6807fa + sha256: c02511ea91ca5c643b514a33a38fa52536f74aa939ec367d02938b5ede6807fa url: "https://pub.dev" source: hosted version: "0.4.0" + version: "0.4.0" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "7ed22c21d7fdcc88dd6ba7860384af438cd220b251ad65dfc142ab722fabef61" + sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" url: "https://pub.dev" source: hosted - version: "1.1.16" + version: "1.1.18" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" url: "https://pub.dev" source: hosted version: "1.1.13" + version: "1.1.13" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" + sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" url: "https://pub.dev" source: hosted version: "1.1.16" + version: "1.1.16" vector_math: dependency: transitive description: @@ -1740,6 +1886,7 @@ packages: relative: true source: path version: "0.4.1" + version: "0.4.1" veilid_support: dependency: "direct main" description: @@ -1760,17 +1907,19 @@ packages: description: name: watcher sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" url: "https://pub.dev" source: hosted version: "1.1.1" + version: "1.1.1" web: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "1.1.0" web_socket: dependency: transitive description: @@ -1784,25 +1933,29 @@ packages: description: name: web_socket_channel sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" + sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" url: "https://pub.dev" source: hosted version: "3.0.2" + version: "3.0.2" win32: dependency: transitive description: name: win32 sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e + sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e url: "https://pub.dev" source: hosted version: "5.10.1" + version: "5.10.1" window_manager: dependency: "direct main" description: name: window_manager - sha256: "8699323b30da4cdbe2aa2e7c9de567a6abd8a97d9a5c850a3c86dcd0b34bbfbf" + sha256: "732896e1416297c63c9e3fb95aea72d0355f61390263982a47fd519169dc5059" url: "https://pub.dev" source: hosted - version: "0.3.9" + version: "0.4.3" xdg_directories: dependency: transitive description: @@ -1832,9 +1985,11 @@ packages: description: name: yaml sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted version: "3.1.3" + version: "3.1.3" zmodem: dependency: transitive description: @@ -1862,3 +2017,5 @@ packages: sdks: dart: ">=3.6.0 <4.0.0" flutter: ">=3.27.0" + dart: ">=3.6.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index 6027eee..5bc5e26 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: animated_switcher_transitions: ^1.0.0 animated_theme_switcher: ^2.0.10 ansicolor: ^2.0.2 - archive: ^3.6.1 + archive: ^4.0.2 async_tools: ^0.1.7 awesome_extensions: ^2.0.16 badges: ^3.1.2 @@ -28,7 +28,7 @@ dependencies: cool_dropdown: ^2.1.0 cupertino_icons: ^1.0.8 equatable: ^2.0.5 - expansion_tile_group: ^1.2.4 + expansion_tile_group: ^2.2.0 fast_immutable_collections: ^10.2.4 file_saver: ^0.2.13 fixnum: ^1.1.0 @@ -46,7 +46,7 @@ dependencies: flutter_localizations: sdk: flutter flutter_native_splash: ^2.4.0 - flutter_slidable: ^3.1.0 + flutter_slidable: ^4.0.0 flutter_spinkit: ^5.2.1 flutter_sticky_header: ^0.7.0 flutter_svg: ^2.0.10+1 @@ -61,20 +61,20 @@ dependencies: json_annotation: ^4.9.0 loggy: ^2.0.3 meta: ^1.12.0 - mobile_scanner: ^5.1.1 + mobile_scanner: ^6.0.6 motion_toast: ^2.10.0 native_device_orientation: ^2.0.3 package_info_plus: ^8.0.0 - pasteboard: ^0.2.0 + pasteboard: ^0.3.0 path: ^1.9.0 path_provider: ^2.1.3 pdf: ^3.11.0 - pinput: ^4.0.0 + pinput: ^5.0.1 preload_page_view: ^0.2.0 printing: ^5.13.1 protobuf: ^3.1.0 provider: ^6.1.2 - qr_code_dart_scan: ^0.8.0 + qr_code_dart_scan: ^0.9.11 qr_flutter: ^4.1.0 radix_colors: ^1.0.4 reorderable_grid: ^1.0.10 @@ -85,7 +85,7 @@ dependencies: git: url: https://gitlab.com/veilid/Searchable-Listview.git ref: main - share_plus: ^9.0.0 + share_plus: ^10.1.4 shared_preferences: ^2.2.3 signal_strength_indicator: ^0.4.1 sliver_expandable: ^1.1.1 @@ -107,7 +107,7 @@ dependencies: path: ../veilid/veilid-flutter veilid_support: path: packages/veilid_support - window_manager: ^0.3.9 + window_manager: ^0.4.3 xterm: ^4.0.0 zxing2: ^0.2.3 @@ -124,9 +124,9 @@ dependencies: dev_dependencies: build_runner: ^2.4.11 freezed: ^2.5.2 - icons_launcher: ^2.1.7 + icons_launcher: ^3.0.0 json_serializable: ^6.8.0 - lint_hard: ^4.0.0 + lint_hard: ^5.0.0 flutter_native_splash: color: "#8588D0" diff --git a/web/index.html b/web/index.html index dfd1680..54ea8ab 100644 --- a/web/index.html +++ b/web/index.html @@ -1,3 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + VeilidChat + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -43,10 +138,15 @@ - + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 041a716..bb1eee2 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -9,9 +9,8 @@ #include #include #include -#include +#include #include -#include #include #include #include @@ -23,12 +22,10 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("PasteboardPlugin")); PrintingPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PrintingPlugin")); - ScreenRetrieverPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); + ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); SharePlusWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); - SmartAuthPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("SmartAuthPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); VeilidPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 17a8144..6cba61e 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -6,9 +6,8 @@ list(APPEND FLUTTER_PLUGIN_LIST file_saver pasteboard printing - screen_retriever + screen_retriever_windows share_plus - smart_auth url_launcher_windows veilid window_manager From 892fdffcf1b47ff341296227ec9eacfc6d765713 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 18 Feb 2025 21:46:08 -0500 Subject: [PATCH 206/270] merge conflicts --- pubspec.lock | 166 +++++---------------------------------------------- 1 file changed, 14 insertions(+), 152 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index d6492c0..9555644 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -110,11 +110,9 @@ packages: description: name: barcode sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4" - sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4" url: "https://pub.dev" source: hosted version: "2.2.9" - version: "2.2.9" basic_utils: dependency: "direct main" description: @@ -160,31 +158,25 @@ packages: description: name: boolean_selector sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted version: "2.1.2" - version: "2.1.2" build: dependency: transitive description: name: build sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 - sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 url: "https://pub.dev" source: hosted version: "2.4.2" - version: "2.4.2" build_config: dependency: transitive description: name: build_config sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" - sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted version: "1.1.2" - version: "1.1.2" build_daemon: dependency: transitive description: @@ -205,22 +197,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" - sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" + sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" url: "https://pub.dev" source: hosted - version: "2.4.14" - version: "2.4.14" + version: "2.4.15" build_runner_core: dependency: transitive description: name: build_runner_core sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" - sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted version: "8.0.0" - version: "8.0.0" built_collection: dependency: transitive description: @@ -234,19 +222,17 @@ packages: description: name: built_value sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2" - sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2" url: "https://pub.dev" source: hosted version: "8.9.3" - version: "8.9.3" cached_network_image: dependency: transitive description: name: cached_network_image - sha256: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819" + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "3.4.1" cached_network_image_platform_interface: dependency: transitive description: @@ -259,10 +245,10 @@ packages: dependency: transitive description: name: cached_network_image_web - sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996" + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.1" camera: dependency: transitive description: @@ -292,11 +278,9 @@ packages: description: name: camera_platform_interface sha256: "953e7baed3a7c8fae92f7200afeb2be503ff1a17c3b4e4ed7b76f008c2810a31" - sha256: "953e7baed3a7c8fae92f7200afeb2be503ff1a17c3b4e4ed7b76f008c2810a31" url: "https://pub.dev" source: hosted version: "2.9.0" - version: "2.9.0" camera_web: dependency: transitive description: @@ -310,11 +294,9 @@ packages: description: name: change_case sha256: e41ef3df58521194ef8d7649928954805aeb08061917cf658322305e61568003 - sha256: e41ef3df58521194ef8d7649928954805aeb08061917cf658322305e61568003 url: "https://pub.dev" source: hosted version: "2.2.0" - version: "2.2.0" characters: dependency: transitive description: @@ -328,11 +310,9 @@ packages: description: name: charcode sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a - sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a url: "https://pub.dev" source: hosted version: "1.4.0" - version: "1.4.0" charset: dependency: transitive description: @@ -378,21 +358,17 @@ packages: description: name: code_builder sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" - sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" url: "https://pub.dev" source: hosted version: "4.10.1" - version: "4.10.1" collection: dependency: transitive description: name: collection sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted version: "1.19.0" - version: "1.19.0" convert: dependency: transitive description: @@ -430,11 +406,9 @@ packages: description: name: csslib sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" - sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" url: "https://pub.dev" source: hosted version: "1.0.2" - version: "1.0.2" cupertino_icons: dependency: "direct main" description: @@ -448,11 +422,9 @@ packages: description: name: dart_style sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac" - sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac" url: "https://pub.dev" source: hosted version: "3.0.1" - version: "3.0.1" diffutil_dart: dependency: transitive description: @@ -466,31 +438,25 @@ packages: description: name: dio sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" - sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" url: "https://pub.dev" source: hosted version: "5.8.0+1" - version: "5.8.0+1" dio_web_adapter: dependency: transitive description: name: dio_web_adapter sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a - sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a url: "https://pub.dev" source: hosted version: "2.1.0" - version: "2.1.0" equatable: dependency: "direct main" description: name: equatable sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" - sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" url: "https://pub.dev" source: hosted version: "2.0.7" - version: "2.0.7" expansion_tile_group: dependency: "direct main" description: @@ -549,11 +515,9 @@ packages: description: name: flutter_animate sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" - sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" url: "https://pub.dev" source: hosted version: "4.5.2" - version: "4.5.2" flutter_bloc: dependency: "direct main" description: @@ -592,11 +556,9 @@ packages: description: name: flutter_form_builder sha256: "375da52998c72f80dec9187bd93afa7ab202b89d5d066699368ff96d39fd4876" - sha256: "375da52998c72f80dec9187bd93afa7ab202b89d5d066699368ff96d39fd4876" url: "https://pub.dev" source: hosted version: "9.7.0" - version: "9.7.0" flutter_hooks: dependency: "direct main" description: @@ -631,11 +593,9 @@ packages: description: name: flutter_native_splash sha256: "7062602e0dbd29141fb8eb19220b5871ca650be5197ab9c1f193a28b17537bc7" - sha256: "7062602e0dbd29141fb8eb19220b5871ca650be5197ab9c1f193a28b17537bc7" url: "https://pub.dev" source: hosted version: "2.4.4" - version: "2.4.4" flutter_parsed_text: dependency: transitive description: @@ -649,11 +609,9 @@ packages: description: name: flutter_plugin_android_lifecycle sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e" - sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e" url: "https://pub.dev" source: hosted version: "2.0.24" - version: "2.0.24" flutter_shaders: dependency: transitive description: @@ -683,21 +641,17 @@ packages: description: name: flutter_sticky_header sha256: "7f76d24d119424ca0c95c146b8627a457e8de8169b0d584f766c2c545db8f8be" - sha256: "7f76d24d119424ca0c95c146b8627a457e8de8169b0d584f766c2c545db8f8be" url: "https://pub.dev" source: hosted version: "0.7.0" - version: "0.7.0" flutter_svg: dependency: "direct main" description: name: flutter_svg sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b - sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b url: "https://pub.dev" source: hosted version: "2.0.17" - version: "2.0.17" flutter_translate: dependency: "direct main" description: @@ -724,21 +678,17 @@ packages: description: name: form_builder_validators sha256: "517fb884183fff7a0ef3db7d375981011da26ee452f20fb3d2e788ad527ad01d" - sha256: "517fb884183fff7a0ef3db7d375981011da26ee452f20fb3d2e788ad527ad01d" url: "https://pub.dev" source: hosted version: "11.1.1" - version: "11.1.1" freezed: dependency: "direct dev" description: name: freezed sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c" - sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c" url: "https://pub.dev" source: hosted version: "2.5.8" - version: "2.5.8" freezed_annotation: dependency: "direct main" description: @@ -768,11 +718,9 @@ packages: description: name: glob sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted version: "2.1.3" - version: "2.1.3" globbing: dependency: transitive description: @@ -810,41 +758,33 @@ packages: description: name: html sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" - sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" url: "https://pub.dev" source: hosted version: "0.15.5" - version: "0.15.5" http: dependency: transitive description: name: http sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f - sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f url: "https://pub.dev" source: hosted version: "1.3.0" - version: "1.3.0" http_multi_server: dependency: transitive description: name: http_multi_server sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 - sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted version: "3.2.2" - version: "3.2.2" http_parser: dependency: transitive description: name: http_parser sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted version: "4.1.2" - version: "4.1.2" hydrated_bloc: dependency: "direct main" description: @@ -882,19 +822,17 @@ packages: description: name: io sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b - sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted version: "1.0.5" - version: "1.0.5" js: 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: @@ -971,10 +909,10 @@ packages: dependency: transitive description: name: mime - sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "2.0.0" mobile_scanner: dependency: "direct main" description: @@ -1020,11 +958,9 @@ packages: description: name: package_config sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" - sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" url: "https://pub.dev" source: hosted version: "2.1.1" - version: "2.1.1" package_info_plus: dependency: "direct main" description: @@ -1062,41 +998,33 @@ packages: description: name: path_parsing sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" - sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" url: "https://pub.dev" source: hosted version: "1.1.0" - version: "1.1.0" path_provider: dependency: "direct main" description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted version: "2.1.5" - version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" - sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" url: "https://pub.dev" source: hosted version: "2.2.15" - version: "2.2.15" path_provider_foundation: dependency: transitive description: name: path_provider_foundation sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" - sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted version: "2.4.1" - version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -1238,21 +1166,17 @@ packages: description: name: pub_semver sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" - sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" url: "https://pub.dev" source: hosted version: "2.1.5" - version: "2.1.5" pubspec_parse: dependency: transitive description: name: pubspec_parse sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" - sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted version: "1.5.0" - version: "1.5.0" qr: dependency: transitive description: @@ -1410,20 +1334,18 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: ea86be7b7114f9e94fddfbb52649e59a03d6627ccd2387ebddcd6624719e9f16 + sha256: a768fc8ede5f0c8e6150476e14f38e2417c0864ca36bb4582be8e21925a03c22 url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.4.6" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" - sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted version: "2.5.4" - version: "2.5.4" shared_preferences_linux: dependency: transitive description: @@ -1461,21 +1383,17 @@ packages: description: name: shelf sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 - sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted version: "1.4.2" - version: "1.4.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 - sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.dev" source: hosted - version: "2.0.1" - version: "2.0.1" + version: "3.0.0" signal_strength_indicator: dependency: "direct main" description: @@ -1489,7 +1407,6 @@ packages: description: flutter source: sdk version: "0.0.0" - version: "0.0.0" sliver_expandable: dependency: "direct main" description: @@ -1528,31 +1445,25 @@ packages: description: name: source_gen sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" - sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" url: "https://pub.dev" source: hosted version: "2.0.0" - version: "2.0.0" source_helper: dependency: transitive description: name: source_helper sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" - sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" url: "https://pub.dev" source: hosted version: "1.3.5" - version: "1.3.5" source_span: dependency: transitive description: name: source_span sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted version: "1.10.1" - version: "1.10.1" split_view: dependency: "direct main" description: @@ -1574,11 +1485,9 @@ packages: description: name: sqflite sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" - sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" url: "https://pub.dev" source: hosted version: "2.4.1" - version: "2.4.1" sqflite_android: dependency: transitive description: @@ -1592,21 +1501,17 @@ packages: description: name: sqflite_common sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" - sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" url: "https://pub.dev" source: hosted version: "2.5.4+6" - version: "2.5.4+6" sqflite_darwin: dependency: transitive description: name: sqflite_darwin sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c" - sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c" url: "https://pub.dev" source: hosted version: "2.4.1+1" - version: "2.4.1+1" sqflite_platform_interface: dependency: transitive description: @@ -1620,11 +1525,9 @@ packages: description: name: stack_trace sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted version: "1.12.1" - version: "1.12.1" star_menu: dependency: "direct main" description: @@ -1638,31 +1541,25 @@ packages: description: name: stream_channel sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted version: "2.1.4" - version: "2.1.4" stream_transform: dependency: "direct main" description: name: stream_transform sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 - sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted version: "2.1.1" - version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted version: "1.4.1" - version: "1.4.1" synchronized: dependency: transitive description: @@ -1692,31 +1589,25 @@ packages: description: name: term_glyph sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted version: "1.2.2" - version: "1.2.2" test_api: dependency: transitive description: name: test_api sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted version: "0.7.4" - version: "0.7.4" timing: dependency: transitive description: name: timing sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" - sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted version: "1.0.2" - version: "1.0.2" transitioned_indexed_stack: dependency: "direct main" description: @@ -1762,41 +1653,33 @@ packages: description: name: url_launcher_android sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" - sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" url: "https://pub.dev" source: hosted version: "6.3.14" - version: "6.3.14" url_launcher_ios: dependency: transitive description: name: url_launcher_ios sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" - sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" url: "https://pub.dev" source: hosted version: "6.3.2" - version: "6.3.2" url_launcher_linux: dependency: transitive description: name: url_launcher_linux sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" - sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted version: "3.2.1" - version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" - sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted version: "3.2.2" - version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: @@ -1810,21 +1693,17 @@ packages: description: name: url_launcher_web sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" - sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" url: "https://pub.dev" source: hosted version: "2.4.0" - version: "2.4.0" url_launcher_windows: dependency: transitive description: name: url_launcher_windows sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" - sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted version: "3.1.4" - version: "3.1.4" uuid: dependency: "direct main" description: @@ -1838,11 +1717,9 @@ packages: description: name: value_layout_builder sha256: c02511ea91ca5c643b514a33a38fa52536f74aa939ec367d02938b5ede6807fa - sha256: c02511ea91ca5c643b514a33a38fa52536f74aa939ec367d02938b5ede6807fa url: "https://pub.dev" source: hosted version: "0.4.0" - version: "0.4.0" vector_graphics: dependency: transitive description: @@ -1856,21 +1733,17 @@ packages: description: name: vector_graphics_codec sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" - sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" url: "https://pub.dev" source: hosted version: "1.1.13" - version: "1.1.13" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" - sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" url: "https://pub.dev" source: hosted version: "1.1.16" - version: "1.1.16" vector_math: dependency: transitive description: @@ -1886,7 +1759,6 @@ packages: relative: true source: path version: "0.4.1" - version: "0.4.1" veilid_support: dependency: "direct main" description: @@ -1907,11 +1779,9 @@ packages: description: name: watcher sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" - sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" url: "https://pub.dev" source: hosted version: "1.1.1" - version: "1.1.1" web: dependency: transitive description: @@ -1933,21 +1803,17 @@ packages: description: name: web_socket_channel sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" - sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" url: "https://pub.dev" source: hosted version: "3.0.2" - version: "3.0.2" win32: dependency: transitive description: name: win32 sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e - sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e url: "https://pub.dev" source: hosted version: "5.10.1" - version: "5.10.1" window_manager: dependency: "direct main" description: @@ -1985,11 +1851,9 @@ packages: description: name: yaml sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted version: "3.1.3" - version: "3.1.3" zmodem: dependency: transitive description: @@ -2017,5 +1881,3 @@ packages: sdks: dart: ">=3.6.0 <4.0.0" flutter: ">=3.27.0" - dart: ">=3.6.0 <4.0.0" - flutter: ">=3.27.0" From 604ec9cfdd5e264884b327e5c6e5efee9308cf32 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 19 Feb 2025 14:30:26 -0500 Subject: [PATCH 207/270] minor cleanups --- .../veilid_support/example/android/settings.gradle | 11 +++++------ packages/veilid_support/lib/src/config.dart | 3 +-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/veilid_support/example/android/settings.gradle b/packages/veilid_support/example/android/settings.gradle index 1d6d19b..b1ae36a 100644 --- a/packages/veilid_support/example/android/settings.gradle +++ b/packages/veilid_support/example/android/settings.gradle @@ -5,10 +5,9 @@ pluginManagement { def flutterSdkPath = properties.getProperty("flutter.sdk") assert flutterSdkPath != null, "flutter.sdk not set in local.properties" return flutterSdkPath - } - settings.ext.flutterSdkPath = flutterSdkPath() + }() - includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") repositories { google() @@ -19,8 +18,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "7.3.0" apply false - id "org.jetbrains.kotlin.android" version "1.7.10" apply false + id "com.android.application" version "8.8.0" apply false + id "org.jetbrains.kotlin.android" version "1.9.25" apply false } -include ":app" +include ":app" \ No newline at end of file diff --git a/packages/veilid_support/lib/src/config.dart b/packages/veilid_support/lib/src/config.dart index 9f7703c..bedc743 100644 --- a/packages/veilid_support/lib/src/config.dart +++ b/packages/veilid_support/lib/src/config.dart @@ -49,8 +49,7 @@ Future> getDefaultVeilidPlatformConfig( return VeilidFFIConfig( logging: VeilidFFIConfigLogging( terminal: VeilidFFIConfigLoggingTerminal( - enabled: - kIsDebugMode && (Platform.isIOS || Platform.isAndroid), + enabled: false, level: kIsDebugMode ? VeilidConfigLogLevel.debug : VeilidConfigLogLevel.info, From d460a0388ce5a7087f0db5e508a3d618e7756494 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 13 Mar 2025 21:34:12 -0400 Subject: [PATCH 208/270] debugging and cleanup --- android/app/.gitignore | 1 + assets/i18n/en.json | 5 +- build.yaml | 3 + .../xcshareddata/xcschemes/Runner.xcscheme | 1 + .../local_account/local_account.freezed.dart | 58 +- .../per_account_collection_state.freezed.dart | 19 +- .../models/user_login/user_login.freezed.dart | 43 +- .../views/edit_account_page.dart | 9 +- .../views/new_account_page.dart | 5 +- .../views/show_recovery_key_page.dart | 4 +- .../models/chat_component_state.freezed.dart | 59 +- lib/chat/models/message_state.freezed.dart | 40 +- lib/chat/models/window_state.freezed.dart | 38 +- lib/chat/views/no_conversation_widget.dart | 2 +- lib/contacts/views/no_contact_widget.dart | 2 +- lib/layout/default_app_bar.dart | 1 + .../notifications_preference.freezed.dart | 21 +- .../models/notifications_state.freezed.dart | 34 +- .../views/notifications_widget.dart | 64 +- lib/proto/veilidchat.pb.dart | 844 +++++++++++++++++- lib/proto/veilidchat.pbenum.dart | 5 +- lib/proto/veilidchat.pbjson.dart | 2 +- lib/proto/veilidchat.pbserver.dart | 2 +- lib/router/cubits/router_cubit.freezed.dart | 21 +- lib/settings/models/preferences.freezed.dart | 48 +- lib/theme/models/chat_theme.dart | 2 +- lib/theme/models/contrast_generator.dart | 8 +- lib/theme/models/models.dart | 5 +- lib/theme/models/radix_generator.dart | 15 +- .../models/{ => scale_theme}/scale_color.dart | 0 .../scale_custom_dropdown_theme.dart | 93 ++ .../scale_input_decorator_theme.dart | 93 +- .../{ => scale_theme}/scale_scheme.dart | 0 lib/theme/models/scale_theme/scale_theme.dart | 44 + .../scale_tile_theme.dart} | 36 +- .../models/scale_theme/scale_toast_theme.dart | 72 ++ lib/theme/models/theme_preference.dart | 2 +- .../models/theme_preference.freezed.dart | 21 +- lib/theme/views/option_box.dart | 4 +- lib/theme/{models => views}/slider_tile.dart | 0 lib/theme/views/styled_scaffold.dart | 21 +- lib/theme/views/views.dart | 1 + lib/theme/views/widget_helpers.dart | 26 +- lib/tools/loggy.dart | 2 +- .../processor_connection_state.freezed.dart | 21 +- lib/veilid_processor/views/developer.dart | 173 ++-- .../views/signal_strength_meter.dart | 19 +- .../xcshareddata/xcschemes/Runner.xcscheme | 1 + .../example/integration_test/app_test.dart | 208 ++--- packages/veilid_support/example/pubspec.lock | 96 +- .../dht_record/dht_record_pool.freezed.dart | 42 +- .../dht_record/dht_record_pool_private.dart | 3 - .../account_record_info.freezed.dart | 28 +- .../identity_support/identity.freezed.dart | 26 +- .../identity_instance.freezed.dart | 50 +- .../super_identity.freezed.dart | 38 +- packages/veilid_support/lib/proto/dht.pb.dart | 103 ++- .../veilid_support/lib/proto/dht.pbenum.dart | 2 +- .../veilid_support/lib/proto/dht.pbjson.dart | 2 +- .../lib/proto/dht.pbserver.dart | 2 +- .../veilid_support/lib/proto/veilid.pb.dart | 177 +++- .../lib/proto/veilid.pbenum.dart | 2 +- .../lib/proto/veilid.pbjson.dart | 2 +- .../lib/proto/veilid.pbserver.dart | 2 +- packages/veilid_support/lib/src/config.dart | 3 +- packages/veilid_support/pubspec.lock | 23 +- packages/veilid_support/pubspec.yaml | 6 +- pubspec.lock | 211 +++-- pubspec.yaml | 80 +- 69 files changed, 2306 insertions(+), 790 deletions(-) create mode 100644 android/app/.gitignore rename lib/theme/models/{ => scale_theme}/scale_color.dart (100%) create mode 100644 lib/theme/models/scale_theme/scale_custom_dropdown_theme.dart rename lib/theme/models/{ => scale_theme}/scale_input_decorator_theme.dart (60%) rename lib/theme/models/{ => scale_theme}/scale_scheme.dart (100%) create mode 100644 lib/theme/models/scale_theme/scale_theme.dart rename lib/theme/models/{scale_theme.dart => scale_theme/scale_tile_theme.dart} (67%) create mode 100644 lib/theme/models/scale_theme/scale_toast_theme.dart rename lib/theme/{models => views}/slider_tile.dart (100%) diff --git a/android/app/.gitignore b/android/app/.gitignore new file mode 100644 index 0000000..0e60033 --- /dev/null +++ b/android/app/.gitignore @@ -0,0 +1 @@ +.cxx diff --git a/assets/i18n/en.json b/assets/i18n/en.json index d45a746..4334a6b 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -65,8 +65,9 @@ "destroy_account_confirm_message": "This action is PERMANENT, and your VeilidChat account will no longer be recoverable with the recovery key. Restoring from backups will not recover your account!", "destroy_account_confirm_message_details": "You will lose access to:\n • Your entire message history\n • Your contacts\n • This will not remove your messages you have sent from other people's devices\n", "confirm_are_you_sure": "Are you sure you want to do this?", - "failed_to_remove": "Failed to remove account.\n\nTry again when you have a more stable network connection.", - "failed_to_destroy": "Failed to destroy account.\n\nTry again when you have a more stable network connection.", + "failed_to_remove_title": "Failed to remove account", + "try_again_network": "Try again when you have a more stable network connection", + "failed_to_destroy_title": "Failed to destroy account", "account_removed": "Account removed successfully", "account_destroyed": "Account destroyed successfully" }, diff --git a/build.yaml b/build.yaml index 950fe95..77b9050 100644 --- a/build.yaml +++ b/build.yaml @@ -1,6 +1,9 @@ targets: $default: builders: + freezed: + options: + generic_argument_factories: true json_serializable: options: explicit_to_json: true diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 8b3e7d0..3e31b44 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -48,6 +48,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> 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 92e376f..effc69a 100644 --- a/lib/account_manager/models/local_account/local_account.freezed.dart +++ b/lib/account_manager/models/local_account/local_account.freezed.dart @@ -37,8 +37,12 @@ mixin _$LocalAccount { throw _privateConstructorUsedError; // Display name for account until it is unlocked String get name => throw _privateConstructorUsedError; + /// Serializes this LocalAccount to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of LocalAccount + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $LocalAccountCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -70,6 +74,8 @@ class _$LocalAccountCopyWithImpl<$Res, $Val extends LocalAccount> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of LocalAccount + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -108,6 +114,8 @@ class _$LocalAccountCopyWithImpl<$Res, $Val extends LocalAccount> ) as $Val); } + /// Create a copy of LocalAccount + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $SuperIdentityCopyWith<$Res> get superIdentity { @@ -145,6 +153,8 @@ class __$$LocalAccountImplCopyWithImpl<$Res> _$LocalAccountImpl _value, $Res Function(_$LocalAccountImpl) _then) : super(_value, _then); + /// Create a copy of LocalAccount + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -244,7 +254,7 @@ class _$LocalAccountImpl implements _LocalAccount { (identical(other.name, name) || other.name == name)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, @@ -255,7 +265,9 @@ class _$LocalAccountImpl implements _LocalAccount { hiddenAccount, name); - @JsonKey(ignore: true) + /// Create a copy of LocalAccount + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$LocalAccountImplCopyWith<_$LocalAccountImpl> get copyWith => @@ -281,24 +293,32 @@ abstract class _LocalAccount implements LocalAccount { factory _LocalAccount.fromJson(Map json) = _$LocalAccountImpl.fromJson; - @override // The super identity key record for the account, +// The super identity key record for the account, // containing the publicKey in the currentIdentity - SuperIdentity get superIdentity; - @override // The encrypted currentIdentity secret that goes with -// the identityPublicKey with appended salt - @Uint8ListJsonConverter() - Uint8List get identitySecretBytes; - @override // The kind of encryption input used on the account - EncryptionKeyType get encryptionKeyType; - @override // If account is not hidden, password can be retrieved via - bool get biometricsEnabled; - @override // Keep account hidden unless account password is entered -// (tries all hidden accounts with auth method (no biometrics)) - bool get hiddenAccount; - @override // Display name for account until it is unlocked - String get name; @override - @JsonKey(ignore: true) + SuperIdentity + get superIdentity; // The encrypted currentIdentity secret that goes with +// the identityPublicKey with appended salt + @override + @Uint8ListJsonConverter() + Uint8List + get identitySecretBytes; // The kind of encryption input used on the account + @override + EncryptionKeyType + get encryptionKeyType; // If account is not hidden, password can be retrieved via + @override + bool + get biometricsEnabled; // Keep account hidden unless account password is entered +// (tries all hidden accounts with auth method (no biometrics)) + @override + bool get hiddenAccount; // Display name for account until it is unlocked + @override + String get name; + + /// Create a copy of LocalAccount + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) _$$LocalAccountImplCopyWith<_$LocalAccountImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/account_manager/models/per_account_collection_state/per_account_collection_state.freezed.dart b/lib/account_manager/models/per_account_collection_state/per_account_collection_state.freezed.dart index 8dcc549..1aa0c7e 100644 --- a/lib/account_manager/models/per_account_collection_state/per_account_collection_state.freezed.dart +++ b/lib/account_manager/models/per_account_collection_state/per_account_collection_state.freezed.dart @@ -35,7 +35,9 @@ mixin _$PerAccountCollectionState { get activeSingleContactChatBlocMapCubit => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of PerAccountCollectionState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $PerAccountCollectionStateCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -74,6 +76,8 @@ class _$PerAccountCollectionStateCopyWithImpl<$Res, // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of PerAccountCollectionState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -139,6 +143,8 @@ class _$PerAccountCollectionStateCopyWithImpl<$Res, ) as $Val); } + /// Create a copy of PerAccountCollectionState + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $AsyncValueCopyWith? get avAccountRecordState { @@ -190,6 +196,8 @@ class __$$PerAccountCollectionStateImplCopyWithImpl<$Res> $Res Function(_$PerAccountCollectionStateImpl) _then) : super(_value, _then); + /// Create a copy of PerAccountCollectionState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -353,7 +361,9 @@ class _$PerAccountCollectionStateImpl implements _PerAccountCollectionState { activeConversationsBlocMapCubit, activeSingleContactChatBlocMapCubit); - @JsonKey(ignore: true) + /// Create a copy of PerAccountCollectionState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$PerAccountCollectionStateImplCopyWith<_$PerAccountCollectionStateImpl> @@ -401,8 +411,11 @@ abstract class _PerAccountCollectionState implements PerAccountCollectionState { ActiveConversationsBlocMapCubit? get activeConversationsBlocMapCubit; @override ActiveSingleContactChatBlocMapCubit? get activeSingleContactChatBlocMapCubit; + + /// Create a copy of PerAccountCollectionState + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$PerAccountCollectionStateImplCopyWith<_$PerAccountCollectionStateImpl> get copyWith => throw _privateConstructorUsedError; } 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 c93ee7b..2804a77 100644 --- a/lib/account_manager/models/user_login/user_login.freezed.dart +++ b/lib/account_manager/models/user_login/user_login.freezed.dart @@ -30,8 +30,12 @@ mixin _$UserLogin { throw _privateConstructorUsedError; // The time this login was most recently used Timestamp get lastActive => throw _privateConstructorUsedError; + /// Serializes this UserLogin to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of UserLogin + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $UserLoginCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -60,6 +64,8 @@ class _$UserLoginCopyWithImpl<$Res, $Val extends UserLogin> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of UserLogin + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -88,6 +94,8 @@ class _$UserLoginCopyWithImpl<$Res, $Val extends UserLogin> ) as $Val); } + /// Create a copy of UserLogin + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $AccountRecordInfoCopyWith<$Res> get accountRecordInfo { @@ -123,6 +131,8 @@ class __$$UserLoginImplCopyWithImpl<$Res> _$UserLoginImpl _value, $Res Function(_$UserLoginImpl) _then) : super(_value, _then); + /// Create a copy of UserLogin + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -198,12 +208,14 @@ class _$UserLoginImpl implements _UserLogin { other.lastActive == lastActive)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, superIdentityRecordKey, identitySecret, accountRecordInfo, lastActive); - @JsonKey(ignore: true) + /// Create a copy of UserLogin + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$UserLoginImplCopyWith<_$UserLoginImpl> get copyWith => @@ -227,17 +239,24 @@ abstract class _UserLogin implements UserLogin { factory _UserLogin.fromJson(Map json) = _$UserLoginImpl.fromJson; - @override // SuperIdentity record key for the user +// SuperIdentity record key for the user // used to index the local accounts table - Typed get superIdentityRecordKey; - @override // The identity secret as unlocked from the local accounts table - Typed get identitySecret; - @override // The account record key, owner key and secret pulled from the identity - AccountRecordInfo get accountRecordInfo; - @override // The time this login was most recently used - Timestamp get lastActive; @override - @JsonKey(ignore: true) + Typed + get superIdentityRecordKey; // The identity secret as unlocked from the local accounts table + @override + Typed + get identitySecret; // The account record key, owner key and secret pulled from the identity + @override + AccountRecordInfo + get accountRecordInfo; // The time this login was most recently used + @override + Timestamp get lastActive; + + /// Create a copy of UserLogin + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) _$$UserLoginImplCopyWith<_$UserLoginImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/account_manager/views/edit_account_page.dart b/lib/account_manager/views/edit_account_page.dart index 3ba0f65..918c4ca 100644 --- a/lib/account_manager/views/edit_account_page.dart +++ b/lib/account_manager/views/edit_account_page.dart @@ -127,9 +127,9 @@ class _EditAccountPageState extends WindowSetupState { .info(text: translate('edit_account_page.account_removed')); GoRouterHelper(context).pop(); } else { - context - .read() - .error(text: translate('edit_account_page.failed_to_remove')); + context.read().error( + title: translate('edit_account_page.failed_to_remove_title'), + text: translate('edit_account_page.try_again_network')); } } } finally { @@ -196,7 +196,8 @@ class _EditAccountPageState extends WindowSetupState { GoRouterHelper(context).pop(); } else { context.read().error( - text: translate('edit_account_page.failed_to_destroy')); + title: translate('edit_account_page.failed_to_destroy_title'), + text: translate('edit_account_page.try_again_network')); } } } finally { diff --git a/lib/account_manager/views/new_account_page.dart b/lib/account_manager/views/new_account_page.dart index 69c75ae..ccd5b00 100644 --- a/lib/account_manager/views/new_account_page.dart +++ b/lib/account_manager/views/new_account_page.dart @@ -135,9 +135,10 @@ class _NewAccountPageState extends WindowSetupState { }) ]), body: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: _newAccountForm( - context, - )).paddingSymmetric(horizontal: 24, vertical: 8), + context, + )).paddingAll(2), ).withModalHUD(context, displayModalHUD); } diff --git a/lib/account_manager/views/show_recovery_key_page.dart b/lib/account_manager/views/show_recovery_key_page.dart index 2649bcf..acbb3f3 100644 --- a/lib/account_manager/views/show_recovery_key_page.dart +++ b/lib/account_manager/views/show_recovery_key_page.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; -import 'dart:typed_data'; import 'package:async_tools/async_tools.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:file_saver/file_saver.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:go_router/go_router.dart'; @@ -61,7 +61,7 @@ class _ShowRecoveryKeyPageState extends WindowSetupState { _isInAsyncCall = false; }); - if (Platform.isLinux) { + if (!kIsWeb && Platform.isLinux) { // Share plus doesn't do Linux yet await FileSaver.instance.saveFile(name: 'recovery_key.png', bytes: bytes); } else { diff --git a/lib/chat/models/chat_component_state.freezed.dart b/lib/chat/models/chat_component_state.freezed.dart index 41ab7e2..f967997 100644 --- a/lib/chat/models/chat_component_state.freezed.dart +++ b/lib/chat/models/chat_component_state.freezed.dart @@ -35,7 +35,9 @@ mixin _$ChatComponentState { throw _privateConstructorUsedError; // Title of the chat String get title => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of ChatComponentState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $ChatComponentStateCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -70,6 +72,8 @@ class _$ChatComponentStateCopyWithImpl<$Res, $Val extends ChatComponentState> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of ChatComponentState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -123,6 +127,8 @@ class _$ChatComponentStateCopyWithImpl<$Res, $Val extends ChatComponentState> ) as $Val); } + /// Create a copy of ChatComponentState + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $AsyncValueCopyWith, $Res> get messageWindow { @@ -164,6 +170,8 @@ class __$$ChatComponentStateImplCopyWithImpl<$Res> $Res Function(_$ChatComponentStateImpl) _then) : super(_value, _then); + /// Create a copy of ChatComponentState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -301,7 +309,9 @@ class _$ChatComponentStateImpl implements _ChatComponentState { messageWindow, title); - @JsonKey(ignore: true) + /// Create a copy of ChatComponentState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$ChatComponentStateImplCopyWith<_$ChatComponentStateImpl> get copyWith => @@ -322,26 +332,33 @@ abstract class _ChatComponentState implements ChatComponentState { required final AsyncValue> messageWindow, required final String title}) = _$ChatComponentStateImpl; - @override // GlobalKey for the chat - GlobalKey get chatKey; - @override // ScrollController for the chat - AutoScrollController get scrollController; - @override // TextEditingController for the chat - InputTextFieldController get textEditingController; - @override // Local user - User? get localUser; - @override // Active remote users - IMap, User> get remoteUsers; - @override // Historical remote users - IMap, User> get historicalRemoteUsers; - @override // Unknown users - IMap, User> get unknownUsers; - @override // Messages state - AsyncValue> get messageWindow; - @override // Title of the chat - String get title; +// GlobalKey for the chat @override - @JsonKey(ignore: true) + GlobalKey get chatKey; // ScrollController for the chat + @override + AutoScrollController + get scrollController; // TextEditingController for the chat + @override + InputTextFieldController get textEditingController; // Local user + @override + User? get localUser; // Active remote users + @override + IMap, User> + get remoteUsers; // Historical remote users + @override + IMap, User> + get historicalRemoteUsers; // Unknown users + @override + IMap, User> get unknownUsers; // Messages state + @override + AsyncValue> get messageWindow; // Title of the chat + @override + String get title; + + /// Create a copy of ChatComponentState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) _$$ChatComponentStateImplCopyWith<_$ChatComponentStateImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/chat/models/message_state.freezed.dart b/lib/chat/models/message_state.freezed.dart index 96c98e2..baafea6 100644 --- a/lib/chat/models/message_state.freezed.dart +++ b/lib/chat/models/message_state.freezed.dart @@ -30,8 +30,12 @@ mixin _$MessageState { throw _privateConstructorUsedError; // The state of the message MessageSendState? get sendState => throw _privateConstructorUsedError; + /// Serializes this MessageState to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of MessageState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $MessageStateCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -60,6 +64,8 @@ class _$MessageStateCopyWithImpl<$Res, $Val extends MessageState> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of MessageState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -113,6 +119,8 @@ class __$$MessageStateImplCopyWithImpl<$Res> _$MessageStateImpl _value, $Res Function(_$MessageStateImpl) _then) : super(_value, _then); + /// Create a copy of MessageState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -199,12 +207,14 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { other.sendState == sendState)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, content, sentTimestamp, reconciledTimestamp, sendState); - @JsonKey(ignore: true) + /// Create a copy of MessageState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$MessageStateImplCopyWith<_$MessageStateImpl> get copyWith => @@ -229,17 +239,21 @@ abstract class _MessageState implements MessageState { factory _MessageState.fromJson(Map json) = _$MessageStateImpl.fromJson; - @override // Content of the message - @JsonKey(fromJson: messageFromJson, toJson: messageToJson) - proto.Message get content; - @override // Sent timestamp - Timestamp get sentTimestamp; - @override // Reconciled timestamp - Timestamp? get reconciledTimestamp; - @override // The state of the message - MessageSendState? get sendState; +// Content of the message @override - @JsonKey(ignore: true) + @JsonKey(fromJson: messageFromJson, toJson: messageToJson) + proto.Message get content; // Sent timestamp + @override + Timestamp get sentTimestamp; // Reconciled timestamp + @override + Timestamp? get reconciledTimestamp; // The state of the message + @override + MessageSendState? get sendState; + + /// Create a copy of MessageState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) _$$MessageStateImplCopyWith<_$MessageStateImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/chat/models/window_state.freezed.dart b/lib/chat/models/window_state.freezed.dart index 604931d..59ff754 100644 --- a/lib/chat/models/window_state.freezed.dart +++ b/lib/chat/models/window_state.freezed.dart @@ -27,7 +27,9 @@ mixin _$WindowState { throw _privateConstructorUsedError; // If we should have the tail following the array bool get follow => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of WindowState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $WindowStateCopyWith> get copyWith => throw _privateConstructorUsedError; } @@ -56,6 +58,8 @@ class _$WindowStateCopyWithImpl> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of WindowState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -114,6 +118,8 @@ class __$$WindowStateImplCopyWithImpl _$WindowStateImpl _value, $Res Function(_$WindowStateImpl) _then) : super(_value, _then); + /// Create a copy of WindowState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -216,7 +222,9 @@ class _$WindowStateImpl windowCount, follow); - @JsonKey(ignore: true) + /// Create a copy of WindowState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$WindowStateImplCopyWith> get copyWith => @@ -232,18 +240,22 @@ abstract class _WindowState implements WindowState { required final int windowCount, required final bool follow}) = _$WindowStateImpl; - @override // List of objects in the window - IList get window; - @override // Total number of objects (windowTail max) - int get length; - @override // One past the end of the last element - int get windowTail; - @override // The total number of elements to try to keep in the window - int get windowCount; - @override // If we should have the tail following the array - bool get follow; +// List of objects in the window @override - @JsonKey(ignore: true) + IList get window; // Total number of objects (windowTail max) + @override + int get length; // One past the end of the last element + @override + int get windowTail; // The total number of elements to try to keep in the window + @override + int get windowCount; // If we should have the tail following the array + @override + bool get follow; + + /// Create a copy of WindowState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) _$$WindowStateImplCopyWith> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/chat/views/no_conversation_widget.dart b/lib/chat/views/no_conversation_widget.dart index 77502e1..e246fee 100644 --- a/lib/chat/views/no_conversation_widget.dart +++ b/lib/chat/views/no_conversation_widget.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import '../../theme/models/scale_scheme.dart'; +import '../../theme/models/scale_theme/scale_scheme.dart'; class NoConversationWidget extends StatelessWidget { const NoConversationWidget({super.key}); diff --git a/lib/contacts/views/no_contact_widget.dart b/lib/contacts/views/no_contact_widget.dart index c559d8b..8edb5d5 100644 --- a/lib/contacts/views/no_contact_widget.dart +++ b/lib/contacts/views/no_contact_widget.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import '../../theme/models/scale_scheme.dart'; +import '../../theme/models/scale_theme/scale_scheme.dart'; class NoContactWidget extends StatelessWidget { const NoContactWidget({super.key}); diff --git a/lib/layout/default_app_bar.dart b/lib/layout/default_app_bar.dart index 41e3601..fbf2360 100644 --- a/lib/layout/default_app_bar.dart +++ b/lib/layout/default_app_bar.dart @@ -6,6 +6,7 @@ class DefaultAppBar extends AppBar { DefaultAppBar( {required super.title, super.key, Widget? leading, super.actions}) : super( + titleSpacing: 0, leading: leading ?? Container( margin: const EdgeInsets.all(4), diff --git a/lib/notifications/models/notifications_preference.freezed.dart b/lib/notifications/models/notifications_preference.freezed.dart index b2cbc67..0335ad8 100644 --- a/lib/notifications/models/notifications_preference.freezed.dart +++ b/lib/notifications/models/notifications_preference.freezed.dart @@ -35,8 +35,12 @@ mixin _$NotificationsPreference { SoundEffect get onMessageReceivedSound => throw _privateConstructorUsedError; SoundEffect get onMessageSentSound => throw _privateConstructorUsedError; + /// Serializes this NotificationsPreference to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of NotificationsPreference + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $NotificationsPreferenceCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -70,6 +74,8 @@ class _$NotificationsPreferenceCopyWithImpl<$Res, // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of NotificationsPreference + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -155,6 +161,8 @@ class __$$NotificationsPreferenceImplCopyWithImpl<$Res> $Res Function(_$NotificationsPreferenceImpl) _then) : super(_value, _then); + /// Create a copy of NotificationsPreference + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -289,7 +297,7 @@ class _$NotificationsPreferenceImpl implements _NotificationsPreference { other.onMessageSentSound == onMessageSentSound)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, @@ -303,7 +311,9 @@ class _$NotificationsPreferenceImpl implements _NotificationsPreference { onMessageReceivedSound, onMessageSentSound); - @JsonKey(ignore: true) + /// Create a copy of NotificationsPreference + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$NotificationsPreferenceImplCopyWith<_$NotificationsPreferenceImpl> @@ -351,8 +361,11 @@ abstract class _NotificationsPreference implements NotificationsPreference { SoundEffect get onMessageReceivedSound; @override SoundEffect get onMessageSentSound; + + /// Create a copy of NotificationsPreference + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$NotificationsPreferenceImplCopyWith<_$NotificationsPreferenceImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/notifications/models/notifications_state.freezed.dart b/lib/notifications/models/notifications_state.freezed.dart index 90893e6..e052e7a 100644 --- a/lib/notifications/models/notifications_state.freezed.dart +++ b/lib/notifications/models/notifications_state.freezed.dart @@ -20,7 +20,9 @@ mixin _$NotificationItem { String get text => throw _privateConstructorUsedError; String? get title => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of NotificationItem + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $NotificationItemCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -44,6 +46,8 @@ class _$NotificationItemCopyWithImpl<$Res, $Val extends NotificationItem> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of NotificationItem + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -87,6 +91,8 @@ class __$$NotificationItemImplCopyWithImpl<$Res> $Res Function(_$NotificationItemImpl) _then) : super(_value, _then); + /// Create a copy of NotificationItem + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -142,7 +148,9 @@ class _$NotificationItemImpl implements _NotificationItem { @override int get hashCode => Object.hash(runtimeType, type, text, title); - @JsonKey(ignore: true) + /// Create a copy of NotificationItem + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$NotificationItemImplCopyWith<_$NotificationItemImpl> get copyWith => @@ -162,8 +170,11 @@ abstract class _NotificationItem implements NotificationItem { String get text; @override String? get title; + + /// Create a copy of NotificationItem + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$NotificationItemImplCopyWith<_$NotificationItemImpl> get copyWith => throw _privateConstructorUsedError; } @@ -172,7 +183,9 @@ abstract class _NotificationItem implements NotificationItem { mixin _$NotificationsState { IList get queue => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of NotificationsState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $NotificationsStateCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -196,6 +209,8 @@ class _$NotificationsStateCopyWithImpl<$Res, $Val extends NotificationsState> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of NotificationsState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -229,6 +244,8 @@ class __$$NotificationsStateImplCopyWithImpl<$Res> $Res Function(_$NotificationsStateImpl) _then) : super(_value, _then); + /// Create a copy of NotificationsState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -268,7 +285,9 @@ class _$NotificationsStateImpl implements _NotificationsState { int get hashCode => Object.hash(runtimeType, const DeepCollectionEquality().hash(queue)); - @JsonKey(ignore: true) + /// Create a copy of NotificationsState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$NotificationsStateImplCopyWith<_$NotificationsStateImpl> get copyWith => @@ -283,8 +302,11 @@ abstract class _NotificationsState implements NotificationsState { @override IList get queue; + + /// Create a copy of NotificationsState + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$NotificationsStateImplCopyWith<_$NotificationsStateImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/notifications/views/notifications_widget.dart b/lib/notifications/views/notifications_widget.dart index 246a570..73fac5f 100644 --- a/lib/notifications/views/notifications_widget.dart +++ b/lib/notifications/views/notifications_widget.dart @@ -1,6 +1,7 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:motion_toast/motion_toast.dart'; +import 'package:toastification/toastification.dart'; import '../../theme/theme.dart'; import '../notifications.dart'; @@ -43,46 +44,47 @@ class NotificationsWidget extends StatelessWidget { //////////////////////////////////////////////////////////////////////////// // Private Implementation + void _toast( + {required BuildContext context, + required String text, + required ScaleToastTheme toastTheme, + String? title}) { + toastification.show( + context: context, + title: title != null + ? Text(title) + .copyWith(style: toastTheme.titleTextStyle) + .paddingLTRB(0, 0, 0, 8) + : null, + description: Text(text).copyWith(style: toastTheme.descriptionTextStyle), + icon: toastTheme.icon, + primaryColor: toastTheme.primaryColor, + backgroundColor: toastTheme.backgroundColor, + foregroundColor: toastTheme.foregroundColor, + padding: toastTheme.padding, + borderRadius: toastTheme.borderRadius, + borderSide: toastTheme.borderSide, + autoCloseDuration: const Duration(seconds: 2), + animationDuration: const Duration(milliseconds: 500), + ); + } + void _info( {required BuildContext context, required String text, String? title}) { final theme = Theme.of(context); - final scale = theme.extension()!; - final scaleConfig = theme.extension()!; + final toastTheme = + theme.extension()!.toastTheme(ScaleToastKind.info); - MotionToast( - title: title != null ? Text(title) : null, - description: Text(text), - constraints: BoxConstraints.loose(const Size(400, 100)), - contentPadding: const EdgeInsets.all(16), - primaryColor: scale.tertiaryScale.elementBackground, - secondaryColor: scale.tertiaryScale.calloutBackground, - borderRadius: 12 * scaleConfig.borderRadiusScale, - toastDuration: const Duration(seconds: 2), - animationDuration: const Duration(milliseconds: 500), - displayBorder: scaleConfig.useVisualIndicators, - icon: Icons.info, - ).show(context); + _toast(context: context, text: text, toastTheme: toastTheme, title: title); } void _error( {required BuildContext context, required String text, String? title}) { final theme = Theme.of(context); - final scale = theme.extension()!; - final scaleConfig = theme.extension()!; + final toastTheme = + theme.extension()!.toastTheme(ScaleToastKind.error); - MotionToast( - title: title != null ? Text(title) : null, - description: Text(text), - constraints: BoxConstraints.loose(const Size(400, 100)), - contentPadding: const EdgeInsets.all(16), - primaryColor: scale.errorScale.elementBackground, - secondaryColor: scale.errorScale.calloutBackground, - borderRadius: 12 * scaleConfig.borderRadiusScale, - toastDuration: const Duration(seconds: 4), - animationDuration: const Duration(milliseconds: 1000), - displayBorder: scaleConfig.useVisualIndicators, - icon: Icons.error, - ).show(context); + _toast(context: context, text: text, toastTheme: toastTheme, title: title); } //////////////////////////////////////////////////////////////////////////// diff --git a/lib/proto/veilidchat.pb.dart b/lib/proto/veilidchat.pb.dart index 5152594..3947baf 100644 --- a/lib/proto/veilidchat.pb.dart +++ b/lib/proto/veilidchat.pb.dart @@ -4,7 +4,7 @@ // // @dart = 2.12 -// ignore_for_file: annotate_overrides, camel_case_types +// ignore_for_file: annotate_overrides, camel_case_types, comment_references // ignore_for_file: constant_identifier_names, library_prefixes // ignore_for_file: non_constant_identifier_names, prefer_final_fields // ignore_for_file: unnecessary_import, unnecessary_this, unused_import @@ -20,8 +20,21 @@ import 'veilidchat.pbenum.dart'; export 'veilidchat.pbenum.dart'; +/// Reference to data on the DHT class DHTDataReference extends $pb.GeneratedMessage { - factory DHTDataReference() => create(); + factory DHTDataReference({ + $0.TypedKey? dhtData, + $0.TypedKey? hash, + }) { + final $result = create(); + if (dhtData != null) { + $result.dhtData = dhtData; + } + if (hash != null) { + $result.hash = hash; + } + return $result; + } DHTDataReference._() : super(); factory DHTDataReference.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory DHTDataReference.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -76,8 +89,17 @@ class DHTDataReference extends $pb.GeneratedMessage { $0.TypedKey ensureHash() => $_ensure(1); } +/// Reference to data on the BlockStore class BlockStoreDataReference extends $pb.GeneratedMessage { - factory BlockStoreDataReference() => create(); + factory BlockStoreDataReference({ + $0.TypedKey? block, + }) { + final $result = create(); + if (block != null) { + $result.block = block; + } + return $result; + } BlockStoreDataReference._() : super(); factory BlockStoreDataReference.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory BlockStoreDataReference.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -126,8 +148,23 @@ enum DataReference_Kind { notSet } +/// DataReference +/// Pointer to data somewhere in Veilid +/// Abstraction over DHTData and BlockStore class DataReference extends $pb.GeneratedMessage { - factory DataReference() => create(); + factory DataReference({ + DHTDataReference? dhtData, + BlockStoreDataReference? blockStoreData, + }) { + final $result = create(); + if (dhtData != null) { + $result.dhtData = dhtData; + } + if (blockStoreData != null) { + $result.blockStoreData = blockStoreData; + } + return $result; + } DataReference._() : super(); factory DataReference.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory DataReference.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -196,8 +233,21 @@ enum Attachment_Kind { notSet } +/// A single attachment class Attachment extends $pb.GeneratedMessage { - factory Attachment() => create(); + factory Attachment({ + AttachmentMedia? media, + $0.Signature? signature, + }) { + final $result = create(); + if (media != null) { + $result.media = media; + } + if (signature != null) { + $result.signature = signature; + } + return $result; + } 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); @@ -248,6 +298,7 @@ class Attachment extends $pb.GeneratedMessage { @$pb.TagNumber(1) AttachmentMedia ensureMedia() => $_ensure(0); + /// Author signature over all attachment fields and content fields and bytes @$pb.TagNumber(2) $0.Signature get signature => $_getN(1); @$pb.TagNumber(2) @@ -260,8 +311,25 @@ class Attachment extends $pb.GeneratedMessage { $0.Signature ensureSignature() => $_ensure(1); } +/// A file, audio, image, or video attachment class AttachmentMedia extends $pb.GeneratedMessage { - factory AttachmentMedia() => create(); + factory AttachmentMedia({ + $core.String? mime, + $core.String? name, + DataReference? content, + }) { + final $result = create(); + if (mime != null) { + $result.mime = mime; + } + if (name != null) { + $result.name = name; + } + if (content != null) { + $result.content = content; + } + return $result; + } AttachmentMedia._() : super(); factory AttachmentMedia.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory AttachmentMedia.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -294,6 +362,7 @@ class AttachmentMedia extends $pb.GeneratedMessage { static AttachmentMedia getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static AttachmentMedia? _defaultInstance; + /// MIME type of the data @$pb.TagNumber(1) $core.String get mime => $_getSZ(0); @$pb.TagNumber(1) @@ -303,6 +372,7 @@ class AttachmentMedia extends $pb.GeneratedMessage { @$pb.TagNumber(1) void clearMime() => clearField(1); + /// Title or filename @$pb.TagNumber(2) $core.String get name => $_getSZ(1); @$pb.TagNumber(2) @@ -312,6 +382,7 @@ class AttachmentMedia extends $pb.GeneratedMessage { @$pb.TagNumber(2) void clearName() => clearField(2); + /// Pointer to the data content @$pb.TagNumber(3) DataReference get content => $_getN(2); @$pb.TagNumber(3) @@ -324,8 +395,25 @@ class AttachmentMedia extends $pb.GeneratedMessage { DataReference ensureContent() => $_ensure(2); } +/// Permissions of a chat class Permissions extends $pb.GeneratedMessage { - factory Permissions() => create(); + factory Permissions({ + Scope? canAddMembers, + Scope? canEditInfo, + $core.bool? moderated, + }) { + final $result = create(); + if (canAddMembers != null) { + $result.canAddMembers = canAddMembers; + } + if (canEditInfo != null) { + $result.canEditInfo = canEditInfo; + } + if (moderated != null) { + $result.moderated = moderated; + } + return $result; + } Permissions._() : super(); factory Permissions.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory Permissions.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -358,6 +446,7 @@ class Permissions extends $pb.GeneratedMessage { static Permissions getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Permissions? _defaultInstance; + /// Parties in this scope or higher can add members to their own group or lower @$pb.TagNumber(1) Scope get canAddMembers => $_getN(0); @$pb.TagNumber(1) @@ -367,6 +456,7 @@ class Permissions extends $pb.GeneratedMessage { @$pb.TagNumber(1) void clearCanAddMembers() => clearField(1); + /// Parties in this scope or higher can change the 'info' of a group @$pb.TagNumber(2) Scope get canEditInfo => $_getN(1); @$pb.TagNumber(2) @@ -376,6 +466,7 @@ class Permissions extends $pb.GeneratedMessage { @$pb.TagNumber(2) void clearCanEditInfo() => clearField(2); + /// If moderation is enabled or not. @$pb.TagNumber(3) $core.bool get moderated => $_getBF(2); @$pb.TagNumber(3) @@ -386,8 +477,33 @@ class Permissions extends $pb.GeneratedMessage { void clearModerated() => clearField(3); } +/// The membership of a chat class Membership extends $pb.GeneratedMessage { - factory Membership() => create(); + factory Membership({ + $core.Iterable<$0.TypedKey>? watchers, + $core.Iterable<$0.TypedKey>? moderated, + $core.Iterable<$0.TypedKey>? talkers, + $core.Iterable<$0.TypedKey>? moderators, + $core.Iterable<$0.TypedKey>? admins, + }) { + final $result = create(); + if (watchers != null) { + $result.watchers.addAll(watchers); + } + if (moderated != null) { + $result.moderated.addAll(moderated); + } + if (talkers != null) { + $result.talkers.addAll(talkers); + } + if (moderators != null) { + $result.moderators.addAll(moderators); + } + if (admins != null) { + $result.admins.addAll(admins); + } + return $result; + } Membership._() : super(); factory Membership.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory Membership.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -422,24 +538,50 @@ class Membership extends $pb.GeneratedMessage { static Membership getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Membership? _defaultInstance; + /// Conversation keys for parties in the 'watchers' group @$pb.TagNumber(1) $core.List<$0.TypedKey> get watchers => $_getList(0); + /// Conversation keys for parties in the 'moderated' group @$pb.TagNumber(2) $core.List<$0.TypedKey> get moderated => $_getList(1); + /// Conversation keys for parties in the 'talkers' group @$pb.TagNumber(3) $core.List<$0.TypedKey> get talkers => $_getList(2); + /// Conversation keys for parties in the 'moderators' group @$pb.TagNumber(4) $core.List<$0.TypedKey> get moderators => $_getList(3); + /// Conversation keys for parties in the 'admins' group @$pb.TagNumber(5) $core.List<$0.TypedKey> get admins => $_getList(4); } +/// The chat settings class ChatSettings extends $pb.GeneratedMessage { - factory ChatSettings() => create(); + factory ChatSettings({ + $core.String? title, + $core.String? description, + DataReference? icon, + $fixnum.Int64? defaultExpiration, + }) { + final $result = create(); + if (title != null) { + $result.title = title; + } + if (description != null) { + $result.description = description; + } + if (icon != null) { + $result.icon = icon; + } + if (defaultExpiration != null) { + $result.defaultExpiration = defaultExpiration; + } + return $result; + } ChatSettings._() : super(); factory ChatSettings.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory ChatSettings.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -473,6 +615,7 @@ class ChatSettings extends $pb.GeneratedMessage { static ChatSettings getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static ChatSettings? _defaultInstance; + /// Title for the chat @$pb.TagNumber(1) $core.String get title => $_getSZ(0); @$pb.TagNumber(1) @@ -482,6 +625,7 @@ class ChatSettings extends $pb.GeneratedMessage { @$pb.TagNumber(1) void clearTitle() => clearField(1); + /// Description for the chat @$pb.TagNumber(2) $core.String get description => $_getSZ(1); @$pb.TagNumber(2) @@ -491,6 +635,7 @@ class ChatSettings extends $pb.GeneratedMessage { @$pb.TagNumber(2) void clearDescription() => clearField(2); + /// Icon for the chat @$pb.TagNumber(3) DataReference get icon => $_getN(2); @$pb.TagNumber(3) @@ -502,6 +647,7 @@ class ChatSettings extends $pb.GeneratedMessage { @$pb.TagNumber(3) DataReference ensureIcon() => $_ensure(2); + /// Default message expiration duration (in us) @$pb.TagNumber(4) $fixnum.Int64 get defaultExpiration => $_getI64(3); @$pb.TagNumber(4) @@ -512,8 +658,37 @@ class ChatSettings extends $pb.GeneratedMessage { void clearDefaultExpiration() => clearField(4); } +/// A text message class Message_Text extends $pb.GeneratedMessage { - factory Message_Text() => create(); + factory Message_Text({ + $core.String? text, + $core.String? topic, + $core.List<$core.int>? replyId, + $fixnum.Int64? expiration, + $core.int? viewLimit, + $core.Iterable? attachments, + }) { + final $result = create(); + if (text != null) { + $result.text = text; + } + if (topic != null) { + $result.topic = topic; + } + if (replyId != null) { + $result.replyId = replyId; + } + if (expiration != null) { + $result.expiration = expiration; + } + if (viewLimit != null) { + $result.viewLimit = viewLimit; + } + if (attachments != null) { + $result.attachments.addAll(attachments); + } + return $result; + } Message_Text._() : super(); factory Message_Text.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory Message_Text.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -549,6 +724,7 @@ class Message_Text extends $pb.GeneratedMessage { static Message_Text getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Message_Text? _defaultInstance; + /// Text of the message @$pb.TagNumber(1) $core.String get text => $_getSZ(0); @$pb.TagNumber(1) @@ -558,6 +734,7 @@ class Message_Text extends $pb.GeneratedMessage { @$pb.TagNumber(1) void clearText() => clearField(1); + /// Topic of the message / Content warning @$pb.TagNumber(2) $core.String get topic => $_getSZ(1); @$pb.TagNumber(2) @@ -567,6 +744,7 @@ class Message_Text extends $pb.GeneratedMessage { @$pb.TagNumber(2) void clearTopic() => clearField(2); + /// Message id replied to (author id + message id) @$pb.TagNumber(3) $core.List<$core.int> get replyId => $_getN(2); @$pb.TagNumber(3) @@ -576,6 +754,7 @@ class Message_Text extends $pb.GeneratedMessage { @$pb.TagNumber(3) void clearReplyId() => clearField(3); + /// Message expiration timestamp @$pb.TagNumber(4) $fixnum.Int64 get expiration => $_getI64(3); @$pb.TagNumber(4) @@ -585,6 +764,7 @@ class Message_Text extends $pb.GeneratedMessage { @$pb.TagNumber(4) void clearExpiration() => clearField(4); + /// Message view limit before deletion @$pb.TagNumber(5) $core.int get viewLimit => $_getIZ(4); @$pb.TagNumber(5) @@ -594,12 +774,26 @@ class Message_Text extends $pb.GeneratedMessage { @$pb.TagNumber(5) void clearViewLimit() => clearField(5); + /// Attachments on the message @$pb.TagNumber(6) $core.List get attachments => $_getList(5); } +/// A secret message class Message_Secret extends $pb.GeneratedMessage { - factory Message_Secret() => create(); + factory Message_Secret({ + $core.List<$core.int>? ciphertext, + $fixnum.Int64? expiration, + }) { + final $result = create(); + if (ciphertext != null) { + $result.ciphertext = ciphertext; + } + if (expiration != null) { + $result.expiration = expiration; + } + return $result; + } Message_Secret._() : super(); factory Message_Secret.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory Message_Secret.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -631,6 +825,7 @@ class Message_Secret extends $pb.GeneratedMessage { static Message_Secret getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Message_Secret? _defaultInstance; + /// Text message protobuf encrypted by a key @$pb.TagNumber(1) $core.List<$core.int> get ciphertext => $_getN(0); @$pb.TagNumber(1) @@ -640,6 +835,8 @@ class Message_Secret extends $pb.GeneratedMessage { @$pb.TagNumber(1) void clearCiphertext() => clearField(1); + /// Secret expiration timestamp + /// This is the time after which an un-revealed secret will get deleted @$pb.TagNumber(2) $fixnum.Int64 get expiration => $_getI64(1); @$pb.TagNumber(2) @@ -650,8 +847,18 @@ class Message_Secret extends $pb.GeneratedMessage { void clearExpiration() => clearField(2); } +/// A 'delete' control message +/// Deletes a set of messages by their ids class Message_ControlDelete extends $pb.GeneratedMessage { - factory Message_ControlDelete() => create(); + factory Message_ControlDelete({ + $core.Iterable<$core.List<$core.int>>? ids, + }) { + final $result = create(); + if (ids != null) { + $result.ids.addAll(ids); + } + return $result; + } Message_ControlDelete._() : super(); factory Message_ControlDelete.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory Message_ControlDelete.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -686,8 +893,18 @@ class Message_ControlDelete extends $pb.GeneratedMessage { $core.List<$core.List<$core.int>> get ids => $_getList(0); } +/// An 'erase' control message +/// Deletes a set of messages from before some timestamp class Message_ControlErase extends $pb.GeneratedMessage { - factory Message_ControlErase() => create(); + factory Message_ControlErase({ + $fixnum.Int64? timestamp, + }) { + final $result = create(); + if (timestamp != null) { + $result.timestamp = timestamp; + } + return $result; + } Message_ControlErase._() : super(); factory Message_ControlErase.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory Message_ControlErase.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -718,6 +935,8 @@ class Message_ControlErase extends $pb.GeneratedMessage { static Message_ControlErase getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Message_ControlErase? _defaultInstance; + /// The latest timestamp to delete messages before + /// If this is zero then all messages are cleared @$pb.TagNumber(1) $fixnum.Int64 get timestamp => $_getI64(0); @$pb.TagNumber(1) @@ -728,8 +947,17 @@ class Message_ControlErase extends $pb.GeneratedMessage { void clearTimestamp() => clearField(1); } +/// A 'change settings' control message class Message_ControlSettings extends $pb.GeneratedMessage { - factory Message_ControlSettings() => create(); + factory Message_ControlSettings({ + ChatSettings? settings, + }) { + final $result = create(); + if (settings != null) { + $result.settings = settings; + } + return $result; + } Message_ControlSettings._() : super(); factory Message_ControlSettings.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory Message_ControlSettings.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -772,8 +1000,18 @@ class Message_ControlSettings extends $pb.GeneratedMessage { ChatSettings ensureSettings() => $_ensure(0); } +/// A 'change permissions' control message +/// Changes the permissions of a chat class Message_ControlPermissions extends $pb.GeneratedMessage { - factory Message_ControlPermissions() => create(); + factory Message_ControlPermissions({ + Permissions? permissions, + }) { + final $result = create(); + if (permissions != null) { + $result.permissions = permissions; + } + return $result; + } Message_ControlPermissions._() : super(); factory Message_ControlPermissions.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory Message_ControlPermissions.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -816,8 +1054,18 @@ class Message_ControlPermissions extends $pb.GeneratedMessage { Permissions ensurePermissions() => $_ensure(0); } +/// A 'change membership' control message +/// Changes the class Message_ControlMembership extends $pb.GeneratedMessage { - factory Message_ControlMembership() => create(); + factory Message_ControlMembership({ + Membership? membership, + }) { + final $result = create(); + if (membership != null) { + $result.membership = membership; + } + return $result; + } Message_ControlMembership._() : super(); factory Message_ControlMembership.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory Message_ControlMembership.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -860,8 +1108,22 @@ class Message_ControlMembership extends $pb.GeneratedMessage { Membership ensureMembership() => $_ensure(0); } +/// A 'moderation' control message +/// Accepts or rejects a set of messages class Message_ControlModeration extends $pb.GeneratedMessage { - factory Message_ControlModeration() => create(); + factory Message_ControlModeration({ + $core.Iterable<$core.List<$core.int>>? acceptedIds, + $core.Iterable<$core.List<$core.int>>? rejectedIds, + }) { + final $result = create(); + if (acceptedIds != null) { + $result.acceptedIds.addAll(acceptedIds); + } + if (rejectedIds != null) { + $result.rejectedIds.addAll(rejectedIds); + } + return $result; + } Message_ControlModeration._() : super(); factory Message_ControlModeration.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory Message_ControlModeration.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -900,8 +1162,17 @@ class Message_ControlModeration extends $pb.GeneratedMessage { $core.List<$core.List<$core.int>> get rejectedIds => $_getList(1); } +/// A 'read receipt' control message class Message_ControlReadReceipt extends $pb.GeneratedMessage { - factory Message_ControlReadReceipt() => create(); + factory Message_ControlReadReceipt({ + $core.Iterable<$core.List<$core.int>>? readIds, + }) { + final $result = create(); + if (readIds != null) { + $result.readIds.addAll(readIds); + } + return $result; + } Message_ControlReadReceipt._() : super(); factory Message_ControlReadReceipt.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory Message_ControlReadReceipt.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -948,8 +1219,61 @@ enum Message_Kind { notSet } +/// A single message as part of a series of messages class Message extends $pb.GeneratedMessage { - factory Message() => create(); + factory Message({ + $core.List<$core.int>? id, + $0.TypedKey? author, + $fixnum.Int64? timestamp, + Message_Text? text, + Message_Secret? secret, + Message_ControlDelete? delete, + Message_ControlErase? erase, + Message_ControlSettings? settings, + Message_ControlPermissions? permissions, + Message_ControlMembership? membership, + Message_ControlModeration? moderation, + $0.Signature? signature, + }) { + final $result = create(); + if (id != null) { + $result.id = id; + } + if (author != null) { + $result.author = author; + } + if (timestamp != null) { + $result.timestamp = timestamp; + } + if (text != null) { + $result.text = text; + } + if (secret != null) { + $result.secret = secret; + } + if (delete != null) { + $result.delete = delete; + } + if (erase != null) { + $result.erase = erase; + } + if (settings != null) { + $result.settings = settings; + } + if (permissions != null) { + $result.permissions = permissions; + } + if (membership != null) { + $result.membership = membership; + } + if (moderation != null) { + $result.moderation = moderation; + } + if (signature != null) { + $result.signature = signature; + } + return $result; + } 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); @@ -1006,6 +1330,8 @@ class Message extends $pb.GeneratedMessage { Message_Kind whichKind() => _Message_KindByTag[$_whichOneof(0)]!; void clearKind() => clearField($_whichOneof(0)); + /// Unique id for this author stream + /// Calculated from the hash of the previous message from this author @$pb.TagNumber(1) $core.List<$core.int> get id => $_getN(0); @$pb.TagNumber(1) @@ -1015,6 +1341,7 @@ class Message extends $pb.GeneratedMessage { @$pb.TagNumber(1) void clearId() => clearField(1); + /// Author of the message (identity public key) @$pb.TagNumber(2) $0.TypedKey get author => $_getN(1); @$pb.TagNumber(2) @@ -1026,6 +1353,7 @@ class Message extends $pb.GeneratedMessage { @$pb.TagNumber(2) $0.TypedKey ensureAuthor() => $_ensure(1); + /// Time the message was sent according to sender @$pb.TagNumber(3) $fixnum.Int64 get timestamp => $_getI64(2); @$pb.TagNumber(3) @@ -1123,6 +1451,7 @@ class Message extends $pb.GeneratedMessage { @$pb.TagNumber(11) Message_ControlModeration ensureModeration() => $_ensure(10); + /// Author signature over all of the fields and attachment signatures @$pb.TagNumber(12) $0.Signature get signature => $_getN(11); @$pb.TagNumber(12) @@ -1135,8 +1464,21 @@ class Message extends $pb.GeneratedMessage { $0.Signature ensureSignature() => $_ensure(11); } +/// Locally stored messages for chats class ReconciledMessage extends $pb.GeneratedMessage { - factory ReconciledMessage() => create(); + factory ReconciledMessage({ + Message? content, + $fixnum.Int64? reconciledTime, + }) { + final $result = create(); + if (content != null) { + $result.content = content; + } + if (reconciledTime != null) { + $result.reconciledTime = reconciledTime; + } + return $result; + } ReconciledMessage._() : super(); factory ReconciledMessage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory ReconciledMessage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -1168,6 +1510,7 @@ class ReconciledMessage extends $pb.GeneratedMessage { static ReconciledMessage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static ReconciledMessage? _defaultInstance; + /// The message as sent @$pb.TagNumber(1) Message get content => $_getN(0); @$pb.TagNumber(1) @@ -1179,6 +1522,7 @@ class ReconciledMessage extends $pb.GeneratedMessage { @$pb.TagNumber(1) Message ensureContent() => $_ensure(0); + /// The timestamp the message was reconciled @$pb.TagNumber(2) $fixnum.Int64 get reconciledTime => $_getI64(1); @$pb.TagNumber(2) @@ -1189,8 +1533,36 @@ class ReconciledMessage extends $pb.GeneratedMessage { void clearReconciledTime() => clearField(2); } +/// 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 +/// * Group chat messages +/// +/// DHT Schema: SMPL(0,1,[identityPublicKey]) +/// DHT Key (UnicastOutbox): localConversation +/// DHT Secret: None +/// Encryption: DH(IdentityA, IdentityB) class Conversation extends $pb.GeneratedMessage { - factory Conversation() => create(); + factory Conversation({ + Profile? profile, + $core.String? superIdentityJson, + $0.TypedKey? messages, + }) { + final $result = create(); + if (profile != null) { + $result.profile = profile; + } + if (superIdentityJson != null) { + $result.superIdentityJson = superIdentityJson; + } + if (messages != null) { + $result.messages = messages; + } + return $result; + } 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); @@ -1223,6 +1595,7 @@ class Conversation extends $pb.GeneratedMessage { static Conversation getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Conversation? _defaultInstance; + /// Profile to publish to friend @$pb.TagNumber(1) Profile get profile => $_getN(0); @$pb.TagNumber(1) @@ -1234,6 +1607,7 @@ class Conversation extends $pb.GeneratedMessage { @$pb.TagNumber(1) Profile ensureProfile() => $_ensure(0); + /// SuperIdentity (JSON) to publish to friend or chat room @$pb.TagNumber(2) $core.String get superIdentityJson => $_getSZ(1); @$pb.TagNumber(2) @@ -1243,6 +1617,7 @@ class Conversation extends $pb.GeneratedMessage { @$pb.TagNumber(2) void clearSuperIdentityJson() => clearField(2); + /// Messages DHTLog @$pb.TagNumber(3) $0.TypedKey get messages => $_getN(2); @$pb.TagNumber(3) @@ -1255,8 +1630,21 @@ class Conversation extends $pb.GeneratedMessage { $0.TypedKey ensureMessages() => $_ensure(2); } +/// A member of chat which may or may not be associated with a contact class ChatMember extends $pb.GeneratedMessage { - factory ChatMember() => create(); + factory ChatMember({ + $0.TypedKey? remoteIdentityPublicKey, + $0.TypedKey? remoteConversationRecordKey, + }) { + final $result = create(); + if (remoteIdentityPublicKey != null) { + $result.remoteIdentityPublicKey = remoteIdentityPublicKey; + } + if (remoteConversationRecordKey != null) { + $result.remoteConversationRecordKey = remoteConversationRecordKey; + } + return $result; + } ChatMember._() : super(); factory ChatMember.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory ChatMember.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -1288,6 +1676,7 @@ class ChatMember extends $pb.GeneratedMessage { static ChatMember getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static ChatMember? _defaultInstance; + /// The identity public key most recently associated with the chat member @$pb.TagNumber(1) $0.TypedKey get remoteIdentityPublicKey => $_getN(0); @$pb.TagNumber(1) @@ -1299,6 +1688,7 @@ class ChatMember extends $pb.GeneratedMessage { @$pb.TagNumber(1) $0.TypedKey ensureRemoteIdentityPublicKey() => $_ensure(0); + /// Conversation key for the other party @$pb.TagNumber(2) $0.TypedKey get remoteConversationRecordKey => $_getN(1); @$pb.TagNumber(2) @@ -1311,8 +1701,26 @@ class ChatMember extends $pb.GeneratedMessage { $0.TypedKey ensureRemoteConversationRecordKey() => $_ensure(1); } +/// A 1-1 chat +/// Privately encrypted, this is the local user's copy of the chat class DirectChat extends $pb.GeneratedMessage { - factory DirectChat() => create(); + factory DirectChat({ + ChatSettings? settings, + $0.TypedKey? localConversationRecordKey, + ChatMember? remoteMember, + }) { + final $result = create(); + if (settings != null) { + $result.settings = settings; + } + if (localConversationRecordKey != null) { + $result.localConversationRecordKey = localConversationRecordKey; + } + if (remoteMember != null) { + $result.remoteMember = remoteMember; + } + return $result; + } DirectChat._() : super(); factory DirectChat.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory DirectChat.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -1345,6 +1753,7 @@ class DirectChat extends $pb.GeneratedMessage { static DirectChat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static DirectChat? _defaultInstance; + /// Settings @$pb.TagNumber(1) ChatSettings get settings => $_getN(0); @$pb.TagNumber(1) @@ -1356,6 +1765,7 @@ class DirectChat extends $pb.GeneratedMessage { @$pb.TagNumber(1) ChatSettings ensureSettings() => $_ensure(0); + /// Conversation key for this user @$pb.TagNumber(2) $0.TypedKey get localConversationRecordKey => $_getN(1); @$pb.TagNumber(2) @@ -1367,6 +1777,7 @@ class DirectChat extends $pb.GeneratedMessage { @$pb.TagNumber(2) $0.TypedKey ensureLocalConversationRecordKey() => $_ensure(1); + /// Conversation key for the other party @$pb.TagNumber(3) ChatMember get remoteMember => $_getN(2); @$pb.TagNumber(3) @@ -1379,8 +1790,34 @@ class DirectChat extends $pb.GeneratedMessage { ChatMember ensureRemoteMember() => $_ensure(2); } +/// A group chat +/// Privately encrypted, this is the local user's copy of the chat class GroupChat extends $pb.GeneratedMessage { - factory GroupChat() => create(); + factory GroupChat({ + ChatSettings? settings, + Membership? membership, + Permissions? permissions, + $0.TypedKey? localConversationRecordKey, + $core.Iterable? remoteMembers, + }) { + final $result = create(); + if (settings != null) { + $result.settings = settings; + } + if (membership != null) { + $result.membership = membership; + } + if (permissions != null) { + $result.permissions = permissions; + } + if (localConversationRecordKey != null) { + $result.localConversationRecordKey = localConversationRecordKey; + } + if (remoteMembers != null) { + $result.remoteMembers.addAll(remoteMembers); + } + return $result; + } GroupChat._() : super(); factory GroupChat.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory GroupChat.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -1415,6 +1852,7 @@ class GroupChat extends $pb.GeneratedMessage { static GroupChat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static GroupChat? _defaultInstance; + /// Settings @$pb.TagNumber(1) ChatSettings get settings => $_getN(0); @$pb.TagNumber(1) @@ -1426,6 +1864,7 @@ class GroupChat extends $pb.GeneratedMessage { @$pb.TagNumber(1) ChatSettings ensureSettings() => $_ensure(0); + /// Membership @$pb.TagNumber(2) Membership get membership => $_getN(1); @$pb.TagNumber(2) @@ -1437,6 +1876,7 @@ class GroupChat extends $pb.GeneratedMessage { @$pb.TagNumber(2) Membership ensureMembership() => $_ensure(1); + /// Permissions @$pb.TagNumber(3) Permissions get permissions => $_getN(2); @$pb.TagNumber(3) @@ -1448,6 +1888,7 @@ class GroupChat extends $pb.GeneratedMessage { @$pb.TagNumber(3) Permissions ensurePermissions() => $_ensure(2); + /// Conversation key for this user @$pb.TagNumber(4) $0.TypedKey get localConversationRecordKey => $_getN(3); @$pb.TagNumber(4) @@ -1459,6 +1900,7 @@ class GroupChat extends $pb.GeneratedMessage { @$pb.TagNumber(4) $0.TypedKey ensureLocalConversationRecordKey() => $_ensure(3); + /// Conversation keys for the other parties @$pb.TagNumber(5) $core.List get remoteMembers => $_getList(4); } @@ -1469,8 +1911,21 @@ enum Chat_Kind { notSet } +/// Some kind of chat class Chat extends $pb.GeneratedMessage { - factory Chat() => create(); + factory Chat({ + DirectChat? direct, + GroupChat? group, + }) { + final $result = create(); + if (direct != null) { + $result.direct = direct; + } + if (group != null) { + $result.group = group; + } + return $result; + } 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); @@ -1534,8 +1989,45 @@ class Chat extends $pb.GeneratedMessage { GroupChat ensureGroup() => $_ensure(1); } +/// Publicly shared profile information for both contacts and accounts +/// Contains: +/// Name - Friendly name +/// Pronouns - Pronouns of user +/// Icon - Little picture to represent user in contact list class Profile extends $pb.GeneratedMessage { - factory Profile() => create(); + factory Profile({ + $core.String? name, + $core.String? pronouns, + $core.String? about, + $core.String? status, + Availability? availability, + DataReference? avatar, + $fixnum.Int64? timestamp, + }) { + final $result = create(); + if (name != null) { + $result.name = name; + } + if (pronouns != null) { + $result.pronouns = pronouns; + } + if (about != null) { + $result.about = about; + } + if (status != null) { + $result.status = status; + } + if (availability != null) { + $result.availability = availability; + } + if (avatar != null) { + $result.avatar = avatar; + } + if (timestamp != null) { + $result.timestamp = timestamp; + } + return $result; + } 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); @@ -1572,6 +2064,7 @@ class Profile extends $pb.GeneratedMessage { static Profile getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Profile? _defaultInstance; + /// Friendy name (max length 64) @$pb.TagNumber(1) $core.String get name => $_getSZ(0); @$pb.TagNumber(1) @@ -1581,6 +2074,7 @@ class Profile extends $pb.GeneratedMessage { @$pb.TagNumber(1) void clearName() => clearField(1); + /// Pronouns of user (max length 64) @$pb.TagNumber(2) $core.String get pronouns => $_getSZ(1); @$pb.TagNumber(2) @@ -1590,6 +2084,7 @@ class Profile extends $pb.GeneratedMessage { @$pb.TagNumber(2) void clearPronouns() => clearField(2); + /// Description of the user (max length 1024) @$pb.TagNumber(3) $core.String get about => $_getSZ(2); @$pb.TagNumber(3) @@ -1599,6 +2094,7 @@ class Profile extends $pb.GeneratedMessage { @$pb.TagNumber(3) void clearAbout() => clearField(3); + /// Status/away message (max length 128) @$pb.TagNumber(4) $core.String get status => $_getSZ(3); @$pb.TagNumber(4) @@ -1608,6 +2104,7 @@ class Profile extends $pb.GeneratedMessage { @$pb.TagNumber(4) void clearStatus() => clearField(4); + /// Availability @$pb.TagNumber(5) Availability get availability => $_getN(4); @$pb.TagNumber(5) @@ -1617,6 +2114,7 @@ class Profile extends $pb.GeneratedMessage { @$pb.TagNumber(5) void clearAvailability() => clearField(5); + /// Avatar @$pb.TagNumber(6) DataReference get avatar => $_getN(5); @$pb.TagNumber(6) @@ -1628,6 +2126,7 @@ class Profile extends $pb.GeneratedMessage { @$pb.TagNumber(6) DataReference ensureAvatar() => $_ensure(5); + /// Timestamp of last change @$pb.TagNumber(7) $fixnum.Int64 get timestamp => $_getI64(6); @$pb.TagNumber(7) @@ -1638,8 +2137,61 @@ class Profile extends $pb.GeneratedMessage { void clearTimestamp() => clearField(7); } +/// A record of an individual account +/// Pointed to by the identity account map in the identity key +/// +/// DHT Schema: DFLT(1) +/// DHT Private: accountSecretKey class Account extends $pb.GeneratedMessage { - factory Account() => create(); + factory Account({ + Profile? profile, + $core.bool? invisible, + $core.int? autoAwayTimeoutMin, + $1.OwnedDHTRecordPointer? contactList, + $1.OwnedDHTRecordPointer? contactInvitationRecords, + $1.OwnedDHTRecordPointer? chatList, + $1.OwnedDHTRecordPointer? groupChatList, + $core.String? freeMessage, + $core.String? busyMessage, + $core.String? awayMessage, + $core.bool? autodetectAway, + }) { + final $result = create(); + if (profile != null) { + $result.profile = profile; + } + if (invisible != null) { + $result.invisible = invisible; + } + if (autoAwayTimeoutMin != null) { + $result.autoAwayTimeoutMin = autoAwayTimeoutMin; + } + if (contactList != null) { + $result.contactList = contactList; + } + if (contactInvitationRecords != null) { + $result.contactInvitationRecords = contactInvitationRecords; + } + if (chatList != null) { + $result.chatList = chatList; + } + if (groupChatList != null) { + $result.groupChatList = groupChatList; + } + if (freeMessage != null) { + $result.freeMessage = freeMessage; + } + if (busyMessage != null) { + $result.busyMessage = busyMessage; + } + if (awayMessage != null) { + $result.awayMessage = awayMessage; + } + if (autodetectAway != null) { + $result.autodetectAway = autodetectAway; + } + return $result; + } 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); @@ -1680,6 +2232,7 @@ class Account extends $pb.GeneratedMessage { static Account getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Account? _defaultInstance; + /// The user's profile that gets shared with contacts @$pb.TagNumber(1) Profile get profile => $_getN(0); @$pb.TagNumber(1) @@ -1691,6 +2244,7 @@ class Account extends $pb.GeneratedMessage { @$pb.TagNumber(1) Profile ensureProfile() => $_ensure(0); + /// Invisibility makes you always look 'Offline' @$pb.TagNumber(2) $core.bool get invisible => $_getBF(1); @$pb.TagNumber(2) @@ -1700,6 +2254,7 @@ class Account extends $pb.GeneratedMessage { @$pb.TagNumber(2) void clearInvisible() => clearField(2); + /// Auto-away sets 'away' mode after an inactivity time (only if autodetect_away is set) @$pb.TagNumber(3) $core.int get autoAwayTimeoutMin => $_getIZ(2); @$pb.TagNumber(3) @@ -1709,6 +2264,8 @@ class Account extends $pb.GeneratedMessage { @$pb.TagNumber(3) void clearAutoAwayTimeoutMin() => clearField(3); + /// The contacts DHTList for this account + /// DHT Private @$pb.TagNumber(4) $1.OwnedDHTRecordPointer get contactList => $_getN(3); @$pb.TagNumber(4) @@ -1720,6 +2277,8 @@ class Account extends $pb.GeneratedMessage { @$pb.TagNumber(4) $1.OwnedDHTRecordPointer ensureContactList() => $_ensure(3); + /// The ContactInvitationRecord DHTShortArray for this account + /// DHT Private @$pb.TagNumber(5) $1.OwnedDHTRecordPointer get contactInvitationRecords => $_getN(4); @$pb.TagNumber(5) @@ -1731,6 +2290,8 @@ class Account extends $pb.GeneratedMessage { @$pb.TagNumber(5) $1.OwnedDHTRecordPointer ensureContactInvitationRecords() => $_ensure(4); + /// The Chats DHTList for this account + /// DHT Private @$pb.TagNumber(6) $1.OwnedDHTRecordPointer get chatList => $_getN(5); @$pb.TagNumber(6) @@ -1742,6 +2303,8 @@ class Account extends $pb.GeneratedMessage { @$pb.TagNumber(6) $1.OwnedDHTRecordPointer ensureChatList() => $_ensure(5); + /// The GroupChats DHTList for this account + /// DHT Private @$pb.TagNumber(7) $1.OwnedDHTRecordPointer get groupChatList => $_getN(6); @$pb.TagNumber(7) @@ -1753,6 +2316,7 @@ class Account extends $pb.GeneratedMessage { @$pb.TagNumber(7) $1.OwnedDHTRecordPointer ensureGroupChatList() => $_ensure(6); + /// Free message (max length 128) @$pb.TagNumber(8) $core.String get freeMessage => $_getSZ(7); @$pb.TagNumber(8) @@ -1762,6 +2326,7 @@ class Account extends $pb.GeneratedMessage { @$pb.TagNumber(8) void clearFreeMessage() => clearField(8); + /// Busy message (max length 128) @$pb.TagNumber(9) $core.String get busyMessage => $_getSZ(8); @$pb.TagNumber(9) @@ -1771,6 +2336,7 @@ class Account extends $pb.GeneratedMessage { @$pb.TagNumber(9) void clearBusyMessage() => clearField(9); + /// Away message (max length 128) @$pb.TagNumber(10) $core.String get awayMessage => $_getSZ(9); @$pb.TagNumber(10) @@ -1780,6 +2346,7 @@ class Account extends $pb.GeneratedMessage { @$pb.TagNumber(10) void clearAwayMessage() => clearField(10); + /// Auto-detect away @$pb.TagNumber(11) $core.bool get autodetectAway => $_getBF(10); @$pb.TagNumber(11) @@ -1790,8 +2357,51 @@ class Account extends $pb.GeneratedMessage { void clearAutodetectAway() => clearField(11); } +/// A record of a contact that has accepted a contact invitation +/// Contains a copy of the most recent remote profile as well as +/// a locally edited profile. +/// Contains a copy of the most recent identity from the contact's +/// Master identity dht key +/// +/// Stored in ContactList DHTList class Contact extends $pb.GeneratedMessage { - factory Contact() => create(); + factory Contact({ + $core.String? nickname, + Profile? profile, + $core.String? superIdentityJson, + $0.TypedKey? identityPublicKey, + $0.TypedKey? remoteConversationRecordKey, + $0.TypedKey? localConversationRecordKey, + $core.bool? showAvailability, + $core.String? notes, + }) { + final $result = create(); + if (nickname != null) { + $result.nickname = nickname; + } + if (profile != null) { + $result.profile = profile; + } + if (superIdentityJson != null) { + $result.superIdentityJson = superIdentityJson; + } + if (identityPublicKey != null) { + $result.identityPublicKey = identityPublicKey; + } + if (remoteConversationRecordKey != null) { + $result.remoteConversationRecordKey = remoteConversationRecordKey; + } + if (localConversationRecordKey != null) { + $result.localConversationRecordKey = localConversationRecordKey; + } + if (showAvailability != null) { + $result.showAvailability = showAvailability; + } + if (notes != null) { + $result.notes = notes; + } + return $result; + } 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); @@ -1829,6 +2439,7 @@ class Contact extends $pb.GeneratedMessage { static Contact getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Contact? _defaultInstance; + /// Friend's nickname @$pb.TagNumber(1) $core.String get nickname => $_getSZ(0); @$pb.TagNumber(1) @@ -1838,6 +2449,7 @@ class Contact extends $pb.GeneratedMessage { @$pb.TagNumber(1) void clearNickname() => clearField(1); + /// Copy of friend's profile from remote conversation @$pb.TagNumber(2) Profile get profile => $_getN(1); @$pb.TagNumber(2) @@ -1849,6 +2461,7 @@ class Contact extends $pb.GeneratedMessage { @$pb.TagNumber(2) Profile ensureProfile() => $_ensure(1); + /// Copy of friend's SuperIdentity in JSON from remote conversation @$pb.TagNumber(3) $core.String get superIdentityJson => $_getSZ(2); @$pb.TagNumber(3) @@ -1858,6 +2471,7 @@ class Contact extends $pb.GeneratedMessage { @$pb.TagNumber(3) void clearSuperIdentityJson() => clearField(3); + /// Copy of friend's most recent identity public key from their identityMaster @$pb.TagNumber(4) $0.TypedKey get identityPublicKey => $_getN(3); @$pb.TagNumber(4) @@ -1869,6 +2483,7 @@ class Contact extends $pb.GeneratedMessage { @$pb.TagNumber(4) $0.TypedKey ensureIdentityPublicKey() => $_ensure(3); + /// Remote conversation key to sync from friend @$pb.TagNumber(5) $0.TypedKey get remoteConversationRecordKey => $_getN(4); @$pb.TagNumber(5) @@ -1880,6 +2495,7 @@ class Contact extends $pb.GeneratedMessage { @$pb.TagNumber(5) $0.TypedKey ensureRemoteConversationRecordKey() => $_ensure(4); + /// Our conversation key for friend to sync @$pb.TagNumber(6) $0.TypedKey get localConversationRecordKey => $_getN(5); @$pb.TagNumber(6) @@ -1891,6 +2507,7 @@ class Contact extends $pb.GeneratedMessage { @$pb.TagNumber(6) $0.TypedKey ensureLocalConversationRecordKey() => $_ensure(5); + /// Show availability to this contact @$pb.TagNumber(7) $core.bool get showAvailability => $_getBF(6); @$pb.TagNumber(7) @@ -1900,6 +2517,7 @@ class Contact extends $pb.GeneratedMessage { @$pb.TagNumber(7) void clearShowAvailability() => clearField(7); + /// Notes about this friend @$pb.TagNumber(8) $core.String get notes => $_getSZ(7); @$pb.TagNumber(8) @@ -1910,8 +2528,24 @@ class Contact extends $pb.GeneratedMessage { void clearNotes() => clearField(8); } +/// Invitation that is shared for VeilidChat contact connections +/// serialized to QR code or data blob, not send over DHT, out of band. +/// Writer secret is unique to this invitation. Writer public key is in the ContactRequestPrivate +/// in the ContactRequestInbox subkey 0 DHT key class ContactInvitation extends $pb.GeneratedMessage { - factory ContactInvitation() => create(); + factory ContactInvitation({ + $0.TypedKey? contactRequestInboxKey, + $core.List<$core.int>? writerSecret, + }) { + final $result = create(); + if (contactRequestInboxKey != null) { + $result.contactRequestInboxKey = contactRequestInboxKey; + } + if (writerSecret != null) { + $result.writerSecret = writerSecret; + } + return $result; + } 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); @@ -1943,6 +2577,7 @@ class ContactInvitation extends $pb.GeneratedMessage { static ContactInvitation getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static ContactInvitation? _defaultInstance; + /// Contact request DHT record key @$pb.TagNumber(1) $0.TypedKey get contactRequestInboxKey => $_getN(0); @$pb.TagNumber(1) @@ -1954,6 +2589,7 @@ class ContactInvitation extends $pb.GeneratedMessage { @$pb.TagNumber(1) $0.TypedKey ensureContactRequestInboxKey() => $_ensure(0); + /// Writer secret key bytes possibly encrypted with nonce appended @$pb.TagNumber(2) $core.List<$core.int> get writerSecret => $_getN(1); @$pb.TagNumber(2) @@ -1964,8 +2600,21 @@ class ContactInvitation extends $pb.GeneratedMessage { void clearWriterSecret() => clearField(2); } +/// Signature of invitation with identity class SignedContactInvitation extends $pb.GeneratedMessage { - factory SignedContactInvitation() => create(); + factory SignedContactInvitation({ + $core.List<$core.int>? contactInvitation, + $0.Signature? identitySignature, + }) { + final $result = create(); + if (contactInvitation != null) { + $result.contactInvitation = contactInvitation; + } + if (identitySignature != null) { + $result.identitySignature = identitySignature; + } + return $result; + } 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); @@ -1997,6 +2646,7 @@ class SignedContactInvitation extends $pb.GeneratedMessage { static SignedContactInvitation getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static SignedContactInvitation? _defaultInstance; + /// The serialized bytes for the contact invitation @$pb.TagNumber(1) $core.List<$core.int> get contactInvitation => $_getN(0); @$pb.TagNumber(1) @@ -2006,6 +2656,7 @@ class SignedContactInvitation extends $pb.GeneratedMessage { @$pb.TagNumber(1) void clearContactInvitation() => clearField(1); + /// The signature of the contact_invitation bytes with the identity @$pb.TagNumber(2) $0.Signature get identitySignature => $_getN(1); @$pb.TagNumber(2) @@ -2018,8 +2669,22 @@ class SignedContactInvitation extends $pb.GeneratedMessage { $0.Signature ensureIdentitySignature() => $_ensure(1); } +/// Contact request unicastinbox on the DHT +/// DHTSchema: SMPL 1 owner key, 1 writer key symmetrically encrypted with writer secret class ContactRequest extends $pb.GeneratedMessage { - factory ContactRequest() => create(); + factory ContactRequest({ + EncryptionKeyType? encryptionKeyType, + $core.List<$core.int>? private, + }) { + final $result = create(); + if (encryptionKeyType != null) { + $result.encryptionKeyType = encryptionKeyType; + } + if (private != null) { + $result.private = private; + } + return $result; + } 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); @@ -2051,6 +2716,7 @@ class ContactRequest extends $pb.GeneratedMessage { static ContactRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static ContactRequest? _defaultInstance; + /// The kind of encryption used on the unicastinbox writer key @$pb.TagNumber(1) EncryptionKeyType get encryptionKeyType => $_getN(0); @$pb.TagNumber(1) @@ -2060,6 +2726,7 @@ class ContactRequest extends $pb.GeneratedMessage { @$pb.TagNumber(1) void clearEncryptionKeyType() => clearField(1); + /// The private part encoded and symmetrically encrypted with the unicastinbox writer secret @$pb.TagNumber(2) $core.List<$core.int> get private => $_getN(1); @$pb.TagNumber(2) @@ -2070,8 +2737,34 @@ class ContactRequest extends $pb.GeneratedMessage { void clearPrivate() => clearField(2); } +/// The private part of a possibly encrypted contact request +/// Symmetrically encrypted with writer secret class ContactRequestPrivate extends $pb.GeneratedMessage { - factory ContactRequestPrivate() => create(); + factory ContactRequestPrivate({ + $0.CryptoKey? writerKey, + Profile? profile, + $0.TypedKey? superIdentityRecordKey, + $0.TypedKey? chatRecordKey, + $fixnum.Int64? expiration, + }) { + final $result = create(); + if (writerKey != null) { + $result.writerKey = writerKey; + } + if (profile != null) { + $result.profile = profile; + } + if (superIdentityRecordKey != null) { + $result.superIdentityRecordKey = superIdentityRecordKey; + } + if (chatRecordKey != null) { + $result.chatRecordKey = chatRecordKey; + } + if (expiration != null) { + $result.expiration = expiration; + } + return $result; + } 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); @@ -2106,6 +2799,7 @@ class ContactRequestPrivate extends $pb.GeneratedMessage { static ContactRequestPrivate getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static ContactRequestPrivate? _defaultInstance; + /// Writer public key for signing writes to contact request unicastinbox @$pb.TagNumber(1) $0.CryptoKey get writerKey => $_getN(0); @$pb.TagNumber(1) @@ -2117,6 +2811,7 @@ class ContactRequestPrivate extends $pb.GeneratedMessage { @$pb.TagNumber(1) $0.CryptoKey ensureWriterKey() => $_ensure(0); + /// Snapshot of profile @$pb.TagNumber(2) Profile get profile => $_getN(1); @$pb.TagNumber(2) @@ -2128,6 +2823,7 @@ class ContactRequestPrivate extends $pb.GeneratedMessage { @$pb.TagNumber(2) Profile ensureProfile() => $_ensure(1); + /// SuperIdentity DHT record key @$pb.TagNumber(3) $0.TypedKey get superIdentityRecordKey => $_getN(2); @$pb.TagNumber(3) @@ -2139,6 +2835,7 @@ class ContactRequestPrivate extends $pb.GeneratedMessage { @$pb.TagNumber(3) $0.TypedKey ensureSuperIdentityRecordKey() => $_ensure(2); + /// Local chat DHT record key @$pb.TagNumber(4) $0.TypedKey get chatRecordKey => $_getN(3); @$pb.TagNumber(4) @@ -2150,6 +2847,7 @@ class ContactRequestPrivate extends $pb.GeneratedMessage { @$pb.TagNumber(4) $0.TypedKey ensureChatRecordKey() => $_ensure(3); + /// Expiration timestamp @$pb.TagNumber(5) $fixnum.Int64 get expiration => $_getI64(4); @$pb.TagNumber(5) @@ -2160,8 +2858,25 @@ class ContactRequestPrivate extends $pb.GeneratedMessage { void clearExpiration() => clearField(5); } +/// To accept or reject a contact request, fill this out and send to the ContactRequest unicastinbox class ContactResponse extends $pb.GeneratedMessage { - factory ContactResponse() => create(); + factory ContactResponse({ + $core.bool? accept, + $0.TypedKey? superIdentityRecordKey, + $0.TypedKey? remoteConversationRecordKey, + }) { + final $result = create(); + if (accept != null) { + $result.accept = accept; + } + if (superIdentityRecordKey != null) { + $result.superIdentityRecordKey = superIdentityRecordKey; + } + if (remoteConversationRecordKey != null) { + $result.remoteConversationRecordKey = remoteConversationRecordKey; + } + return $result; + } 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); @@ -2194,6 +2909,7 @@ class ContactResponse extends $pb.GeneratedMessage { static ContactResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static ContactResponse? _defaultInstance; + /// Accept or reject @$pb.TagNumber(1) $core.bool get accept => $_getBF(0); @$pb.TagNumber(1) @@ -2203,6 +2919,7 @@ class ContactResponse extends $pb.GeneratedMessage { @$pb.TagNumber(1) void clearAccept() => clearField(1); + /// Remote SuperIdentity DHT record key @$pb.TagNumber(2) $0.TypedKey get superIdentityRecordKey => $_getN(1); @$pb.TagNumber(2) @@ -2214,6 +2931,7 @@ class ContactResponse extends $pb.GeneratedMessage { @$pb.TagNumber(2) $0.TypedKey ensureSuperIdentityRecordKey() => $_ensure(1); + /// Remote chat DHT record key if accepted @$pb.TagNumber(3) $0.TypedKey get remoteConversationRecordKey => $_getN(2); @$pb.TagNumber(3) @@ -2226,8 +2944,22 @@ class ContactResponse extends $pb.GeneratedMessage { $0.TypedKey ensureRemoteConversationRecordKey() => $_ensure(2); } +/// Signature of response with identity +/// Symmetrically encrypted with writer secret class SignedContactResponse extends $pb.GeneratedMessage { - factory SignedContactResponse() => create(); + factory SignedContactResponse({ + $core.List<$core.int>? contactResponse, + $0.Signature? identitySignature, + }) { + final $result = create(); + if (contactResponse != null) { + $result.contactResponse = contactResponse; + } + if (identitySignature != null) { + $result.identitySignature = identitySignature; + } + return $result; + } 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); @@ -2259,6 +2991,7 @@ class SignedContactResponse extends $pb.GeneratedMessage { static SignedContactResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static SignedContactResponse? _defaultInstance; + /// Serialized bytes for ContactResponse @$pb.TagNumber(1) $core.List<$core.int> get contactResponse => $_getN(0); @$pb.TagNumber(1) @@ -2268,6 +3001,7 @@ class SignedContactResponse extends $pb.GeneratedMessage { @$pb.TagNumber(1) void clearContactResponse() => clearField(1); + /// Signature of the contact_accept bytes with the identity @$pb.TagNumber(2) $0.Signature get identitySignature => $_getN(1); @$pb.TagNumber(2) @@ -2280,8 +3014,41 @@ class SignedContactResponse extends $pb.GeneratedMessage { $0.Signature ensureIdentitySignature() => $_ensure(1); } +/// Contact request record kept in Account DHTList to keep track of extant contact invitations class ContactInvitationRecord extends $pb.GeneratedMessage { - factory ContactInvitationRecord() => create(); + factory ContactInvitationRecord({ + $1.OwnedDHTRecordPointer? contactRequestInbox, + $0.CryptoKey? writerKey, + $0.CryptoKey? writerSecret, + $0.TypedKey? localConversationRecordKey, + $fixnum.Int64? expiration, + $core.List<$core.int>? invitation, + $core.String? message, + }) { + final $result = create(); + if (contactRequestInbox != null) { + $result.contactRequestInbox = contactRequestInbox; + } + if (writerKey != null) { + $result.writerKey = writerKey; + } + if (writerSecret != null) { + $result.writerSecret = writerSecret; + } + if (localConversationRecordKey != null) { + $result.localConversationRecordKey = localConversationRecordKey; + } + if (expiration != null) { + $result.expiration = expiration; + } + if (invitation != null) { + $result.invitation = invitation; + } + if (message != null) { + $result.message = message; + } + return $result; + } 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); @@ -2318,6 +3085,7 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { static ContactInvitationRecord getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static ContactInvitationRecord? _defaultInstance; + /// Contact request unicastinbox DHT record key (parent is accountkey) @$pb.TagNumber(1) $1.OwnedDHTRecordPointer get contactRequestInbox => $_getN(0); @$pb.TagNumber(1) @@ -2329,6 +3097,7 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { @$pb.TagNumber(1) $1.OwnedDHTRecordPointer ensureContactRequestInbox() => $_ensure(0); + /// Writer key sent to contact for the contact_request_inbox smpl inbox subkey @$pb.TagNumber(2) $0.CryptoKey get writerKey => $_getN(1); @$pb.TagNumber(2) @@ -2340,6 +3109,7 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { @$pb.TagNumber(2) $0.CryptoKey ensureWriterKey() => $_ensure(1); + /// Writer secret sent encrypted in the invitation @$pb.TagNumber(3) $0.CryptoKey get writerSecret => $_getN(2); @$pb.TagNumber(3) @@ -2351,6 +3121,7 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { @$pb.TagNumber(3) $0.CryptoKey ensureWriterSecret() => $_ensure(2); + /// Local chat DHT record key (parent is accountkey, will be moved to Contact if accepted) @$pb.TagNumber(4) $0.TypedKey get localConversationRecordKey => $_getN(3); @$pb.TagNumber(4) @@ -2362,6 +3133,7 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { @$pb.TagNumber(4) $0.TypedKey ensureLocalConversationRecordKey() => $_ensure(3); + /// Expiration timestamp @$pb.TagNumber(5) $fixnum.Int64 get expiration => $_getI64(4); @$pb.TagNumber(5) @@ -2371,6 +3143,7 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { @$pb.TagNumber(5) void clearExpiration() => clearField(5); + /// A copy of the raw SignedContactInvitation invitation bytes post-encryption and signing @$pb.TagNumber(6) $core.List<$core.int> get invitation => $_getN(5); @$pb.TagNumber(6) @@ -2380,6 +3153,7 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { @$pb.TagNumber(6) void clearInvitation() => clearField(6); + /// The message sent along with the invitation @$pb.TagNumber(7) $core.String get message => $_getSZ(6); @$pb.TagNumber(7) diff --git a/lib/proto/veilidchat.pbenum.dart b/lib/proto/veilidchat.pbenum.dart index 9133788..42009e8 100644 --- a/lib/proto/veilidchat.pbenum.dart +++ b/lib/proto/veilidchat.pbenum.dart @@ -4,7 +4,7 @@ // // @dart = 2.12 -// ignore_for_file: annotate_overrides, camel_case_types +// ignore_for_file: annotate_overrides, camel_case_types, comment_references // ignore_for_file: constant_identifier_names, library_prefixes // ignore_for_file: non_constant_identifier_names, prefer_final_fields // ignore_for_file: unnecessary_import, unnecessary_this, unused_import @@ -13,6 +13,7 @@ import 'dart:core' as $core; import 'package:protobuf/protobuf.dart' as $pb; +/// Contact availability class Availability extends $pb.ProtobufEnum { static const Availability AVAILABILITY_UNSPECIFIED = Availability._(0, _omitEnumNames ? '' : 'AVAILABILITY_UNSPECIFIED'); static const Availability AVAILABILITY_OFFLINE = Availability._(1, _omitEnumNames ? '' : 'AVAILABILITY_OFFLINE'); @@ -34,6 +35,7 @@ class Availability extends $pb.ProtobufEnum { const Availability._($core.int v, $core.String n) : super(v, n); } +/// Encryption used on secret keys class EncryptionKeyType extends $pb.ProtobufEnum { static const EncryptionKeyType ENCRYPTION_KEY_TYPE_UNSPECIFIED = EncryptionKeyType._(0, _omitEnumNames ? '' : 'ENCRYPTION_KEY_TYPE_UNSPECIFIED'); static const EncryptionKeyType ENCRYPTION_KEY_TYPE_NONE = EncryptionKeyType._(1, _omitEnumNames ? '' : 'ENCRYPTION_KEY_TYPE_NONE'); @@ -53,6 +55,7 @@ class EncryptionKeyType extends $pb.ProtobufEnum { const EncryptionKeyType._($core.int v, $core.String n) : super(v, n); } +/// Scope of a chat class Scope extends $pb.ProtobufEnum { static const Scope WATCHERS = Scope._(0, _omitEnumNames ? '' : 'WATCHERS'); static const Scope MODERATED = Scope._(1, _omitEnumNames ? '' : 'MODERATED'); diff --git a/lib/proto/veilidchat.pbjson.dart b/lib/proto/veilidchat.pbjson.dart index ec327f4..e102d40 100644 --- a/lib/proto/veilidchat.pbjson.dart +++ b/lib/proto/veilidchat.pbjson.dart @@ -4,7 +4,7 @@ // // @dart = 2.12 -// ignore_for_file: annotate_overrides, camel_case_types +// ignore_for_file: annotate_overrides, camel_case_types, comment_references // ignore_for_file: constant_identifier_names, library_prefixes // ignore_for_file: non_constant_identifier_names, prefer_final_fields // ignore_for_file: unnecessary_import, unnecessary_this, unused_import diff --git a/lib/proto/veilidchat.pbserver.dart b/lib/proto/veilidchat.pbserver.dart index 02a9ae4..047feed 100644 --- a/lib/proto/veilidchat.pbserver.dart +++ b/lib/proto/veilidchat.pbserver.dart @@ -4,7 +4,7 @@ // // @dart = 2.12 -// ignore_for_file: annotate_overrides, camel_case_types +// ignore_for_file: annotate_overrides, camel_case_types, comment_references // ignore_for_file: constant_identifier_names // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes // ignore_for_file: non_constant_identifier_names, prefer_final_fields diff --git a/lib/router/cubits/router_cubit.freezed.dart b/lib/router/cubits/router_cubit.freezed.dart index e44cd91..8377607 100644 --- a/lib/router/cubits/router_cubit.freezed.dart +++ b/lib/router/cubits/router_cubit.freezed.dart @@ -22,8 +22,12 @@ RouterState _$RouterStateFromJson(Map json) { mixin _$RouterState { bool get hasAnyAccount => throw _privateConstructorUsedError; + /// Serializes this RouterState to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of RouterState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $RouterStateCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -47,6 +51,8 @@ class _$RouterStateCopyWithImpl<$Res, $Val extends RouterState> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of RouterState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -80,6 +86,8 @@ class __$$RouterStateImplCopyWithImpl<$Res> _$RouterStateImpl _value, $Res Function(_$RouterStateImpl) _then) : super(_value, _then); + /// Create a copy of RouterState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -127,11 +135,13 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState { other.hasAnyAccount == hasAnyAccount)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, hasAnyAccount); - @JsonKey(ignore: true) + /// Create a copy of RouterState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$RouterStateImplCopyWith<_$RouterStateImpl> get copyWith => @@ -154,8 +164,11 @@ abstract class _RouterState implements RouterState { @override bool get hasAnyAccount; + + /// Create a copy of RouterState + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$RouterStateImplCopyWith<_$RouterStateImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/settings/models/preferences.freezed.dart b/lib/settings/models/preferences.freezed.dart index 1735d45..a7ebed3 100644 --- a/lib/settings/models/preferences.freezed.dart +++ b/lib/settings/models/preferences.freezed.dart @@ -24,8 +24,12 @@ mixin _$LockPreference { bool get lockWhenSwitching => throw _privateConstructorUsedError; bool get lockWithSystemLock => throw _privateConstructorUsedError; + /// Serializes this LockPreference to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of LockPreference + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $LockPreferenceCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -52,6 +56,8 @@ class _$LockPreferenceCopyWithImpl<$Res, $Val extends LockPreference> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of LockPreference + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -98,6 +104,8 @@ class __$$LockPreferenceImplCopyWithImpl<$Res> _$LockPreferenceImpl _value, $Res Function(_$LockPreferenceImpl) _then) : super(_value, _then); + /// Create a copy of LockPreference + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -161,12 +169,14 @@ class _$LockPreferenceImpl implements _LockPreference { other.lockWithSystemLock == lockWithSystemLock)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, inactivityLockSecs, lockWhenSwitching, lockWithSystemLock); - @JsonKey(ignore: true) + /// Create a copy of LockPreference + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$LockPreferenceImplCopyWith<_$LockPreferenceImpl> get copyWith => @@ -196,8 +206,11 @@ abstract class _LockPreference implements LockPreference { bool get lockWhenSwitching; @override bool get lockWithSystemLock; + + /// Create a copy of LockPreference + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$LockPreferenceImplCopyWith<_$LockPreferenceImpl> get copyWith => throw _privateConstructorUsedError; } @@ -215,8 +228,12 @@ mixin _$Preferences { NotificationsPreference get notificationsPreference => throw _privateConstructorUsedError; + /// Serializes this Preferences to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of Preferences + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $PreferencesCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -248,6 +265,8 @@ class _$PreferencesCopyWithImpl<$Res, $Val extends Preferences> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of Preferences + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -276,6 +295,8 @@ class _$PreferencesCopyWithImpl<$Res, $Val extends Preferences> ) as $Val); } + /// Create a copy of Preferences + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $ThemePreferencesCopyWith<$Res> get themePreference { @@ -284,6 +305,8 @@ class _$PreferencesCopyWithImpl<$Res, $Val extends Preferences> }); } + /// Create a copy of Preferences + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $LockPreferenceCopyWith<$Res> get lockPreference { @@ -292,6 +315,8 @@ class _$PreferencesCopyWithImpl<$Res, $Val extends Preferences> }); } + /// Create a copy of Preferences + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $NotificationsPreferenceCopyWith<$Res> get notificationsPreference { @@ -332,6 +357,8 @@ class __$$PreferencesImplCopyWithImpl<$Res> _$PreferencesImpl _value, $Res Function(_$PreferencesImpl) _then) : super(_value, _then); + /// Create a copy of Preferences + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -407,12 +434,14 @@ class _$PreferencesImpl implements _Preferences { other.notificationsPreference == notificationsPreference)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, themePreference, languagePreference, lockPreference, notificationsPreference); - @JsonKey(ignore: true) + /// Create a copy of Preferences + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$PreferencesImplCopyWith<_$PreferencesImpl> get copyWith => @@ -445,8 +474,11 @@ abstract class _Preferences implements Preferences { LockPreference get lockPreference; @override NotificationsPreference get notificationsPreference; + + /// Create a copy of Preferences + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$PreferencesImplCopyWith<_$PreferencesImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/theme/models/chat_theme.dart b/lib/theme/models/chat_theme.dart index 8623427..a7039e5 100644 --- a/lib/theme/models/chat_theme.dart +++ b/lib/theme/models/chat_theme.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_chat_ui/flutter_chat_ui.dart'; -import 'scale_scheme.dart'; +import 'scale_theme/scale_scheme.dart'; ChatTheme makeChatTheme( ScaleScheme scale, ScaleConfig scaleConfig, TextTheme textTheme) => diff --git a/lib/theme/models/contrast_generator.dart b/lib/theme/models/contrast_generator.dart index 4271c1a..b71ebea 100644 --- a/lib/theme/models/contrast_generator.dart +++ b/lib/theme/models/contrast_generator.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'radix_generator.dart'; -import 'scale_color.dart'; -import 'scale_input_decorator_theme.dart'; -import 'scale_scheme.dart'; -import 'scale_theme.dart'; +import 'scale_theme/scale_color.dart'; +import 'scale_theme/scale_input_decorator_theme.dart'; +import 'scale_theme/scale_scheme.dart'; +import 'scale_theme/scale_theme.dart'; ScaleColor _contrastScaleColor( {required Brightness brightness, diff --git a/lib/theme/models/models.dart b/lib/theme/models/models.dart index 45b54f9..be80542 100644 --- a/lib/theme/models/models.dart +++ b/lib/theme/models/models.dart @@ -1,7 +1,4 @@ export 'chat_theme.dart'; export 'radix_generator.dart'; -export 'scale_color.dart'; -export 'scale_scheme.dart'; -export 'scale_theme.dart'; -export 'slider_tile.dart'; +export 'scale_theme/scale_theme.dart'; export 'theme_preference.dart'; diff --git a/lib/theme/models/radix_generator.dart b/lib/theme/models/radix_generator.dart index a2bdbb1..3dac7bc 100644 --- a/lib/theme/models/radix_generator.dart +++ b/lib/theme/models/radix_generator.dart @@ -1,13 +1,14 @@ import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:radix_colors/radix_colors.dart'; import '../../tools/tools.dart'; -import 'scale_color.dart'; -import 'scale_input_decorator_theme.dart'; -import 'scale_scheme.dart'; -import 'scale_theme.dart'; +import 'scale_theme/scale_color.dart'; +import 'scale_theme/scale_input_decorator_theme.dart'; +import 'scale_theme/scale_scheme.dart'; +import 'scale_theme/scale_theme.dart'; enum RadixThemeColor { scarlet, // red + violet + tomato @@ -571,7 +572,11 @@ RadixScheme _radixScheme(Brightness brightness, RadixThemeColor themeColor) { TextTheme makeRadixTextTheme(Brightness brightness) { late final TextTheme textTheme; - if (Platform.isIOS) { + if (kIsWeb) { + textTheme = (brightness == Brightness.light) + ? Typography.blackHelsinki + : Typography.whiteHelsinki; + } else if (Platform.isIOS) { textTheme = (brightness == Brightness.light) ? Typography.blackCupertino : Typography.whiteCupertino; diff --git a/lib/theme/models/scale_color.dart b/lib/theme/models/scale_theme/scale_color.dart similarity index 100% rename from lib/theme/models/scale_color.dart rename to lib/theme/models/scale_theme/scale_color.dart diff --git a/lib/theme/models/scale_theme/scale_custom_dropdown_theme.dart b/lib/theme/models/scale_theme/scale_custom_dropdown_theme.dart new file mode 100644 index 0000000..94764a5 --- /dev/null +++ b/lib/theme/models/scale_theme/scale_custom_dropdown_theme.dart @@ -0,0 +1,93 @@ +import 'package:animated_custom_dropdown/custom_dropdown.dart'; +import 'package:flutter/material.dart'; + +import 'scale_scheme.dart'; +import 'scale_theme.dart'; + +class ScaleCustomDropdownTheme { + ScaleCustomDropdownTheme({ + required this.decoration, + required this.closedHeaderPadding, + required this.expandedHeaderPadding, + required this.itemsListPadding, + required this.listItemPadding, + required this.disabledDecoration, + required this.textStyle, + }); + + final CustomDropdownDecoration decoration; + final EdgeInsets closedHeaderPadding; + final EdgeInsets expandedHeaderPadding; + final EdgeInsets itemsListPadding; + final EdgeInsets listItemPadding; + final CustomDropdownDisabledDecoration disabledDecoration; + final TextStyle textStyle; +} + +extension ScaleCustomDropdownThemeExt on ScaleTheme { + ScaleCustomDropdownTheme customDropdownTheme() { + final scale = scheme.primaryScale; + final borderColor = scale.borderText; + final fillColor = scale.subtleBorder; + + // final backgroundColor = config.useVisualIndicators && !selected + // ? tileColor.borderText + // : borderColor; + // final textColor = config.useVisualIndicators && !selected + // ? borderColor + // : tileColor.borderText; + + // final largeTextStyle = textTheme.labelMedium!.copyWith(color: textColor); + // final smallTextStyle = textTheme.labelSmall!.copyWith(color: textColor); + + final border = Border.fromBorderSide(config.useVisualIndicators + ? BorderSide(width: 2, color: borderColor, strokeAlign: 0) + : BorderSide.none); + final borderRadius = BorderRadius.circular(8 * config.borderRadiusScale); + + final decoration = CustomDropdownDecoration( + closedFillColor: fillColor, + expandedFillColor: fillColor, + closedShadow: [], + expandedShadow: [], + closedSuffixIcon: Icon(Icons.arrow_drop_down, color: borderColor), + expandedSuffixIcon: Icon(Icons.arrow_drop_up, color: borderColor), + prefixIcon: null, + closedBorder: border, + closedBorderRadius: borderRadius, + closedErrorBorder: null, + closedErrorBorderRadius: null, + expandedBorder: border, + expandedBorderRadius: borderRadius, + hintStyle: null, + headerStyle: null, + noResultFoundStyle: null, + errorStyle: null, + listItemStyle: null, + overlayScrollbarDecoration: null, + searchFieldDecoration: null, + listItemDecoration: null, + ); + + final disabledDecoration = CustomDropdownDisabledDecoration( + fillColor: null, + shadow: null, + suffixIcon: null, + prefixIcon: null, + border: null, + borderRadius: null, + headerStyle: null, + hintStyle: null, + ); + + return ScaleCustomDropdownTheme( + textStyle: textTheme.labelSmall!.copyWith(color: borderColor), + decoration: decoration, + closedHeaderPadding: const EdgeInsets.all(4), + expandedHeaderPadding: const EdgeInsets.all(4), + itemsListPadding: const EdgeInsets.all(4), + listItemPadding: const EdgeInsets.all(4), + disabledDecoration: disabledDecoration, + ); + } +} diff --git a/lib/theme/models/scale_input_decorator_theme.dart b/lib/theme/models/scale_theme/scale_input_decorator_theme.dart similarity index 60% rename from lib/theme/models/scale_input_decorator_theme.dart rename to lib/theme/models/scale_theme/scale_input_decorator_theme.dart index 265670e..3af1f15 100644 --- a/lib/theme/models/scale_input_decorator_theme.dart +++ b/lib/theme/models/scale_theme/scale_input_decorator_theme.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'scale_scheme.dart'; +import 'scale_theme.dart'; class ScaleInputDecoratorTheme extends InputDecorationTheme { ScaleInputDecoratorTheme( @@ -25,41 +26,41 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme { final TextTheme _textTheme; @override - TextStyle? get hintStyle => MaterialStateTextStyle.resolveWith((states) { - if (states.contains(MaterialState.disabled)) { + TextStyle? get hintStyle => WidgetStateTextStyle.resolveWith((states) { + if (states.contains(WidgetState.disabled)) { return TextStyle(color: _scaleScheme.grayScale.border); } return TextStyle(color: _scaleScheme.primaryScale.border); }); @override - Color? get fillColor => MaterialStateColor.resolveWith((states) { - if (states.contains(MaterialState.disabled)) { - return _scaleScheme.grayScale.primary.withOpacity(0.04); + Color? get fillColor => WidgetStateColor.resolveWith((states) { + if (states.contains(WidgetState.disabled)) { + return _scaleScheme.grayScale.primary.withAlpha(10); } - return _scaleScheme.primaryScale.primary.withOpacity(0.04); + return _scaleScheme.primaryScale.primary.withAlpha(10); }); @override BorderSide? get activeIndicatorBorder => - MaterialStateBorderSide.resolveWith((states) { - if (states.contains(MaterialState.disabled)) { + WidgetStateBorderSide.resolveWith((states) { + if (states.contains(WidgetState.disabled)) { return BorderSide( - color: _scaleScheme.grayScale.border.withAlpha(0x7F)); + color: _scaleScheme.grayScale.border.withAlpha(127)); } - if (states.contains(MaterialState.error)) { - if (states.contains(MaterialState.hovered)) { + if (states.contains(WidgetState.error)) { + if (states.contains(WidgetState.hovered)) { return BorderSide(color: _scaleScheme.errorScale.hoverBorder); } - if (states.contains(MaterialState.focused)) { + if (states.contains(WidgetState.focused)) { return BorderSide(color: _scaleScheme.errorScale.border, width: 2); } return BorderSide(color: _scaleScheme.errorScale.subtleBorder); } - if (states.contains(MaterialState.hovered)) { + if (states.contains(WidgetState.hovered)) { return BorderSide(color: _scaleScheme.secondaryScale.hoverBorder); } - if (states.contains(MaterialState.focused)) { + if (states.contains(WidgetState.focused)) { return BorderSide( color: _scaleScheme.secondaryScale.border, width: 2); } @@ -67,25 +68,24 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme { }); @override - BorderSide? get outlineBorder => - MaterialStateBorderSide.resolveWith((states) { - if (states.contains(MaterialState.disabled)) { + BorderSide? get outlineBorder => WidgetStateBorderSide.resolveWith((states) { + if (states.contains(WidgetState.disabled)) { return BorderSide( - color: _scaleScheme.grayScale.border.withAlpha(0x7F)); + color: _scaleScheme.grayScale.border.withAlpha(127)); } - if (states.contains(MaterialState.error)) { - if (states.contains(MaterialState.hovered)) { + if (states.contains(WidgetState.error)) { + if (states.contains(WidgetState.hovered)) { return BorderSide(color: _scaleScheme.errorScale.hoverBorder); } - if (states.contains(MaterialState.focused)) { + if (states.contains(WidgetState.focused)) { return BorderSide(color: _scaleScheme.errorScale.border, width: 2); } return BorderSide(color: _scaleScheme.errorScale.subtleBorder); } - if (states.contains(MaterialState.hovered)) { + if (states.contains(WidgetState.hovered)) { return BorderSide(color: _scaleScheme.primaryScale.hoverBorder); } - if (states.contains(MaterialState.focused)) { + if (states.contains(WidgetState.focused)) { return BorderSide(color: _scaleScheme.primaryScale.border, width: 2); } return BorderSide(color: _scaleScheme.primaryScale.subtleBorder); @@ -95,51 +95,51 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme { Color? get iconColor => _scaleScheme.primaryScale.primary; @override - Color? get prefixIconColor => MaterialStateColor.resolveWith((states) { - if (states.contains(MaterialState.disabled)) { - return _scaleScheme.primaryScale.primary.withAlpha(0x3F); + Color? get prefixIconColor => WidgetStateColor.resolveWith((states) { + if (states.contains(WidgetState.disabled)) { + return _scaleScheme.primaryScale.primary.withAlpha(127); } - if (states.contains(MaterialState.error)) { + if (states.contains(WidgetState.error)) { return _scaleScheme.errorScale.primary; } return _scaleScheme.primaryScale.primary; }); @override - Color? get suffixIconColor => MaterialStateColor.resolveWith((states) { - if (states.contains(MaterialState.disabled)) { - return _scaleScheme.primaryScale.primary.withAlpha(0x3F); + Color? get suffixIconColor => WidgetStateColor.resolveWith((states) { + if (states.contains(WidgetState.disabled)) { + return _scaleScheme.primaryScale.primary.withAlpha(127); } - if (states.contains(MaterialState.error)) { + if (states.contains(WidgetState.error)) { return _scaleScheme.errorScale.primary; } return _scaleScheme.primaryScale.primary; }); @override - TextStyle? get labelStyle => MaterialStateTextStyle.resolveWith((states) { + TextStyle? get labelStyle => WidgetStateTextStyle.resolveWith((states) { final textStyle = _textTheme.bodyLarge ?? const TextStyle(); - if (states.contains(MaterialState.disabled)) { + if (states.contains(WidgetState.disabled)) { return textStyle.copyWith( - color: _scaleScheme.grayScale.border.withAlpha(0x7F)); + color: _scaleScheme.grayScale.border.withAlpha(127)); } - if (states.contains(MaterialState.error)) { - if (states.contains(MaterialState.hovered)) { + if (states.contains(WidgetState.error)) { + if (states.contains(WidgetState.hovered)) { return textStyle.copyWith( color: _scaleScheme.errorScale.hoverBorder); } - if (states.contains(MaterialState.focused)) { + if (states.contains(WidgetState.focused)) { return textStyle.copyWith( color: _scaleScheme.errorScale.hoverBorder); } return textStyle.copyWith( color: _scaleScheme.errorScale.subtleBorder); } - if (states.contains(MaterialState.hovered)) { + if (states.contains(WidgetState.hovered)) { return textStyle.copyWith( color: _scaleScheme.primaryScale.hoverBorder); } - if (states.contains(MaterialState.focused)) { + if (states.contains(WidgetState.focused)) { return textStyle.copyWith( color: _scaleScheme.primaryScale.hoverBorder); } @@ -150,19 +150,24 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme { TextStyle? get floatingLabelStyle => labelStyle; @override - TextStyle? get helperStyle => MaterialStateTextStyle.resolveWith((states) { + TextStyle? get helperStyle => WidgetStateTextStyle.resolveWith((states) { final textStyle = _textTheme.bodySmall ?? const TextStyle(); - if (states.contains(MaterialState.disabled)) { + if (states.contains(WidgetState.disabled)) { return textStyle.copyWith( - color: _scaleScheme.grayScale.border.withAlpha(0x7F)); + color: _scaleScheme.grayScale.border.withAlpha(127)); } return textStyle.copyWith( - color: _scaleScheme.secondaryScale.border.withAlpha(0x7F)); + color: _scaleScheme.secondaryScale.border.withAlpha(127)); }); @override - TextStyle? get errorStyle => MaterialStateTextStyle.resolveWith((states) { + TextStyle? get errorStyle => WidgetStateTextStyle.resolveWith((states) { final textStyle = _textTheme.bodySmall ?? const TextStyle(); return textStyle.copyWith(color: _scaleScheme.errorScale.primary); }); } + +extension ScaleInputDecoratorThemeExt on ScaleTheme { + ScaleInputDecoratorTheme inputDecoratorTheme() => + ScaleInputDecoratorTheme(scheme, config, textTheme); +} diff --git a/lib/theme/models/scale_scheme.dart b/lib/theme/models/scale_theme/scale_scheme.dart similarity index 100% rename from lib/theme/models/scale_scheme.dart rename to lib/theme/models/scale_theme/scale_scheme.dart diff --git a/lib/theme/models/scale_theme/scale_theme.dart b/lib/theme/models/scale_theme/scale_theme.dart new file mode 100644 index 0000000..d539c86 --- /dev/null +++ b/lib/theme/models/scale_theme/scale_theme.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +import 'scale_scheme.dart'; + +export 'scale_color.dart'; +export 'scale_input_decorator_theme.dart'; +export 'scale_scheme.dart'; +export 'scale_tile_theme.dart'; +export 'scale_toast_theme.dart'; + +class ScaleTheme extends ThemeExtension { + ScaleTheme({ + required this.textTheme, + required this.scheme, + required this.config, + }); + + final TextTheme textTheme; + final ScaleScheme scheme; + final ScaleConfig config; + + @override + ScaleTheme copyWith({ + TextTheme? textTheme, + ScaleScheme? scheme, + ScaleConfig? config, + }) => + ScaleTheme( + textTheme: textTheme ?? this.textTheme, + scheme: scheme ?? this.scheme, + config: config ?? this.config, + ); + + @override + ScaleTheme lerp(ScaleTheme? other, double t) { + if (other is! ScaleTheme) { + return this; + } + return ScaleTheme( + textTheme: TextTheme.lerp(textTheme, other.textTheme, t), + scheme: scheme.lerp(other.scheme, t), + config: config.lerp(other.config, t)); + } +} diff --git a/lib/theme/models/scale_theme.dart b/lib/theme/models/scale_theme/scale_tile_theme.dart similarity index 67% rename from lib/theme/models/scale_theme.dart rename to lib/theme/models/scale_theme/scale_tile_theme.dart index 95f7db9..e7339d1 100644 --- a/lib/theme/models/scale_theme.dart +++ b/lib/theme/models/scale_theme/scale_tile_theme.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'scale_scheme.dart'; +import 'scale_theme.dart'; class ScaleTileTheme { ScaleTileTheme( @@ -19,40 +20,7 @@ class ScaleTileTheme { final TextStyle smallTextStyle; } -class ScaleTheme extends ThemeExtension { - ScaleTheme({ - required this.textTheme, - required this.scheme, - required this.config, - }); - - final TextTheme textTheme; - final ScaleScheme scheme; - final ScaleConfig config; - - @override - ScaleTheme copyWith({ - TextTheme? textTheme, - ScaleScheme? scheme, - ScaleConfig? config, - }) => - ScaleTheme( - textTheme: textTheme ?? this.textTheme, - scheme: scheme ?? this.scheme, - config: config ?? this.config, - ); - - @override - ScaleTheme lerp(ScaleTheme? other, double t) { - if (other is! ScaleTheme) { - return this; - } - return ScaleTheme( - textTheme: TextTheme.lerp(textTheme, other.textTheme, t), - scheme: scheme.lerp(other.scheme, t), - config: config.lerp(other.config, t)); - } - +extension ScaleTileThemeExt on ScaleTheme { ScaleTileTheme tileTheme( {bool disabled = false, bool selected = false, diff --git a/lib/theme/models/scale_theme/scale_toast_theme.dart b/lib/theme/models/scale_theme/scale_toast_theme.dart new file mode 100644 index 0000000..de310d4 --- /dev/null +++ b/lib/theme/models/scale_theme/scale_toast_theme.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; + +import 'scale_scheme.dart'; +import 'scale_theme.dart'; + +enum ScaleToastKind { + info, + error, +} + +class ScaleToastTheme { + ScaleToastTheme( + {required this.primaryColor, + required this.backgroundColor, + required this.foregroundColor, + required this.borderSide, + required this.borderRadius, + required this.padding, + required this.icon, + required this.titleTextStyle, + required this.descriptionTextStyle}); + + final Color primaryColor; + final Color backgroundColor; + final Color foregroundColor; + final BorderSide? borderSide; + final BorderRadiusGeometry borderRadius; + final EdgeInsetsGeometry padding; + final Icon icon; + final TextStyle titleTextStyle; + final TextStyle descriptionTextStyle; +} + +extension ScaleToastThemeExt on ScaleTheme { + ScaleToastTheme toastTheme(ScaleToastKind kind) { + final toastScaleColor = scheme.scale(ScaleKind.tertiary); + + Icon icon; + switch (kind) { + case ScaleToastKind.info: + icon = const Icon(Icons.info, size: 32); + case ScaleToastKind.error: + icon = const Icon(Icons.dangerous, size: 32); + } + + final primaryColor = toastScaleColor.calloutText; + final borderColor = toastScaleColor.border; + final backgroundColor = config.useVisualIndicators + ? toastScaleColor.calloutText + : toastScaleColor.calloutBackground; + final textColor = config.useVisualIndicators + ? toastScaleColor.calloutBackground + : toastScaleColor.calloutText; + final titleColor = config.useVisualIndicators + ? toastScaleColor.calloutBackground + : toastScaleColor.calloutText; + + return ScaleToastTheme( + primaryColor: primaryColor, + backgroundColor: backgroundColor, + foregroundColor: textColor, + borderSide: (config.useVisualIndicators || config.preferBorders) + ? BorderSide(color: borderColor, width: 2) + : const BorderSide(color: Colors.transparent, width: 0), + borderRadius: BorderRadius.circular(12 * config.borderRadiusScale), + padding: const EdgeInsets.all(8), + icon: icon, + titleTextStyle: textTheme.labelMedium!.copyWith(color: titleColor), + descriptionTextStyle: + textTheme.labelMedium!.copyWith(color: textColor)); + } +} diff --git a/lib/theme/models/theme_preference.dart b/lib/theme/models/theme_preference.dart index ec7a40e..9d6f6dc 100644 --- a/lib/theme/models/theme_preference.dart +++ b/lib/theme/models/theme_preference.dart @@ -5,7 +5,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import '../views/widget_helpers.dart'; import 'contrast_generator.dart'; import 'radix_generator.dart'; -import 'scale_scheme.dart'; +import 'scale_theme/scale_scheme.dart'; part 'theme_preference.freezed.dart'; part 'theme_preference.g.dart'; diff --git a/lib/theme/models/theme_preference.freezed.dart b/lib/theme/models/theme_preference.freezed.dart index 97e3f81..657d021 100644 --- a/lib/theme/models/theme_preference.freezed.dart +++ b/lib/theme/models/theme_preference.freezed.dart @@ -25,8 +25,12 @@ mixin _$ThemePreferences { ColorPreference get colorPreference => throw _privateConstructorUsedError; double get displayScale => throw _privateConstructorUsedError; + /// Serializes this ThemePreferences to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of ThemePreferences + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $ThemePreferencesCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -53,6 +57,8 @@ class _$ThemePreferencesCopyWithImpl<$Res, $Val extends ThemePreferences> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of ThemePreferences + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -99,6 +105,8 @@ class __$$ThemePreferencesImplCopyWithImpl<$Res> $Res Function(_$ThemePreferencesImpl) _then) : super(_value, _then); + /// Create a copy of ThemePreferences + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -162,12 +170,14 @@ class _$ThemePreferencesImpl implements _ThemePreferences { other.displayScale == displayScale)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, brightnessPreference, colorPreference, displayScale); - @JsonKey(ignore: true) + /// Create a copy of ThemePreferences + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$ThemePreferencesImplCopyWith<_$ThemePreferencesImpl> get copyWith => @@ -197,8 +207,11 @@ abstract class _ThemePreferences implements ThemePreferences { ColorPreference get colorPreference; @override double get displayScale; + + /// Create a copy of ThemePreferences + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$ThemePreferencesImplCopyWith<_$ThemePreferencesImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/theme/views/option_box.dart b/lib/theme/views/option_box.dart index 508c7ba..06a3293 100644 --- a/lib/theme/views/option_box.dart +++ b/lib/theme/views/option_box.dart @@ -40,7 +40,9 @@ class OptionBox extends StatelessWidget { ElevatedButton( onPressed: _onClick, child: Row(mainAxisSize: MainAxisSize.min, children: [ - Icon(_buttonIcon, size: 24).paddingLTRB(0, 8, 8, 8), + Icon(_buttonIcon, + size: 24, color: scale.primaryScale.appText) + .paddingLTRB(0, 8, 8, 8), Text(textAlign: TextAlign.center, _buttonText) ])).paddingLTRB(0, 12, 0, 0).toCenter() ]).paddingAll(12)) diff --git a/lib/theme/models/slider_tile.dart b/lib/theme/views/slider_tile.dart similarity index 100% rename from lib/theme/models/slider_tile.dart rename to lib/theme/views/slider_tile.dart diff --git a/lib/theme/views/styled_scaffold.dart b/lib/theme/views/styled_scaffold.dart index 9560ecc..9ee3d36 100644 --- a/lib/theme/views/styled_scaffold.dart +++ b/lib/theme/views/styled_scaffold.dart @@ -14,17 +14,22 @@ class StyledScaffold extends StatelessWidget { final enableBorder = !isMobileSize(context); - final scaffold = clipBorder( - clipEnabled: enableBorder, - borderEnabled: scaleConfig.useVisualIndicators, - borderRadius: 16 * scaleConfig.borderRadiusScale, - borderColor: scale.primaryScale.border, - child: Scaffold(appBar: appBar, body: body, key: key)) - .paddingAll(enableBorder ? 32 : 0); + var scaffold = clipBorder( + clipEnabled: enableBorder, + borderEnabled: scaleConfig.useVisualIndicators, + borderRadius: 16 * scaleConfig.borderRadiusScale, + borderColor: scale.primaryScale.border, + child: Scaffold(appBar: appBar, body: body, key: key)); + + if (!scaleConfig.useVisualIndicators) { + scaffold = scaffold.withShadow( + offset: const Offset(0, 16), + shadowColor: scale.primaryScale.primary.withAlpha(0x3F).darken(60)); + } return GestureDetector( onTap: () => FocusManager.instance.primaryFocus?.unfocus(), - child: scaffold); + child: scaffold.paddingAll(enableBorder ? 32 : 0)); } //////////////////////////////////////////////////////////////////////////// diff --git a/lib/theme/views/views.dart b/lib/theme/views/views.dart index e8aa1d8..b5aa809 100644 --- a/lib/theme/views/views.dart +++ b/lib/theme/views/views.dart @@ -8,6 +8,7 @@ export 'pop_control.dart'; export 'recovery_key_widget.dart'; export 'responsive.dart'; export 'scanner_error_widget.dart'; +export 'slider_tile.dart'; export 'styled_alert.dart'; export 'styled_dialog.dart'; export 'styled_scaffold.dart'; diff --git a/lib/theme/views/widget_helpers.dart b/lib/theme/views/widget_helpers.dart index 970e065..1b9e80f 100644 --- a/lib/theme/views/widget_helpers.dart +++ b/lib/theme/views/widget_helpers.dart @@ -500,24 +500,26 @@ const grayColorFilter = ColorFilter.matrix([ 0, ]); -Widget clipBorder({ +Container clipBorder({ required bool clipEnabled, required bool borderEnabled, required double borderRadius, required Color borderColor, required Widget child, }) => - ClipRRect( - borderRadius: clipEnabled - ? BorderRadius.circular(borderRadius) - : BorderRadius.zero, - child: DecoratedBox( - decoration: BoxDecoration(boxShadow: [ - if (borderEnabled) BoxShadow(color: borderColor, spreadRadius: 2) - ]), - child: ClipRRect( + // ignore: avoid_unnecessary_containers, use_decorated_box + Container( + decoration: ShapeDecoration( + color: borderColor, + shape: RoundedRectangleBorder( borderRadius: clipEnabled ? BorderRadius.circular(borderRadius) : BorderRadius.zero, - child: child, - )).paddingAll(clipEnabled && borderEnabled ? 2 : 0)); + )), + child: ClipRRect( + clipBehavior: Clip.hardEdge, + borderRadius: clipEnabled + ? BorderRadius.circular(borderRadius) + : BorderRadius.zero, + child: child) + .paddingAll(clipEnabled && borderEnabled ? 2 : 0)); diff --git a/lib/tools/loggy.dart b/lib/tools/loggy.dart index d8d4880..2730888 100644 --- a/lib/tools/loggy.dart +++ b/lib/tools/loggy.dart @@ -112,7 +112,7 @@ class CallbackPrinter extends LoggyPrinter { void onLog(LogRecord record) { final out = record.pretty().replaceAll('\uFFFD', ''); - if (Platform.isAndroid) { + if (!kIsWeb && Platform.isAndroid) { debugPrint(out); } else { debugPrintSynchronously(out); diff --git a/lib/veilid_processor/models/processor_connection_state.freezed.dart b/lib/veilid_processor/models/processor_connection_state.freezed.dart index d857318..87ad295 100644 --- a/lib/veilid_processor/models/processor_connection_state.freezed.dart +++ b/lib/veilid_processor/models/processor_connection_state.freezed.dart @@ -19,7 +19,9 @@ mixin _$ProcessorConnectionState { VeilidStateAttachment get attachment => throw _privateConstructorUsedError; VeilidStateNetwork get network => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of ProcessorConnectionState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $ProcessorConnectionStateCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -47,6 +49,8 @@ class _$ProcessorConnectionStateCopyWithImpl<$Res, // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of ProcessorConnectionState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -65,6 +69,8 @@ class _$ProcessorConnectionStateCopyWithImpl<$Res, ) as $Val); } + /// Create a copy of ProcessorConnectionState + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $VeilidStateAttachmentCopyWith<$Res> get attachment { @@ -73,6 +79,8 @@ class _$ProcessorConnectionStateCopyWithImpl<$Res, }); } + /// Create a copy of ProcessorConnectionState + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $VeilidStateNetworkCopyWith<$Res> get network { @@ -109,6 +117,8 @@ class __$$ProcessorConnectionStateImplCopyWithImpl<$Res> $Res Function(_$ProcessorConnectionStateImpl) _then) : super(_value, _then); + /// Create a copy of ProcessorConnectionState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -158,7 +168,9 @@ class _$ProcessorConnectionStateImpl extends _ProcessorConnectionState { @override int get hashCode => Object.hash(runtimeType, attachment, network); - @JsonKey(ignore: true) + /// Create a copy of ProcessorConnectionState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$ProcessorConnectionStateImplCopyWith<_$ProcessorConnectionStateImpl> @@ -177,8 +189,11 @@ abstract class _ProcessorConnectionState extends ProcessorConnectionState { VeilidStateAttachment get attachment; @override VeilidStateNetwork get network; + + /// Create a copy of ProcessorConnectionState + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$ProcessorConnectionStateImplCopyWith<_$ProcessorConnectionStateImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/veilid_processor/views/developer.dart b/lib/veilid_processor/views/developer.dart index e40677d..f561f07 100644 --- a/lib/veilid_processor/views/developer.dart +++ b/lib/veilid_processor/views/developer.dart @@ -1,13 +1,11 @@ import 'dart:async'; +import 'package:animated_custom_dropdown/custom_dropdown.dart'; import 'package:ansicolor/ansicolor.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; -import 'package:cool_dropdown/cool_dropdown.dart'; -import 'package:cool_dropdown/models/cool_dropdown_item.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:go_router/go_router.dart'; @@ -17,6 +15,7 @@ import 'package:xterm/xterm.dart'; import '../../layout/layout.dart'; import '../../notifications/notifications.dart'; +import '../../theme/models/scale_theme/scale_custom_dropdown_theme.dart'; import '../../theme/theme.dart'; import '../../tools/tools.dart'; import 'history_text_editing_controller.dart'; @@ -30,6 +29,15 @@ const kDefaultTerminalStyle = TerminalStyle( // height: 1.2, fontFamily: 'Source Code Pro'); +class LogLevelDropdownItem { + const LogLevelDropdownItem( + {required this.label, required this.icon, required this.value}); + + final String label; + final Widget icon; + final LogLevel value; +} + class DeveloperPage extends StatefulWidget { const DeveloperPage({super.key}); @@ -49,7 +57,7 @@ class _DeveloperPageState extends State { }); for (var i = 0; i < logLevels.length; i++) { - _logLevelDropdownItems.add(CoolDropdownItem( + _logLevelDropdownItems.add(LogLevelDropdownItem( label: logLevelName(logLevels[i]), icon: Text(logLevelEmoji(logLevels[i])), value: logLevels[i])); @@ -167,29 +175,28 @@ class _DeveloperPageState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final textTheme = theme.textTheme; final scale = theme.extension()!; + final scaleTheme = theme.extension()!; + final dropdownTheme = scaleTheme.customDropdownTheme(); final scaleConfig = theme.extension()!; - // WidgetsBinding.instance.addPostFrameCallback((_) { - // if (!_isScrolling && _wantsBottom) { - // _scrollToBottom(); - // } - // }); + final hintColor = scaleConfig.useVisualIndicators + ? scale.primaryScale.primaryText + : scale.primaryScale.primary; return Scaffold( - backgroundColor: scale.primaryScale.primary, + backgroundColor: scale.primaryScale.border, appBar: DefaultAppBar( title: Text(translate('developer.title')), leading: IconButton( - icon: Icon(Icons.arrow_back, color: scale.primaryScale.primaryText), + icon: Icon(Icons.arrow_back, color: scale.primaryScale.borderText), onPressed: () => GoRouterHelper(context).pop(), ), actions: [ IconButton( icon: const Icon(Icons.copy), - color: scale.primaryScale.primaryText, - disabledColor: scale.primaryScale.primaryText.withAlpha(0x3F), + color: scale.primaryScale.borderText, + disabledColor: scale.primaryScale.borderText.withAlpha(0x3F), onPressed: _terminalController.selection == null ? null : () async { @@ -197,15 +204,15 @@ class _DeveloperPageState extends State { }), IconButton( icon: const Icon(Icons.copy_all), - color: scale.primaryScale.primaryText, - disabledColor: scale.primaryScale.primaryText.withAlpha(0x3F), + color: scale.primaryScale.borderText, + disabledColor: scale.primaryScale.borderText.withAlpha(0x3F), onPressed: () async { await copyAll(context); }), IconButton( icon: const Icon(Icons.clear_all), - color: scale.primaryScale.primaryText, - disabledColor: scale.primaryScale.primaryText.withAlpha(0x3F), + color: scale.primaryScale.borderText, + disabledColor: scale.primaryScale.borderText.withAlpha(0x3F), onPressed: () async { final confirm = await showConfirmModal( context: context, @@ -216,74 +223,39 @@ class _DeveloperPageState extends State { await clear(context); } }), - CoolDropdown( - controller: _logLevelController, - defaultItem: _logLevelDropdownItems - .singleWhere((x) => x.value == _logLevelDropDown), - onChange: (value) { - setState(() { - _logLevelDropDown = value; - Loggy('').level = getLogOptions(value); - setVeilidLogLevel(value); - _logLevelController.close(); - }); - }, - resultOptions: ResultOptions( - width: 64, - height: 40, - render: ResultRender.icon, - icon: SizedBox( - width: 10, - height: 10, - child: CustomPaint( - painter: DropdownArrowPainter( - color: scale.primaryScale.primaryText))), - textStyle: textTheme.labelMedium! - .copyWith(color: scale.primaryScale.primaryText), - padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), - openBoxDecoration: BoxDecoration( - //color: scale.primaryScale.border, - border: Border.all( - color: scaleConfig.useVisualIndicators - ? scale.primaryScale.hoverBorder - : scale.primaryScale.borderText), - borderRadius: - BorderRadius.circular(8 * scaleConfig.borderRadiusScale), - ), - boxDecoration: BoxDecoration( - //color: scale.primaryScale.hoverBorder, - border: Border.all( - color: scaleConfig.useVisualIndicators - ? scale.primaryScale.hoverBorder - : scale.primaryScale.borderText), - borderRadius: - BorderRadius.circular(8 * scaleConfig.borderRadiusScale), - ), - ), - dropdownOptions: DropdownOptions( - width: 160, - align: DropdownAlign.right, - duration: 150.ms, - color: scale.primaryScale.elementBackground, - borderSide: BorderSide(color: scale.primaryScale.border), - borderRadius: - BorderRadius.circular(8 * scaleConfig.borderRadiusScale), - padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), - ), - dropdownTriangleOptions: const DropdownTriangleOptions( - align: DropdownTriangleAlign.right), - dropdownItemOptions: DropdownItemOptions( - selectedTextStyle: textTheme.labelMedium! - .copyWith(color: scale.primaryScale.appText), - textStyle: textTheme.labelMedium! - .copyWith(color: scale.primaryScale.appText), - selectedBoxDecoration: BoxDecoration( - color: scale.primaryScale.activeElementBackground), - mainAxisAlignment: MainAxisAlignment.spaceBetween, - padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), - selectedPadding: const EdgeInsets.fromLTRB(8, 4, 8, 4)), - dropdownList: _logLevelDropdownItems, - ).paddingLTRB(0, 0, 8, 0) + SizedBox.fromSize( + size: const Size(120, 48), + child: CustomDropdown( + items: _logLevelDropdownItems, + initialItem: _logLevelDropdownItems + .singleWhere((x) => x.value == _logLevelDropDown), + onChanged: (item) { + if (item != null) { + setState(() { + _logLevelDropDown = item.value; + Loggy('').level = getLogOptions(item.value); + setVeilidLogLevel(item.value); + }); + } + }, + headerBuilder: (context, item, enabled) => Row(children: [ + item.icon, + const Spacer(), + Text(item.label).copyWith(style: dropdownTheme.textStyle) + ]), + listItemBuilder: (context, item, enabled, onItemSelect) => + Row(children: [ + item.icon, + const Spacer(), + Text(item.label).copyWith(style: dropdownTheme.textStyle) + ]), + decoration: dropdownTheme.decoration, + disabledDecoration: dropdownTheme.disabledDecoration, + listItemPadding: dropdownTheme.listItemPadding, + itemsListPadding: dropdownTheme.itemsListPadding, + expandedHeaderPadding: dropdownTheme.expandedHeaderPadding, + closedHeaderPadding: dropdownTheme.closedHeaderPadding, + )).paddingLTRB(0, 4, 8, 4), ], ), body: GestureDetector( @@ -312,21 +284,24 @@ class _DeveloperPageState extends State { decoration: InputDecoration( filled: true, contentPadding: const EdgeInsets.fromLTRB(8, 2, 8, 2), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular( - 8 * scaleConfig.borderRadiusScale), - borderSide: BorderSide.none), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular( - 8 * scaleConfig.borderRadiusScale), - ), - fillColor: scale.primaryScale.subtleBackground, + enabledBorder: + const OutlineInputBorder(borderSide: BorderSide.none), + border: + const OutlineInputBorder(borderSide: BorderSide.none), + focusedBorder: + const OutlineInputBorder(borderSide: BorderSide.none), + fillColor: scale.primaryScale.elementBackground, + hoverColor: scale.primaryScale.elementBackground, + hintStyle: scaleTheme.textTheme.labelMedium!.copyWith( + color: scaleConfig.useVisualIndicators + ? hintColor.withAlpha(0x7F) + : hintColor), hintText: translate('developer.command'), suffixIcon: IconButton( icon: Icon(Icons.send, color: _historyController.controller.text.isEmpty - ? scale.primaryScale.primary.withAlpha(0x3F) - : scale.primaryScale.primary), + ? hintColor.withAlpha(0x7F) + : hintColor), onPressed: (_historyController.controller.text.isEmpty || _busy) ? null @@ -366,9 +341,9 @@ class _DeveloperPageState extends State { final _terminalController = TerminalController(); late final HistoryTextEditingController _historyController; - final _logLevelController = DropdownController(duration: 250.ms); - final List> _logLevelDropdownItems = []; + final List _logLevelDropdownItems = []; var _logLevelDropDown = log.level.logLevel; + var _showEllet = false; var _busy = false; diff --git a/lib/veilid_processor/views/signal_strength_meter.dart b/lib/veilid_processor/views/signal_strength_meter.dart index 74230ed..5385bb1 100644 --- a/lib/veilid_processor/views/signal_strength_meter.dart +++ b/lib/veilid_processor/views/signal_strength_meter.dart @@ -34,34 +34,33 @@ class SignalStrengthMeterWidget extends StatelessWidget { case AttachmentState.detached: iconWidget = Icon(Icons.signal_cellular_nodata, size: iconSize, - color: this.color ?? scale.primaryScale.primaryText); + color: this.color ?? scale.primaryScale.borderText); return; case AttachmentState.detaching: iconWidget = Icon(Icons.signal_cellular_off, size: iconSize, - color: this.color ?? scale.primaryScale.primaryText); + color: this.color ?? scale.primaryScale.borderText); return; case AttachmentState.attaching: value = 0; - color = this.color ?? scale.primaryScale.primaryText; + color = this.color ?? scale.primaryScale.borderText; case AttachmentState.attachedWeak: value = 1; - color = this.color ?? scale.primaryScale.primaryText; + color = this.color ?? scale.primaryScale.borderText; case AttachmentState.attachedStrong: value = 2; - color = this.color ?? scale.primaryScale.primaryText; + color = this.color ?? scale.primaryScale.borderText; case AttachmentState.attachedGood: value = 3; - color = this.color ?? scale.primaryScale.primaryText; + color = this.color ?? scale.primaryScale.borderText; case AttachmentState.fullyAttached: value = 4; - color = this.color ?? scale.primaryScale.primaryText; + color = this.color ?? scale.primaryScale.borderText; case AttachmentState.overAttached: value = 4; - color = this.color ?? scale.primaryScale.primaryText; + color = this.color ?? scale.primaryScale.borderText; } - inactiveColor = - this.inactiveColor ?? scale.primaryScale.primaryText; + inactiveColor = this.inactiveColor ?? scale.grayScale.borderText; iconWidget = SignalStrengthIndicator.bars( value: value, diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c94b139..408e781 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -48,6 +48,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/packages/veilid_support/example/integration_test/app_test.dart b/packages/veilid_support/example/integration_test/app_test.dart index a0f3b7f..6912fd3 100644 --- a/packages/veilid_support/example/integration_test/app_test.dart +++ b/packages/veilid_support/example/integration_test/app_test.dart @@ -36,116 +36,116 @@ void main() { setUpAll(veilidFixture.attach); tearDownAll(veilidFixture.detach); - // group('TableDB Tests', () { - // group('TableDBArray Tests', () { - // // test('create/delete TableDBArray', testTableDBArrayCreateDelete); + group('TableDB Tests', () { + group('TableDBArray Tests', () { + // test('create/delete TableDBArray', testTableDBArrayCreateDelete); - // group('TableDBArray Add/Get Tests', () { - // for (final params in [ - // // - // (99, 3, 15), - // (100, 4, 16), - // (101, 5, 17), - // // - // (511, 3, 127), - // (512, 4, 128), - // (513, 5, 129), - // // - // (4095, 3, 1023), - // (4096, 4, 1024), - // (4097, 5, 1025), - // // - // (65535, 3, 16383), - // (65536, 4, 16384), - // (65537, 5, 16385), - // ]) { - // final count = params.$1; - // final singles = params.$2; - // final batchSize = params.$3; + group('TableDBArray Add/Get Tests', () { + for (final params in [ + // + (99, 3, 15), + (100, 4, 16), + (101, 5, 17), + // + (511, 3, 127), + (512, 4, 128), + (513, 5, 129), + // + (4095, 3, 1023), + (4096, 4, 1024), + (4097, 5, 1025), + // + (65535, 3, 16383), + (65536, 4, 16384), + (65537, 5, 16385), + ]) { + final count = params.$1; + final singles = params.$2; + final batchSize = params.$3; - // test( - // timeout: const Timeout(Duration(seconds: 480)), - // 'add/remove TableDBArray count = $count batchSize=$batchSize', - // makeTestTableDBArrayAddGetClear( - // count: count, - // singles: singles, - // batchSize: batchSize, - // crypto: const VeilidCryptoPublic()), - // ); - // } - // }); + test( + timeout: const Timeout(Duration(seconds: 480)), + 'add/remove TableDBArray count = $count batchSize=$batchSize', + makeTestTableDBArrayAddGetClear( + count: count, + singles: singles, + batchSize: batchSize, + crypto: const VeilidCryptoPublic()), + ); + } + }); - // group('TableDBArray Insert Tests', () { - // for (final params in [ - // // - // (99, 3, 15), - // (100, 4, 16), - // (101, 5, 17), - // // - // (511, 3, 127), - // (512, 4, 128), - // (513, 5, 129), - // // - // (4095, 3, 1023), - // (4096, 4, 1024), - // (4097, 5, 1025), - // // - // (65535, 3, 16383), - // (65536, 4, 16384), - // (65537, 5, 16385), - // ]) { - // final count = params.$1; - // final singles = params.$2; - // final batchSize = params.$3; + group('TableDBArray Insert Tests', () { + for (final params in [ + // + (99, 3, 15), + (100, 4, 16), + (101, 5, 17), + // + (511, 3, 127), + (512, 4, 128), + (513, 5, 129), + // + (4095, 3, 1023), + (4096, 4, 1024), + (4097, 5, 1025), + // + (65535, 3, 16383), + (65536, 4, 16384), + (65537, 5, 16385), + ]) { + final count = params.$1; + final singles = params.$2; + final batchSize = params.$3; - // test( - // timeout: const Timeout(Duration(seconds: 480)), - // 'insert TableDBArray count=$count singles=$singles batchSize=$batchSize', - // makeTestTableDBArrayInsert( - // count: count, - // singles: singles, - // batchSize: batchSize, - // crypto: const VeilidCryptoPublic()), - // ); - // } - // }); + test( + timeout: const Timeout(Duration(seconds: 480)), + 'insert TableDBArray count=$count singles=$singles batchSize=$batchSize', + makeTestTableDBArrayInsert( + count: count, + singles: singles, + batchSize: batchSize, + crypto: const VeilidCryptoPublic()), + ); + } + }); - // group('TableDBArray Remove Tests', () { - // for (final params in [ - // // - // (99, 3, 15), - // (100, 4, 16), - // (101, 5, 17), - // // - // (511, 3, 127), - // (512, 4, 128), - // (513, 5, 129), - // // - // (4095, 3, 1023), - // (4096, 4, 1024), - // (4097, 5, 1025), - // // - // (16383, 3, 4095), - // (16384, 4, 4096), - // (16385, 5, 4097), - // ]) { - // final count = params.$1; - // final singles = params.$2; - // final batchSize = params.$3; + group('TableDBArray Remove Tests', () { + for (final params in [ + // + (99, 3, 15), + (100, 4, 16), + (101, 5, 17), + // + (511, 3, 127), + (512, 4, 128), + (513, 5, 129), + // + (4095, 3, 1023), + (4096, 4, 1024), + (4097, 5, 1025), + // + (16383, 3, 4095), + (16384, 4, 4096), + (16385, 5, 4097), + ]) { + final count = params.$1; + final singles = params.$2; + final batchSize = params.$3; - // test( - // timeout: const Timeout(Duration(seconds: 480)), - // 'remove TableDBArray count=$count singles=$singles batchSize=$batchSize', - // makeTestTableDBArrayRemove( - // count: count, - // singles: singles, - // batchSize: batchSize, - // crypto: const VeilidCryptoPublic()), - // ); - // } - // }); - // }); - // }); + test( + timeout: const Timeout(Duration(seconds: 480)), + 'remove TableDBArray count=$count singles=$singles batchSize=$batchSize', + makeTestTableDBArrayRemove( + count: count, + singles: singles, + batchSize: batchSize, + crypto: const VeilidCryptoPublic()), + ); + } + }); + }); + }); group('DHT Support Tests', () { setUpAll(updateProcessorFixture.setUp); diff --git a/packages/veilid_support/example/pubspec.lock b/packages/veilid_support/example/pubspec.lock index ade4030..3844db3 100644 --- a/packages/veilid_support/example/pubspec.lock +++ b/packages/veilid_support/example/pubspec.lock @@ -34,10 +34,10 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.12.0" async_tools: dependency: "direct dev" description: @@ -66,10 +66,10 @@ packages: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" change_case: dependency: transitive description: @@ -82,10 +82,10 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" charcode: dependency: transitive description: @@ -98,18 +98,18 @@ packages: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" convert: dependency: transitive description: @@ -154,10 +154,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" fast_immutable_collections: dependency: transitive description: @@ -178,10 +178,10 @@ packages: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" fixnum: dependency: transitive description: @@ -296,18 +296,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -352,10 +352,10 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -368,10 +368,10 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mime: dependency: transitive description: @@ -400,10 +400,10 @@ packages: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_provider: dependency: transitive description: @@ -456,10 +456,10 @@ packages: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -480,10 +480,10 @@ packages: dependency: transitive description: name: process - sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "5.0.3" protobuf: dependency: transitive description: @@ -557,34 +557,34 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" sync_http: dependency: transitive description: @@ -613,34 +613,34 @@ packages: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test: dependency: "direct dev" description: name: test - sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" + sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e" url: "https://pub.dev" source: hosted - version: "1.25.8" + version: "1.25.15" test_api: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.4" test_core: dependency: transitive description: name: test_core - sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" + sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.8" typed_data: dependency: transitive description: @@ -663,7 +663,7 @@ packages: path: "../../../../veilid/veilid-flutter" relative: true source: path - version: "0.4.1" + version: "0.4.3" veilid_support: dependency: "direct main" description: @@ -682,10 +682,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "14.3.1" watcher: dependency: transitive description: @@ -751,5 +751,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.7.0-0 <4.0.0" flutter: ">=3.24.0" diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.freezed.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.freezed.dart index e09fc0c..9e51ef8 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.freezed.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.freezed.dart @@ -29,8 +29,12 @@ mixin _$DHTRecordPoolAllocations { throw _privateConstructorUsedError; IMap get debugNames => throw _privateConstructorUsedError; + /// Serializes this DHTRecordPoolAllocations to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of DHTRecordPoolAllocations + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $DHTRecordPoolAllocationsCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -59,6 +63,8 @@ class _$DHTRecordPoolAllocationsCopyWithImpl<$Res, // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of DHTRecordPoolAllocations + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -114,6 +120,8 @@ class __$$DHTRecordPoolAllocationsImplCopyWithImpl<$Res> $Res Function(_$DHTRecordPoolAllocationsImpl) _then) : super(_value, _then); + /// Create a copy of DHTRecordPoolAllocations + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -188,12 +196,14 @@ class _$DHTRecordPoolAllocationsImpl implements _DHTRecordPoolAllocations { other.debugNames == debugNames)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, childrenByParent, parentByChild, const DeepCollectionEquality().hash(rootRecords), debugNames); - @JsonKey(ignore: true) + /// Create a copy of DHTRecordPoolAllocations + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$DHTRecordPoolAllocationsImplCopyWith<_$DHTRecordPoolAllocationsImpl> @@ -226,8 +236,11 @@ abstract class _DHTRecordPoolAllocations implements DHTRecordPoolAllocations { ISet> get rootRecords; @override IMap get debugNames; + + /// Create a copy of DHTRecordPoolAllocations + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$DHTRecordPoolAllocationsImplCopyWith<_$DHTRecordPoolAllocationsImpl> get copyWith => throw _privateConstructorUsedError; } @@ -243,8 +256,12 @@ mixin _$OwnedDHTRecordPointer { throw _privateConstructorUsedError; KeyPair get owner => throw _privateConstructorUsedError; + /// Serializes this OwnedDHTRecordPointer to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of OwnedDHTRecordPointer + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $OwnedDHTRecordPointerCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -269,6 +286,8 @@ class _$OwnedDHTRecordPointerCopyWithImpl<$Res, // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of OwnedDHTRecordPointer + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -309,6 +328,8 @@ class __$$OwnedDHTRecordPointerImplCopyWithImpl<$Res> $Res Function(_$OwnedDHTRecordPointerImpl) _then) : super(_value, _then); + /// Create a copy of OwnedDHTRecordPointer + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -357,11 +378,13 @@ class _$OwnedDHTRecordPointerImpl implements _OwnedDHTRecordPointer { (identical(other.owner, owner) || other.owner == owner)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, recordKey, owner); - @JsonKey(ignore: true) + /// Create a copy of OwnedDHTRecordPointer + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$OwnedDHTRecordPointerImplCopyWith<_$OwnedDHTRecordPointerImpl> @@ -388,8 +411,11 @@ abstract class _OwnedDHTRecordPointer implements OwnedDHTRecordPointer { Typed get recordKey; @override KeyPair get owner; + + /// Create a copy of OwnedDHTRecordPointer + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$OwnedDHTRecordPointerImplCopyWith<_$OwnedDHTRecordPointerImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool_private.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool_private.dart index b7cbba8..d1fb5d1 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool_private.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool_private.dart @@ -1,8 +1,5 @@ part of 'dht_record_pool.dart'; -const int _watchBackoffMultiplier = 2; -const int _watchBackoffMax = 30; - const int? _defaultWatchDurationSecs = null; // 600 const int _watchRenewalNumerator = 4; const int _watchRenewalDenominator = 5; diff --git a/packages/veilid_support/lib/identity_support/account_record_info.freezed.dart b/packages/veilid_support/lib/identity_support/account_record_info.freezed.dart index 0d5b327..a266230 100644 --- a/packages/veilid_support/lib/identity_support/account_record_info.freezed.dart +++ b/packages/veilid_support/lib/identity_support/account_record_info.freezed.dart @@ -23,8 +23,12 @@ mixin _$AccountRecordInfo { // Top level account keys and secrets OwnedDHTRecordPointer get accountRecord => throw _privateConstructorUsedError; + /// Serializes this AccountRecordInfo to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of AccountRecordInfo + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $AccountRecordInfoCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -50,6 +54,8 @@ class _$AccountRecordInfoCopyWithImpl<$Res, $Val extends AccountRecordInfo> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of AccountRecordInfo + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -63,6 +69,8 @@ class _$AccountRecordInfoCopyWithImpl<$Res, $Val extends AccountRecordInfo> ) as $Val); } + /// Create a copy of AccountRecordInfo + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $OwnedDHTRecordPointerCopyWith<$Res> get accountRecord { @@ -94,6 +102,8 @@ class __$$AccountRecordInfoImplCopyWithImpl<$Res> $Res Function(_$AccountRecordInfoImpl) _then) : super(_value, _then); + /// Create a copy of AccountRecordInfo + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -134,11 +144,13 @@ class _$AccountRecordInfoImpl implements _AccountRecordInfo { other.accountRecord == accountRecord)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, accountRecord); - @JsonKey(ignore: true) + /// Create a copy of AccountRecordInfo + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$AccountRecordInfoImplCopyWith<_$AccountRecordInfoImpl> get copyWith => @@ -161,10 +173,14 @@ abstract class _AccountRecordInfo implements AccountRecordInfo { factory _AccountRecordInfo.fromJson(Map json) = _$AccountRecordInfoImpl.fromJson; - @override // Top level account keys and secrets - OwnedDHTRecordPointer get accountRecord; +// Top level account keys and secrets @override - @JsonKey(ignore: true) + OwnedDHTRecordPointer get accountRecord; + + /// Create a copy of AccountRecordInfo + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) _$$AccountRecordInfoImplCopyWith<_$AccountRecordInfoImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/packages/veilid_support/lib/identity_support/identity.freezed.dart b/packages/veilid_support/lib/identity_support/identity.freezed.dart index 5977a26..3a276b0 100644 --- a/packages/veilid_support/lib/identity_support/identity.freezed.dart +++ b/packages/veilid_support/lib/identity_support/identity.freezed.dart @@ -24,8 +24,12 @@ mixin _$Identity { IMap> get accountRecords => throw _privateConstructorUsedError; + /// Serializes this Identity to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of Identity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $IdentityCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -48,6 +52,8 @@ class _$IdentityCopyWithImpl<$Res, $Val extends Identity> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of Identity + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -81,6 +87,8 @@ class __$$IdentityImplCopyWithImpl<$Res> _$IdentityImpl _value, $Res Function(_$IdentityImpl) _then) : super(_value, _then); + /// Create a copy of Identity + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -121,11 +129,13 @@ class _$IdentityImpl implements _Identity { other.accountRecords == accountRecords)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, accountRecords); - @JsonKey(ignore: true) + /// Create a copy of Identity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$IdentityImplCopyWith<_$IdentityImpl> get copyWith => @@ -147,10 +157,14 @@ abstract class _Identity implements Identity { factory _Identity.fromJson(Map json) = _$IdentityImpl.fromJson; - @override // Top level account keys and secrets - IMap> get accountRecords; +// Top level account keys and secrets @override - @JsonKey(ignore: true) + IMap> get accountRecords; + + /// Create a copy of Identity + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) _$$IdentityImplCopyWith<_$IdentityImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/packages/veilid_support/lib/identity_support/identity_instance.freezed.dart b/packages/veilid_support/lib/identity_support/identity_instance.freezed.dart index a7c3e78..28bbad4 100644 --- a/packages/veilid_support/lib/identity_support/identity_instance.freezed.dart +++ b/packages/veilid_support/lib/identity_support/identity_instance.freezed.dart @@ -38,8 +38,12 @@ mixin _$IdentityInstance { // by SuperIdentity publicKey FixedEncodedString86 get signature => throw _privateConstructorUsedError; + /// Serializes this IdentityInstance to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of IdentityInstance + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $IdentityInstanceCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -68,6 +72,8 @@ class _$IdentityInstanceCopyWithImpl<$Res, $Val extends IdentityInstance> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of IdentityInstance + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -126,6 +132,8 @@ class __$$IdentityInstanceImplCopyWithImpl<$Res> $Res Function(_$IdentityInstanceImpl) _then) : super(_value, _then); + /// Create a copy of IdentityInstance + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -219,7 +227,7 @@ class _$IdentityInstanceImpl extends _IdentityInstance { other.signature == signature)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, @@ -229,7 +237,9 @@ class _$IdentityInstanceImpl extends _IdentityInstance { superSignature, signature); - @JsonKey(ignore: true) + /// Create a copy of IdentityInstance + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$IdentityInstanceImplCopyWith<_$IdentityInstanceImpl> get copyWith => @@ -256,25 +266,31 @@ abstract class _IdentityInstance extends IdentityInstance { factory _IdentityInstance.fromJson(Map json) = _$IdentityInstanceImpl.fromJson; - @override // Private DHT record storing identity account mapping - Typed get recordKey; - @override // Public key of identity instance - FixedEncodedString43 get publicKey; - @override // Secret key of identity instance +// Private DHT record storing identity account mapping + @override + Typed get recordKey; // Public key of identity instance + @override + FixedEncodedString43 get publicKey; // Secret key of identity instance // Encrypted with appended salt, key is DeriveSharedSecret( // password = SuperIdentity.secret, // salt = publicKey) // Used to recover accounts without generating a new instance - @Uint8ListJsonConverter() - Uint8List get encryptedSecretKey; - @override // Signature of SuperInstance recordKey and SuperInstance publicKey -// by publicKey - FixedEncodedString86 get superSignature; - @override // Signature of recordKey, publicKey, encryptedSecretKey, and superSignature -// by SuperIdentity publicKey - FixedEncodedString86 get signature; @override - @JsonKey(ignore: true) + @Uint8ListJsonConverter() + Uint8List + get encryptedSecretKey; // Signature of SuperInstance recordKey and SuperInstance publicKey +// by publicKey + @override + FixedEncodedString86 + get superSignature; // Signature of recordKey, publicKey, encryptedSecretKey, and superSignature +// by SuperIdentity publicKey + @override + FixedEncodedString86 get signature; + + /// Create a copy of IdentityInstance + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) _$$IdentityInstanceImplCopyWith<_$IdentityInstanceImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/packages/veilid_support/lib/identity_support/super_identity.freezed.dart b/packages/veilid_support/lib/identity_support/super_identity.freezed.dart index dc1c69a..9c5c6a7 100644 --- a/packages/veilid_support/lib/identity_support/super_identity.freezed.dart +++ b/packages/veilid_support/lib/identity_support/super_identity.freezed.dart @@ -53,8 +53,12 @@ mixin _$SuperIdentity { /// by publicKey FixedEncodedString86 get signature => throw _privateConstructorUsedError; + /// Serializes this SuperIdentity to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of SuperIdentity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $SuperIdentityCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -86,6 +90,8 @@ class _$SuperIdentityCopyWithImpl<$Res, $Val extends SuperIdentity> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of SuperIdentity + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -124,6 +130,8 @@ class _$SuperIdentityCopyWithImpl<$Res, $Val extends SuperIdentity> ) as $Val); } + /// Create a copy of SuperIdentity + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $IdentityInstanceCopyWith<$Res> get currentInstance { @@ -161,6 +169,8 @@ class __$$SuperIdentityImplCopyWithImpl<$Res> _$SuperIdentityImpl _value, $Res Function(_$SuperIdentityImpl) _then) : super(_value, _then); + /// Create a copy of SuperIdentity + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -298,7 +308,7 @@ class _$SuperIdentityImpl extends _SuperIdentity { other.signature == signature)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, @@ -309,7 +319,9 @@ class _$SuperIdentityImpl extends _SuperIdentity { const DeepCollectionEquality().hash(_deprecatedSuperRecordKeys), signature); - @JsonKey(ignore: true) + /// Create a copy of SuperIdentity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$SuperIdentityImplCopyWith<_$SuperIdentityImpl> get copyWith => @@ -337,44 +349,46 @@ abstract class _SuperIdentity extends SuperIdentity { factory _SuperIdentity.fromJson(Map json) = _$SuperIdentityImpl.fromJson; - @override - /// Public DHT record storing this structure for account recovery /// changing this can migrate/forward the SuperIdentity to a new DHT record /// Instances should not hash this recordKey, rather the actual record /// key used to store the superIdentity, as this may change. - Typed get recordKey; @override + Typed get recordKey; /// Public key of the SuperIdentity used to sign identity keys for recovery /// This must match the owner of the superRecord DHT record and can not be /// changed without changing the record - FixedEncodedString43 get publicKey; @override + FixedEncodedString43 get publicKey; /// Current identity instance /// The most recently generated identity instance for this SuperIdentity - IdentityInstance get currentInstance; @override + IdentityInstance get currentInstance; /// Deprecated identity instances /// These may be compromised and should not be considered valid for /// new signatures, but may be used to validate old signatures - List get deprecatedInstances; @override + List get deprecatedInstances; /// Deprecated superRecords /// These may be compromised and should not be considered valid for /// new signatures, but may be used to validate old signatures - List> get deprecatedSuperRecordKeys; @override + List> get deprecatedSuperRecordKeys; /// Signature of recordKey, currentInstance signature, /// signatures of deprecatedInstances, and deprecatedSuperRecordKeys /// by publicKey - FixedEncodedString86 get signature; @override - @JsonKey(ignore: true) + FixedEncodedString86 get signature; + + /// Create a copy of SuperIdentity + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) _$$SuperIdentityImplCopyWith<_$SuperIdentityImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/packages/veilid_support/lib/proto/dht.pb.dart b/packages/veilid_support/lib/proto/dht.pb.dart index 7a9ac9a..b1c0b47 100644 --- a/packages/veilid_support/lib/proto/dht.pb.dart +++ b/packages/veilid_support/lib/proto/dht.pb.dart @@ -4,7 +4,7 @@ // // @dart = 2.12 -// ignore_for_file: annotate_overrides, camel_case_types +// ignore_for_file: annotate_overrides, camel_case_types, comment_references // ignore_for_file: constant_identifier_names, library_prefixes // ignore_for_file: non_constant_identifier_names, prefer_final_fields // ignore_for_file: unnecessary_import, unnecessary_this, unused_import @@ -16,7 +16,27 @@ import 'package:protobuf/protobuf.dart' as $pb; import 'veilid.pb.dart' as $0; class DHTData extends $pb.GeneratedMessage { - factory DHTData() => create(); + factory DHTData({ + $core.Iterable<$0.TypedKey>? keys, + $0.TypedKey? hash, + $core.int? chunk, + $core.int? size, + }) { + final $result = create(); + if (keys != null) { + $result.keys.addAll(keys); + } + if (hash != null) { + $result.hash = hash; + } + if (chunk != null) { + $result.chunk = chunk; + } + if (size != null) { + $result.size = size; + } + return $result; + } DHTData._() : super(); factory DHTData.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory DHTData.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -50,9 +70,12 @@ class DHTData extends $pb.GeneratedMessage { static DHTData getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static DHTData? _defaultInstance; + /// Other keys to concatenate + /// Uses the same writer as this DHTList with SMPL schema @$pb.TagNumber(1) $core.List<$0.TypedKey> get keys => $_getList(0); + /// Hash of reassembled data to verify contents @$pb.TagNumber(2) $0.TypedKey get hash => $_getN(1); @$pb.TagNumber(2) @@ -64,6 +87,7 @@ class DHTData extends $pb.GeneratedMessage { @$pb.TagNumber(2) $0.TypedKey ensureHash() => $_ensure(1); + /// Chunk size per subkey @$pb.TagNumber(3) $core.int get chunk => $_getIZ(2); @$pb.TagNumber(3) @@ -73,6 +97,7 @@ class DHTData extends $pb.GeneratedMessage { @$pb.TagNumber(3) void clearChunk() => clearField(3); + /// Total data size @$pb.TagNumber(4) $core.int get size => $_getIZ(3); @$pb.TagNumber(4) @@ -83,8 +108,26 @@ class DHTData extends $pb.GeneratedMessage { void clearSize() => clearField(4); } +/// DHTLog - represents a ring buffer of many elements with append/truncate semantics +/// Header in subkey 0 of first key follows this structure class DHTLog extends $pb.GeneratedMessage { - factory DHTLog() => create(); + factory DHTLog({ + $core.int? head, + $core.int? tail, + $core.int? stride, + }) { + final $result = create(); + if (head != null) { + $result.head = head; + } + if (tail != null) { + $result.tail = tail; + } + if (stride != null) { + $result.stride = stride; + } + return $result; + } DHTLog._() : super(); factory DHTLog.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory DHTLog.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -117,6 +160,7 @@ class DHTLog extends $pb.GeneratedMessage { static DHTLog getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static DHTLog? _defaultInstance; + /// Position of the start of the log (oldest items) @$pb.TagNumber(1) $core.int get head => $_getIZ(0); @$pb.TagNumber(1) @@ -126,6 +170,7 @@ class DHTLog extends $pb.GeneratedMessage { @$pb.TagNumber(1) void clearHead() => clearField(1); + /// Position of the end of the log (newest items) @$pb.TagNumber(2) $core.int get tail => $_getIZ(1); @$pb.TagNumber(2) @@ -135,6 +180,7 @@ class DHTLog extends $pb.GeneratedMessage { @$pb.TagNumber(2) void clearTail() => clearField(2); + /// Stride of each segment of the dhtlog @$pb.TagNumber(3) $core.int get stride => $_getIZ(2); @$pb.TagNumber(3) @@ -145,8 +191,32 @@ class DHTLog extends $pb.GeneratedMessage { void clearStride() => clearField(3); } +/// DHTShortArray - represents a re-orderable collection of up to 256 individual elements +/// Header in subkey 0 of first key follows this structure +/// +/// stride = descriptor subkey count on first key - 1 +/// Subkeys 1..=stride on the first key are individual elements +/// Subkeys 0..stride on the 'keys' keys are also individual elements +/// +/// Keys must use writable schema in order to make this list mutable class DHTShortArray extends $pb.GeneratedMessage { - factory DHTShortArray() => create(); + factory DHTShortArray({ + $core.Iterable<$0.TypedKey>? keys, + $core.List<$core.int>? index, + $core.Iterable<$core.int>? seqs, + }) { + final $result = create(); + if (keys != null) { + $result.keys.addAll(keys); + } + if (index != null) { + $result.index = index; + } + if (seqs != null) { + $result.seqs.addAll(seqs); + } + return $result; + } DHTShortArray._() : super(); factory DHTShortArray.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory DHTShortArray.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -179,9 +249,16 @@ class DHTShortArray extends $pb.GeneratedMessage { static DHTShortArray getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static DHTShortArray? _defaultInstance; + /// Other keys to concatenate + /// Uses the same writer as this DHTList with SMPL schema @$pb.TagNumber(1) $core.List<$0.TypedKey> get keys => $_getList(0); + /// Item position index (uint8[256./]) + /// Actual item location is: + /// idx = index[n] + 1 (offset for header at idx 0) + /// key = idx / stride + /// subkey = idx % stride @$pb.TagNumber(2) $core.List<$core.int> get index => $_getN(1); @$pb.TagNumber(2) @@ -191,12 +268,26 @@ class DHTShortArray extends $pb.GeneratedMessage { @$pb.TagNumber(2) void clearIndex() => clearField(2); + /// Most recent sequence numbers for elements @$pb.TagNumber(3) $core.List<$core.int> get seqs => $_getList(2); } +/// A pointer to an child DHT record class OwnedDHTRecordPointer extends $pb.GeneratedMessage { - factory OwnedDHTRecordPointer() => create(); + factory OwnedDHTRecordPointer({ + $0.TypedKey? recordKey, + $0.KeyPair? owner, + }) { + final $result = create(); + if (recordKey != null) { + $result.recordKey = recordKey; + } + if (owner != null) { + $result.owner = owner; + } + return $result; + } OwnedDHTRecordPointer._() : super(); factory OwnedDHTRecordPointer.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory OwnedDHTRecordPointer.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -228,6 +319,7 @@ class OwnedDHTRecordPointer extends $pb.GeneratedMessage { static OwnedDHTRecordPointer getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static OwnedDHTRecordPointer? _defaultInstance; + /// DHT Record key @$pb.TagNumber(1) $0.TypedKey get recordKey => $_getN(0); @$pb.TagNumber(1) @@ -239,6 +331,7 @@ class OwnedDHTRecordPointer extends $pb.GeneratedMessage { @$pb.TagNumber(1) $0.TypedKey ensureRecordKey() => $_ensure(0); + /// DHT record owner key @$pb.TagNumber(2) $0.KeyPair get owner => $_getN(1); @$pb.TagNumber(2) diff --git a/packages/veilid_support/lib/proto/dht.pbenum.dart b/packages/veilid_support/lib/proto/dht.pbenum.dart index f76992d..7059e85 100644 --- a/packages/veilid_support/lib/proto/dht.pbenum.dart +++ b/packages/veilid_support/lib/proto/dht.pbenum.dart @@ -4,7 +4,7 @@ // // @dart = 2.12 -// ignore_for_file: annotate_overrides, camel_case_types +// ignore_for_file: annotate_overrides, camel_case_types, comment_references // ignore_for_file: constant_identifier_names, library_prefixes // ignore_for_file: non_constant_identifier_names, prefer_final_fields // ignore_for_file: unnecessary_import, unnecessary_this, unused_import diff --git a/packages/veilid_support/lib/proto/dht.pbjson.dart b/packages/veilid_support/lib/proto/dht.pbjson.dart index 9d505f0..dd14566 100644 --- a/packages/veilid_support/lib/proto/dht.pbjson.dart +++ b/packages/veilid_support/lib/proto/dht.pbjson.dart @@ -4,7 +4,7 @@ // // @dart = 2.12 -// ignore_for_file: annotate_overrides, camel_case_types +// ignore_for_file: annotate_overrides, camel_case_types, comment_references // ignore_for_file: constant_identifier_names, library_prefixes // ignore_for_file: non_constant_identifier_names, prefer_final_fields // ignore_for_file: unnecessary_import, unnecessary_this, unused_import diff --git a/packages/veilid_support/lib/proto/dht.pbserver.dart b/packages/veilid_support/lib/proto/dht.pbserver.dart index ffbf990..02e8c03 100644 --- a/packages/veilid_support/lib/proto/dht.pbserver.dart +++ b/packages/veilid_support/lib/proto/dht.pbserver.dart @@ -4,7 +4,7 @@ // // @dart = 2.12 -// ignore_for_file: annotate_overrides, camel_case_types +// ignore_for_file: annotate_overrides, camel_case_types, comment_references // ignore_for_file: constant_identifier_names // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes // ignore_for_file: non_constant_identifier_names, prefer_final_fields diff --git a/packages/veilid_support/lib/proto/veilid.pb.dart b/packages/veilid_support/lib/proto/veilid.pb.dart index a53133a..5431b80 100644 --- a/packages/veilid_support/lib/proto/veilid.pb.dart +++ b/packages/veilid_support/lib/proto/veilid.pb.dart @@ -4,7 +4,7 @@ // // @dart = 2.12 -// ignore_for_file: annotate_overrides, camel_case_types +// ignore_for_file: annotate_overrides, camel_case_types, comment_references // ignore_for_file: constant_identifier_names, library_prefixes // ignore_for_file: non_constant_identifier_names, prefer_final_fields // ignore_for_file: unnecessary_import, unnecessary_this, unused_import @@ -13,8 +13,45 @@ import 'dart:core' as $core; import 'package:protobuf/protobuf.dart' as $pb; +/// 32-byte value in bigendian format class CryptoKey extends $pb.GeneratedMessage { - factory CryptoKey() => create(); + factory CryptoKey({ + $core.int? u0, + $core.int? u1, + $core.int? u2, + $core.int? u3, + $core.int? u4, + $core.int? u5, + $core.int? u6, + $core.int? u7, + }) { + final $result = create(); + if (u0 != null) { + $result.u0 = u0; + } + if (u1 != null) { + $result.u1 = u1; + } + if (u2 != null) { + $result.u2 = u2; + } + if (u3 != null) { + $result.u3 = u3; + } + if (u4 != null) { + $result.u4 = u4; + } + if (u5 != null) { + $result.u5 = u5; + } + if (u6 != null) { + $result.u6 = u6; + } + if (u7 != null) { + $result.u7 = u7; + } + return $result; + } CryptoKey._() : super(); factory CryptoKey.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory CryptoKey.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -125,8 +162,77 @@ class CryptoKey extends $pb.GeneratedMessage { void clearU7() => clearField(8); } +/// 64-byte value in bigendian format class Signature extends $pb.GeneratedMessage { - factory Signature() => create(); + factory Signature({ + $core.int? u0, + $core.int? u1, + $core.int? u2, + $core.int? u3, + $core.int? u4, + $core.int? u5, + $core.int? u6, + $core.int? u7, + $core.int? u8, + $core.int? u9, + $core.int? u10, + $core.int? u11, + $core.int? u12, + $core.int? u13, + $core.int? u14, + $core.int? u15, + }) { + final $result = create(); + if (u0 != null) { + $result.u0 = u0; + } + if (u1 != null) { + $result.u1 = u1; + } + if (u2 != null) { + $result.u2 = u2; + } + if (u3 != null) { + $result.u3 = u3; + } + if (u4 != null) { + $result.u4 = u4; + } + if (u5 != null) { + $result.u5 = u5; + } + if (u6 != null) { + $result.u6 = u6; + } + if (u7 != null) { + $result.u7 = u7; + } + if (u8 != null) { + $result.u8 = u8; + } + if (u9 != null) { + $result.u9 = u9; + } + if (u10 != null) { + $result.u10 = u10; + } + if (u11 != null) { + $result.u11 = u11; + } + if (u12 != null) { + $result.u12 = u12; + } + if (u13 != null) { + $result.u13 = u13; + } + if (u14 != null) { + $result.u14 = u14; + } + if (u15 != null) { + $result.u15 = u15; + } + return $result; + } Signature._() : super(); factory Signature.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory Signature.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -317,8 +423,37 @@ class Signature extends $pb.GeneratedMessage { void clearU15() => clearField(16); } +/// 24-byte value in bigendian format class Nonce extends $pb.GeneratedMessage { - factory Nonce() => create(); + factory Nonce({ + $core.int? u0, + $core.int? u1, + $core.int? u2, + $core.int? u3, + $core.int? u4, + $core.int? u5, + }) { + final $result = create(); + if (u0 != null) { + $result.u0 = u0; + } + if (u1 != null) { + $result.u1 = u1; + } + if (u2 != null) { + $result.u2 = u2; + } + if (u3 != null) { + $result.u3 = u3; + } + if (u4 != null) { + $result.u4 = u4; + } + if (u5 != null) { + $result.u5 = u5; + } + return $result; + } Nonce._() : super(); factory Nonce.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory Nonce.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -409,8 +544,21 @@ class Nonce extends $pb.GeneratedMessage { void clearU5() => clearField(6); } +/// 36-byte typed crypto key class TypedKey extends $pb.GeneratedMessage { - factory TypedKey() => create(); + factory TypedKey({ + $core.int? kind, + CryptoKey? value, + }) { + final $result = create(); + if (kind != null) { + $result.kind = kind; + } + if (value != null) { + $result.value = value; + } + return $result; + } TypedKey._() : super(); factory TypedKey.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory TypedKey.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -442,6 +590,7 @@ class TypedKey extends $pb.GeneratedMessage { static TypedKey getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static TypedKey? _defaultInstance; + /// CryptoKind FourCC in bigendian format @$pb.TagNumber(1) $core.int get kind => $_getIZ(0); @$pb.TagNumber(1) @@ -451,6 +600,7 @@ class TypedKey extends $pb.GeneratedMessage { @$pb.TagNumber(1) void clearKind() => clearField(1); + /// Key value @$pb.TagNumber(2) CryptoKey get value => $_getN(1); @$pb.TagNumber(2) @@ -463,8 +613,21 @@ class TypedKey extends $pb.GeneratedMessage { CryptoKey ensureValue() => $_ensure(1); } +/// Key pair class KeyPair extends $pb.GeneratedMessage { - factory KeyPair() => create(); + factory KeyPair({ + CryptoKey? key, + CryptoKey? secret, + }) { + final $result = create(); + if (key != null) { + $result.key = key; + } + if (secret != null) { + $result.secret = secret; + } + return $result; + } KeyPair._() : super(); factory KeyPair.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); factory KeyPair.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @@ -496,6 +659,7 @@ class KeyPair extends $pb.GeneratedMessage { static KeyPair getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static KeyPair? _defaultInstance; + /// Public key @$pb.TagNumber(1) CryptoKey get key => $_getN(0); @$pb.TagNumber(1) @@ -507,6 +671,7 @@ class KeyPair extends $pb.GeneratedMessage { @$pb.TagNumber(1) CryptoKey ensureKey() => $_ensure(0); + /// Private key @$pb.TagNumber(2) CryptoKey get secret => $_getN(1); @$pb.TagNumber(2) diff --git a/packages/veilid_support/lib/proto/veilid.pbenum.dart b/packages/veilid_support/lib/proto/veilid.pbenum.dart index 1ade7e9..89c0019 100644 --- a/packages/veilid_support/lib/proto/veilid.pbenum.dart +++ b/packages/veilid_support/lib/proto/veilid.pbenum.dart @@ -4,7 +4,7 @@ // // @dart = 2.12 -// ignore_for_file: annotate_overrides, camel_case_types +// ignore_for_file: annotate_overrides, camel_case_types, comment_references // ignore_for_file: constant_identifier_names, library_prefixes // ignore_for_file: non_constant_identifier_names, prefer_final_fields // ignore_for_file: unnecessary_import, unnecessary_this, unused_import diff --git a/packages/veilid_support/lib/proto/veilid.pbjson.dart b/packages/veilid_support/lib/proto/veilid.pbjson.dart index b92b4e5..db8318e 100644 --- a/packages/veilid_support/lib/proto/veilid.pbjson.dart +++ b/packages/veilid_support/lib/proto/veilid.pbjson.dart @@ -4,7 +4,7 @@ // // @dart = 2.12 -// ignore_for_file: annotate_overrides, camel_case_types +// ignore_for_file: annotate_overrides, camel_case_types, comment_references // ignore_for_file: constant_identifier_names, library_prefixes // ignore_for_file: non_constant_identifier_names, prefer_final_fields // ignore_for_file: unnecessary_import, unnecessary_this, unused_import diff --git a/packages/veilid_support/lib/proto/veilid.pbserver.dart b/packages/veilid_support/lib/proto/veilid.pbserver.dart index 2de2834..f799a3f 100644 --- a/packages/veilid_support/lib/proto/veilid.pbserver.dart +++ b/packages/veilid_support/lib/proto/veilid.pbserver.dart @@ -4,7 +4,7 @@ // // @dart = 2.12 -// ignore_for_file: annotate_overrides, camel_case_types +// ignore_for_file: annotate_overrides, camel_case_types, comment_references // ignore_for_file: constant_identifier_names // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes // ignore_for_file: non_constant_identifier_names, prefer_final_fields diff --git a/packages/veilid_support/lib/src/config.dart b/packages/veilid_support/lib/src/config.dart index bedc743..47bd84e 100644 --- a/packages/veilid_support/lib/src/config.dart +++ b/packages/veilid_support/lib/src/config.dart @@ -115,6 +115,7 @@ Future getVeilidConfig(bool isWeb, String programName) async { const VeilidConfigCapabilities(disable: ['DHTV', 'DHTW', 'TUNL']), protectedStore: // XXX: Linux often does not have a secret storage mechanism installed - config.protectedStore.copyWith(allowInsecureFallback: Platform.isLinux), + config.protectedStore + .copyWith(allowInsecureFallback: !isWeb && Platform.isLinux), ); } diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index 260c991..447a27c 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -36,10 +36,9 @@ packages: async_tools: dependency: "direct main" description: - name: async_tools - sha256: bbded696bfcb1437d0ca510ac047f261f9c7494fea2c488dd32ba2800e7f49e8 - url: "https://pub.dev" - source: hosted + path: "../../../dart_async_tools" + relative: true + source: path version: "0.1.7" bloc: dependency: "direct main" @@ -141,10 +140,10 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" charcode: dependency: "direct main" description: @@ -173,10 +172,10 @@ packages: dependency: "direct main" description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" convert: dependency: transitive description: @@ -407,10 +406,10 @@ packages: dependency: "direct main" description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mime: dependency: transitive description: @@ -726,7 +725,7 @@ packages: path: "../../../veilid/veilid-flutter" relative: true source: path - version: "0.4.1" + version: "0.4.3" vm_service: dependency: transitive description: @@ -792,5 +791,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.6.0 <4.0.0" + dart: ">=3.7.0-0 <4.0.0" flutter: ">=3.24.0" diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index 8ed1f58..bcd965d 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -26,9 +26,9 @@ dependencies: # veilid: ^0.0.1 path: ../../../veilid/veilid-flutter -# dependency_overrides: -# async_tools: -# path: ../../../dart_async_tools +dependency_overrides: + async_tools: + path: ../../../dart_async_tools # bloc_advanced_tools: # path: ../../../bloc_advanced_tools diff --git a/pubspec.lock b/pubspec.lock index 9555644..b98424d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + animated_custom_dropdown: + dependency: "direct main" + description: + name: animated_custom_dropdown + sha256: "5a72dc209041bb53f6c7164bc2e366552d5197cdb032b1c9b2c36e3013024486" + url: "https://pub.dev" + source: hosted + version: "3.1.1" animated_switcher_transitions: dependency: "direct main" description: @@ -61,18 +69,18 @@ packages: dependency: "direct main" description: name: archive - sha256: "6199c74e3db4fbfbd04f66d739e72fe11c8a8957d5f219f1f4482dbde6420b5a" + sha256: "0c64e928dcbefddecd234205422bcfc2b5e6d31be0b86fef0d0dd48d7b4c9742" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.4" args: dependency: transitive description: name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" async: dependency: transitive description: @@ -84,19 +92,18 @@ packages: async_tools: dependency: "direct main" description: - name: async_tools - sha256: bbded696bfcb1437d0ca510ac047f261f9c7494fea2c488dd32ba2800e7f49e8 - url: "https://pub.dev" - source: hosted + path: "../dart_async_tools" + relative: true + source: path version: "0.1.7" awesome_extensions: dependency: "direct main" description: name: awesome_extensions - sha256: "91dc128e8cf01fbd3d3567b8f1dd1e3183cbf9fd6b1850e8b0fafce9a7eee0da" + sha256: "9b1693e986e4045141add298fa2d7f9aa6cdd3c125b951e2cde739a5058ed879" url: "https://pub.dev" source: hosted - version: "2.0.20" + version: "2.0.21" badges: dependency: "direct main" description: @@ -117,10 +124,10 @@ packages: dependency: "direct main" description: name: basic_utils - sha256: "2064b21d3c41ed7654bc82cc476fd65542e04d60059b74d5eed490a4da08fc6c" + sha256: "548047bef0b3b697be19fa62f46de54d99c9019a69fb7db92c69e19d87f633c7" url: "https://pub.dev" source: hosted - version: "5.7.0" + version: "5.8.2" bidi: dependency: transitive description: @@ -221,10 +228,10 @@ packages: dependency: transitive description: name: built_value - sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2" + sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 url: "https://pub.dev" source: hosted - version: "8.9.3" + version: "8.9.5" cached_network_image: dependency: transitive description: @@ -261,18 +268,18 @@ packages: dependency: transitive description: name: camera_android_camerax - sha256: "7cc6adf1868bdcf4e63a56b24b41692dfbad2bec1cdceea451c77798f6a605c3" + sha256: "13784f539c7f104766bff84e4479a70f03b29d78b208278be45c939250d9d7f5" url: "https://pub.dev" source: hosted - version: "0.6.13" + version: "0.6.14+1" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: "1eeb9ce7c9a397e312343fd7db337d95f35c3e65ad5a62ff637c8abce5102b98" + sha256: "3057ada0b30402e3a9b6dffec365c9736a36edbf04abaecc67c4309eadc86b49" url: "https://pub.dev" source: hosted - version: "0.9.18+8" + version: "0.9.18+9" camera_platform_interface: dependency: transitive description: @@ -301,10 +308,10 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" charcode: dependency: "direct main" description: @@ -349,10 +356,10 @@ packages: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" code_builder: dependency: transitive description: @@ -365,10 +372,10 @@ packages: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" convert: dependency: transitive description: @@ -377,14 +384,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" - cool_dropdown: - dependency: "direct main" - description: - name: cool_dropdown - sha256: "23926fd242b625bcb7ab30c1336498d60f78267518db439141ca19de403ab030" - url: "https://pub.dev" - source: hosted - version: "2.1.1" cross_file: dependency: transitive description: @@ -445,10 +444,10 @@ packages: dependency: transitive description: name: dio_web_adapter - sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" equatable: dependency: "direct main" description: @@ -477,10 +476,10 @@ packages: dependency: transitive description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" file: dependency: transitive description: @@ -592,10 +591,10 @@ packages: dependency: "direct main" description: name: flutter_native_splash - sha256: "7062602e0dbd29141fb8eb19220b5871ca650be5197ab9c1f193a28b17537bc7" + sha256: edb09c35ee9230c4b03f13dd45bb3a276d0801865f0a4650b7e2a3bba61a803a url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.4.5" flutter_parsed_text: dependency: transitive description: @@ -608,10 +607,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e" + sha256: "5a1e6fb2c0561958d7e4c33574674bda7b77caaca7a33b758876956f2902eea3" url: "https://pub.dev" source: hosted - version: "2.0.24" + version: "2.0.27" flutter_shaders: dependency: transitive description: @@ -677,10 +676,10 @@ packages: dependency: "direct main" description: name: form_builder_validators - sha256: "517fb884183fff7a0ef3db7d375981011da26ee452f20fb3d2e788ad527ad01d" + sha256: cd617fa346250293ff3e2709961d0faf7b80e6e4f0ff7b500126b28d7422dd67 url: "https://pub.dev" source: hosted - version: "11.1.1" + version: "11.1.2" freezed: dependency: "direct dev" description: @@ -733,10 +732,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "04539267a740931c6d4479a10d466717ca5901c6fdfd3fcda09391bbb8ebd651" + sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3 url: "https://pub.dev" source: hosted - version: "14.8.0" + version: "14.8.1" graphs: dependency: transitive description: @@ -797,18 +796,26 @@ packages: dependency: "direct dev" description: name: icons_launcher - sha256: a7c83fbc837dc6f81944ef35c3756f533bb2aba32fcca5cbcdb2dbcd877d5ae9 + sha256: "2949eef3d336028d89133f69ef221d877e09deed04ebd8e738ab4a427850a7a2" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.1" + iconsax_flutter: + dependency: transitive + description: + name: iconsax_flutter + sha256: "95b65699da8ea98f87c5d232f06b0debaaf1ec1332b697e4d90969ec9a93037d" + url: "https://pub.dev" + source: hosted + version: "1.0.0" image: dependency: "direct main" description: name: image - sha256: "8346ad4b5173924b5ddddab782fc7d8a6300178c8b1dc427775405a01701c4a6" + sha256: "13d3349ace88f12f4a0d175eb5c12dcdd39d35c4c109a8a13dfeb6d0bd9e31c3" url: "https://pub.dev" source: hosted - version: "4.5.2" + version: "4.5.3" intl: dependency: "direct main" description: @@ -829,10 +836,10 @@ packages: dependency: transitive description: name: js - sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "0.7.2" json_annotation: dependency: "direct main" description: @@ -901,10 +908,10 @@ packages: dependency: "direct main" description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mime: dependency: transitive description: @@ -917,18 +924,10 @@ packages: dependency: "direct main" description: name: mobile_scanner - sha256: "91d28b825784e15572fdc39165c5733099ce0e69c6f6f0964ebdbf98a62130fd" + sha256: "9cb9e371ee9b5b548714f9ab5fd33b530d799745c83d5729ecd1e8ab2935dbd1" url: "https://pub.dev" source: hosted - version: "6.0.6" - motion_toast: - dependency: "direct main" - description: - name: motion_toast - sha256: "5a4775bf5a89a2402456047f194df5a5d6ac717af0d7694c8b9e37f324d1efd7" - url: "https://pub.dev" - source: hosted - version: "2.11.0" + version: "6.0.7" native_device_orientation: dependency: "direct main" description: @@ -957,26 +956,26 @@ packages: dependency: transitive description: name: package_config - sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - sha256: "67eae327b1b0faf761964a1d2e5d323c797f3799db0e85aa232db8d9e922bc35" + sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191" url: "https://pub.dev" source: hosted - version: "8.2.1" + version: "8.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: "205ec83335c2ab9107bbba3f8997f9356d72ca3c715d2f038fc773d0366b4c76" + sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.0" pasteboard: dependency: "direct main" description: @@ -989,10 +988,10 @@ packages: dependency: "direct main" description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_parsing: dependency: transitive description: @@ -1013,10 +1012,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12" url: "https://pub.dev" source: hosted - version: "2.2.15" + version: "2.2.16" path_provider_foundation: dependency: transitive description: @@ -1049,6 +1048,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + pausable_timer: + dependency: transitive + description: + name: pausable_timer + sha256: "6ef1a95441ec3439de6fb63f39a011b67e693198e7dae14e20675c3c00e86074" + url: "https://pub.dev" + source: hosted + version: "3.1.0+3" pdf: dependency: "direct main" description: @@ -1069,10 +1076,10 @@ packages: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "6.1.0" photo_view: dependency: transitive description: @@ -1109,10 +1116,10 @@ packages: dependency: transitive description: name: pointycastle - sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" url: "https://pub.dev" source: hosted - version: "3.9.1" + version: "4.0.0" pool: dependency: transitive description: @@ -1165,10 +1172,10 @@ packages: dependency: transitive description: name: pub_semver - sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" pubspec_parse: dependency: transitive description: @@ -1334,10 +1341,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: a768fc8ede5f0c8e6150476e14f38e2417c0864ca36bb4582be8e21925a03c22 + sha256: "3ec7210872c4ba945e3244982918e502fa2bfb5230dff6832459ca0e1879b7ad" url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.4.8" shared_preferences_foundation: dependency: transitive description: @@ -1484,34 +1491,34 @@ packages: dependency: transitive description: name: sqflite - sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" sqflite_android: dependency: transitive description: name: sqflite_android - sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3" + sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" + sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b" url: "https://pub.dev" source: hosted - version: "2.5.4+6" + version: "2.5.5" sqflite_darwin: dependency: transitive description: name: sqflite_darwin - sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c" + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" url: "https://pub.dev" source: hosted - version: "2.4.1+1" + version: "2.4.2" sqflite_platform_interface: dependency: transitive description: @@ -1564,10 +1571,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" + sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" url: "https://pub.dev" source: hosted - version: "3.3.0+3" + version: "3.3.1" system_info2: dependency: transitive description: @@ -1608,6 +1615,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + toastification: + dependency: "direct main" + description: + name: toastification + sha256: "9713989549d60754fd0522425d1251501919cfb7bab4ffbbb36ef40de5ea72b9" + url: "https://pub.dev" + source: hosted + version: "3.0.2" transitioned_indexed_stack: dependency: "direct main" description: @@ -1652,10 +1667,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" + sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4" url: "https://pub.dev" source: hosted - version: "6.3.14" + version: "6.3.15" url_launcher_ios: dependency: transitive description: @@ -1758,7 +1773,7 @@ packages: path: "../veilid/veilid-flutter" relative: true source: path - version: "0.4.1" + version: "0.4.3" veilid_support: dependency: "direct main" description: @@ -1786,10 +1801,10 @@ packages: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web_socket: dependency: transitive description: @@ -1810,10 +1825,10 @@ packages: dependency: transitive description: name: win32 - sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e + sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f url: "https://pub.dev" source: hosted - version: "5.10.1" + version: "5.12.0" window_manager: dependency: "direct main" description: @@ -1879,5 +1894,5 @@ packages: source: hosted version: "1.1.2" sdks: - dart: ">=3.6.0 <4.0.0" + dart: ">=3.7.0 <4.0.0" flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index 5bc5e26..1e0a526 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,69 +9,68 @@ environment: dependencies: accordion: ^2.6.0 - animated_bottom_navigation_bar: ^1.3.3 + animated_bottom_navigation_bar: ^1.4.0 + animated_custom_dropdown: ^3.1.1 animated_switcher_transitions: ^1.0.0 animated_theme_switcher: ^2.0.10 - ansicolor: ^2.0.2 - archive: ^4.0.2 + ansicolor: ^2.0.3 + archive: ^4.0.4 async_tools: ^0.1.7 - awesome_extensions: ^2.0.16 + awesome_extensions: ^2.0.21 badges: ^3.1.2 - basic_utils: ^5.7.0 + basic_utils: ^5.8.2 bloc: ^8.1.4 bloc_advanced_tools: ^0.1.8 blurry_modal_progress_hud: ^1.1.1 - change_case: ^2.1.0 - charcode: ^1.3.1 + change_case: ^2.2.0 + charcode: ^1.4.0 circular_profile_avatar: ^2.0.5 circular_reveal_animation: ^2.0.1 - cool_dropdown: ^2.1.0 cupertino_icons: ^1.0.8 - equatable: ^2.0.5 + equatable: ^2.0.7 expansion_tile_group: ^2.2.0 fast_immutable_collections: ^10.2.4 - file_saver: ^0.2.13 - fixnum: ^1.1.0 + file_saver: ^0.2.14 + fixnum: ^1.1.1 flutter: sdk: flutter - flutter_animate: ^4.5.0 - flutter_bloc: ^8.1.5 + flutter_animate: ^4.5.2 + flutter_bloc: ^8.1.6 flutter_chat_types: ^3.6.2 flutter_chat_ui: git: url: https://gitlab.com/veilid/flutter-chat-ui.git ref: main - flutter_form_builder: ^9.3.0 + flutter_form_builder: ^9.7.0 flutter_hooks: ^0.20.5 flutter_localizations: sdk: flutter - flutter_native_splash: ^2.4.0 + flutter_native_splash: ^2.4.5 flutter_slidable: ^4.0.0 flutter_spinkit: ^5.2.1 flutter_sticky_header: ^0.7.0 - flutter_svg: ^2.0.10+1 + flutter_svg: ^2.0.17 flutter_translate: ^4.1.0 flutter_zoom_drawer: ^3.2.0 - form_builder_validators: ^11.0.0 - freezed_annotation: ^2.4.1 - go_router: ^14.1.4 + form_builder_validators: ^11.1.2 + freezed_annotation: ^2.4.4 + go_router: ^14.8.1 hydrated_bloc: ^9.1.5 - image: ^4.2.0 + image: ^4.5.3 intl: ^0.19.0 json_annotation: ^4.9.0 loggy: ^2.0.3 - meta: ^1.12.0 - mobile_scanner: ^6.0.6 - motion_toast: ^2.10.0 + meta: ^1.16.0 + mobile_scanner: ^6.0.7 native_device_orientation: ^2.0.3 - package_info_plus: ^8.0.0 + package_info_plus: ^8.3.0 pasteboard: ^0.3.0 - path: ^1.9.0 - path_provider: ^2.1.3 - pdf: ^3.11.0 + path: ^1.9.1 + path_provider: ^2.1.5 + pdf: ^3.11.3 pinput: ^5.0.1 preload_page_view: ^0.2.0 - printing: ^5.13.1 + printing: ^5.14.2 protobuf: ^3.1.0 provider: ^6.1.2 qr_code_dart_scan: ^0.9.11 @@ -86,7 +85,7 @@ dependencies: url: https://gitlab.com/veilid/Searchable-Listview.git ref: main share_plus: ^10.1.4 - shared_preferences: ^2.2.3 + shared_preferences: ^2.5.2 signal_strength_indicator: ^0.4.1 sliver_expandable: ^1.1.1 sliver_fill_remaining_box_adapter: ^1.0.0 @@ -96,12 +95,13 @@ dependencies: url: https://gitlab.com/veilid/dart-sorted-list-improved.git ref: main split_view: ^3.2.1 - stack_trace: ^1.11.1 + stack_trace: ^1.12.1 star_menu: ^4.0.1 - stream_transform: ^2.1.0 + stream_transform: ^2.1.1 + toastification: ^3.0.2 transitioned_indexed_stack: ^1.0.2 - url_launcher: ^6.3.0 - uuid: ^4.4.0 + url_launcher: ^6.3.1 + uuid: ^4.5.1 veilid: # veilid: ^0.0.1 path: ../veilid/veilid-flutter @@ -111,9 +111,9 @@ dependencies: xterm: ^4.0.0 zxing2: ^0.2.3 -# dependency_overrides: -# async_tools: -# path: ../dart_async_tools +dependency_overrides: + async_tools: + path: ../dart_async_tools # bloc_advanced_tools: # path: ../bloc_advanced_tools # searchable_listview: @@ -122,10 +122,10 @@ dependencies: # path: ../flutter_chat_ui dev_dependencies: - build_runner: ^2.4.11 - freezed: ^2.5.2 - icons_launcher: ^3.0.0 - json_serializable: ^6.8.0 + build_runner: ^2.4.15 + freezed: ^2.5.8 + icons_launcher: ^3.0.1 + json_serializable: ^6.9.4 lint_hard: ^5.0.0 flutter_native_splash: From 77c68aa45f4c94ce10383b2fdd34a3e81c6f35ac Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 17 Mar 2025 00:51:16 -0400 Subject: [PATCH 209/270] ui cleanup --- assets/i18n/en.json | 28 +- assets/images/grid.svg | 1 + .../cubits/account_record_cubit.dart | 46 +--- lib/account_manager/models/account_spec.dart | 102 +++++-- .../views/edit_account_page.dart | 134 +++++----- .../views/edit_profile_form.dart | 186 ++++++------- .../views/new_account_page.dart | 37 +-- lib/app.dart | 46 ++-- lib/chat/views/chat_component_widget.dart | 3 +- lib/chat/views/no_conversation_widget.dart | 6 +- .../chat_single_contact_item_widget.dart | 16 +- .../cubits/contact_invitation_list_cubit.dart | 4 +- .../cubits/invitation_generator_cubit.dart | 2 +- .../views/contact_invitation_display.dart | 45 +++- .../views/contact_invitation_item_widget.dart | 14 +- .../views/create_invitation_dialog.dart | 47 ++-- .../views/new_contact_bottom_sheet.dart | 71 ----- lib/contact_invitation/views/views.dart | 1 - lib/contacts/contacts.dart | 1 + lib/contacts/cubits/contact_list_cubit.dart | 17 +- lib/contacts/models/contact_spec.dart | 37 +++ lib/contacts/models/models.dart | 1 + lib/contacts/views/availability_widget.dart | 6 +- .../views/contact_details_widget.dart | 39 ++- lib/contacts/views/contact_item_widget.dart | 6 +- lib/contacts/views/contacts_browser.dart | 22 +- lib/contacts/views/contacts_dialog.dart | 126 +++++---- lib/contacts/views/edit_contact_form.dart | 252 +++++++++++------- lib/layout/home/drawer_menu/drawer_menu.dart | 118 +++----- .../home/drawer_menu/menu_item_widget.dart | 61 +++-- lib/layout/home/home_screen.dart | 5 +- .../views/notifications_preferences.dart | 6 + lib/proto/veilidchat.pb.dart | 15 ++ lib/proto/veilidchat.pbjson.dart | 4 +- lib/proto/veilidchat.proto | 2 + lib/router/cubits/router_cubit.dart | 5 +- lib/settings/settings_page.dart | 3 +- lib/theme/models/chat_theme.dart | 2 +- lib/theme/models/contrast_generator.dart | 94 +++---- lib/theme/models/radix_generator.dart | 68 +---- lib/theme/models/scale_theme/scale_color.dart | 7 + .../scale_custom_dropdown_theme.dart | 1 - .../scale_input_decorator_theme.dart | 117 ++++++-- .../models/scale_theme/scale_scheme.dart | 13 +- lib/theme/models/scale_theme/scale_theme.dart | 83 ++++++ .../models/scale_theme/scale_tile_theme.dart | 5 +- .../models/scale_theme/scale_toast_theme.dart | 16 +- lib/theme/views/avatar_widget.dart | 21 +- lib/theme/views/slider_tile.dart | 11 +- lib/theme/views/styled_alert.dart | 35 --- lib/theme/views/styled_dialog.dart | 2 +- lib/theme/views/widget_helpers.dart | 61 +++-- lib/veilid_processor/views/developer.dart | 4 +- pubspec.lock | 8 + pubspec.yaml | 9 +- build.bat => update_generated_files.bat | 0 build.sh => update_generated_files.sh | 0 57 files changed, 1158 insertions(+), 914 deletions(-) create mode 100644 assets/images/grid.svg delete mode 100644 lib/contact_invitation/views/new_contact_bottom_sheet.dart create mode 100644 lib/contacts/models/contact_spec.dart create mode 100644 lib/contacts/models/models.dart rename build.bat => update_generated_files.bat (100%) rename build.sh => update_generated_files.sh (100%) diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 4334a6b..ef4c44c 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -50,7 +50,6 @@ "edit_account_page": { "titlebar": "Edit Account", "header": "Account Profile", - "update": "Update", "instructions": "This information will be shared with the people you invite to connect with you on VeilidChat.", "error": "Account modification error", "name": "Name", @@ -64,7 +63,6 @@ "destroy_account_description": "Destroy account, removing it completely from all devices everywhere", "destroy_account_confirm_message": "This action is PERMANENT, and your VeilidChat account will no longer be recoverable with the recovery key. Restoring from backups will not recover your account!", "destroy_account_confirm_message_details": "You will lose access to:\n • Your entire message history\n • Your contacts\n • This will not remove your messages you have sent from other people's devices\n", - "confirm_are_you_sure": "Are you sure you want to do this?", "failed_to_remove_title": "Failed to remove account", "try_again_network": "Try again when you have a more stable network connection", "failed_to_destroy_title": "Failed to destroy account", @@ -84,6 +82,12 @@ "view": "View", "share": "Share" }, + "confirmation": { + "confirm": "Confirm", + "discard_changes": "Discard changes?", + "are_you_sure_discard": "Are you sure you want to discard your changes?", + "are_you_sure": "Are you sure you want to do this?" + }, "button": { "ok": "Ok", "cancel": "Cancel", @@ -95,10 +99,10 @@ "close": "Close", "yes": "Yes", "no": "No", + "update": "Update", "waiting_for_network": "Waiting For Network" }, "toast": { - "confirm": "Confirm", "error": "Error", "info": "Info" }, @@ -142,9 +146,7 @@ "form_nickname": "Nickname", "form_notes": "Notes", "form_fingerprint": "Fingerprint", - "form_show_availability": "Show availability", - "save": "Save", - "save_disabled": "Save" + "form_show_availability": "Show availability" }, "availability": { "unspecified": "Unspecified", @@ -172,18 +174,21 @@ "create_invitation_dialog": { "title": "Create Contact Invitation", "me": "me", - "fingerprint": "Fingerprint:", + "recipient_name": "Contact Name", + "recipient_hint": "Enter the recipient's name", + "recipient_helper": "Name of the person you are inviting to chat", + "message_hint": "Enter message for contact (optional)", + "message_label": "Message", + "message_helper": "Message to send with invitation", + "fingerprint": "Fingerprint", "connect_with_me": "Connect with {name} on VeilidChat!", - "enter_message_hint": "Enter message for contact (optional)", - "message_to_contact": "Message to send with invitation (not encrypted)", "generate": "Generate Invitation", - "message": "Message", "unlocked": "Unlocked", "pin": "PIN", "password": "Password", "protect_this_invitation": "Protect this invitation:", "note": "Note:", - "note_text": "Contact invitations can be used by anyone. Make sure you send the invitation to your contact over a secure medium, and preferably use a password or pin to ensure that they are the only ones who can unlock the invitation and accept it.", + "note_text": "Do not post contact invitations publicly.\n\nContact invitations can be used by anyone. Make sure you send the invitation to your contact over a secure medium, and preferably use a password or pin to ensure that they are the only ones who can unlock the invitation and accept it.", "pin_description": "Choose a PIN to protect the contact invite.\n\nThis level of security is appropriate only for casual connections in public environments for 'shoulder surfing' protection.", "password_description": "Choose a strong password to protect the contact invite.\n\nThis level of security is appropriate when you must be sure the contact invitation is only accepted by its intended recipient. Share this password over a different medium than the invite itself.", "pin_does_not_match": "PIN does not match", @@ -193,6 +198,7 @@ "invitation_copied": "Invitation Copied" }, "invitation_dialog": { + "to": "To", "message_from_contact": "Message from contact", "validating": "Validating...", "failed_to_accept": "Failed to accept contact invitation", diff --git a/assets/images/grid.svg b/assets/images/grid.svg new file mode 100644 index 0000000..f30d577 --- /dev/null +++ b/assets/images/grid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/account_manager/cubits/account_record_cubit.dart b/lib/account_manager/cubits/account_record_cubit.dart index 9a73246..16ab2e0 100644 --- a/lib/account_manager/cubits/account_record_cubit.dart +++ b/lib/account_manager/cubits/account_record_cubit.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:async_tools/async_tools.dart'; -import 'package:protobuf/protobuf.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../proto/proto.dart' as proto; @@ -47,53 +46,30 @@ class AccountRecordCubit extends DefaultDHTRecordCubit { // Public Interface void updateAccount( - AccountSpec accountSpec, Future Function() onSuccess) { - _sspUpdate.updateState((accountSpec, onSuccess), (state) async { + AccountSpec accountSpec, Future Function() onChanged) { + _sspUpdate.updateState((accountSpec, onChanged), (state) async { await _updateAccountAsync(state.$1, state.$2); }); } Future _updateAccountAsync( - AccountSpec accountSpec, Future Function() onSuccess) async { - var changed = false; - + AccountSpec accountSpec, Future Function() onChanged) async { + var changed = true; await record?.eventualUpdateProtobuf(proto.Account.fromBuffer, (old) async { - changed = false; if (old == null) { return null; } - final newAccount = old.deepCopy() - ..profile.name = accountSpec.name - ..profile.pronouns = accountSpec.pronouns - ..profile.about = accountSpec.about - ..profile.availability = accountSpec.availability - ..profile.status = accountSpec.status - //..profile.avatar = - ..profile.timestamp = Veilid.instance.now().toInt64() - ..invisible = accountSpec.invisible - ..autodetectAway = accountSpec.autoAway - ..autoAwayTimeoutMin = accountSpec.autoAwayTimeout - ..freeMessage = accountSpec.freeMessage - ..awayMessage = accountSpec.awayMessage - ..busyMessage = accountSpec.busyMessage; + final oldAccountSpec = AccountSpec.fromProto(old); + changed = oldAccountSpec != accountSpec; + if (!changed) { + return null; + } - if (newAccount.profile != old.profile || - newAccount.invisible != old.invisible || - newAccount.autodetectAway != old.autodetectAway || - newAccount.autoAwayTimeoutMin != old.autoAwayTimeoutMin || - newAccount.freeMessage != old.freeMessage || - newAccount.busyMessage != old.busyMessage || - newAccount.awayMessage != old.awayMessage) { - changed = true; - } - if (changed) { - return newAccount; - } - return null; + return accountSpec.updateProto(old); }); if (changed) { - await onSuccess(); + await onChanged(); } } diff --git a/lib/account_manager/models/account_spec.dart b/lib/account_manager/models/account_spec.dart index 539b8d0..918e192 100644 --- a/lib/account_manager/models/account_spec.dart +++ b/lib/account_manager/models/account_spec.dart @@ -1,12 +1,16 @@ -import 'package:flutter/widgets.dart'; +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; +import 'package:protobuf/protobuf.dart'; +import 'package:veilid_support/veilid_support.dart'; import '../../proto/proto.dart' as proto; /// Profile and Account configurable fields /// Some are publicly visible via the proto.Profile /// Some are privately held as proto.Account configurations -class AccountSpec { - AccountSpec( +@immutable +class AccountSpec extends Equatable { + const AccountSpec( {required this.name, required this.pronouns, required this.about, @@ -19,37 +23,99 @@ class AccountSpec { required this.autoAway, required this.autoAwayTimeout}); + const AccountSpec.empty() + : name = '', + pronouns = '', + about = '', + availability = proto.Availability.AVAILABILITY_FREE, + invisible = false, + freeMessage = '', + awayMessage = '', + busyMessage = '', + avatar = null, + autoAway = false, + autoAwayTimeout = 15; + + AccountSpec.fromProto(proto.Account p) + : name = p.profile.name, + pronouns = p.profile.pronouns, + about = p.profile.about, + availability = p.profile.availability, + invisible = p.invisible, + freeMessage = p.freeMessage, + awayMessage = p.awayMessage, + busyMessage = p.busyMessage, + avatar = p.profile.hasAvatar() ? p.profile.avatar : null, + autoAway = p.autodetectAway, + autoAwayTimeout = p.autoAwayTimeoutMin; + String get status { late final String status; switch (availability) { case proto.Availability.AVAILABILITY_AWAY: status = awayMessage; - break; case proto.Availability.AVAILABILITY_BUSY: status = busyMessage; - break; case proto.Availability.AVAILABILITY_FREE: status = freeMessage; - break; case proto.Availability.AVAILABILITY_UNSPECIFIED: case proto.Availability.AVAILABILITY_OFFLINE: status = ''; - break; } return status; } + Future updateProto(proto.Account old) async { + final newProto = old.deepCopy() + ..profile.name = name + ..profile.pronouns = pronouns + ..profile.about = about + ..profile.availability = availability + ..profile.status = status + ..profile.timestamp = Veilid.instance.now().toInt64() + ..invisible = invisible + ..autodetectAway = autoAway + ..autoAwayTimeoutMin = autoAwayTimeout + ..freeMessage = freeMessage + ..awayMessage = awayMessage + ..busyMessage = busyMessage; + + final newAvatar = avatar; + if (newAvatar != null) { + newProto.profile.avatar = newAvatar; + } else { + newProto.profile.clearAvatar(); + } + + return newProto; + } + //////////////////////////////////////////////////////////////////////////// - String name; - String pronouns; - String about; - proto.Availability availability; - bool invisible; - String freeMessage; - String awayMessage; - String busyMessage; - ImageProvider? avatar; - bool autoAway; - int autoAwayTimeout; + final String name; + final String pronouns; + final String about; + final proto.Availability availability; + final bool invisible; + final String freeMessage; + final String awayMessage; + final String busyMessage; + final proto.DataReference? avatar; + final bool autoAway; + final int autoAwayTimeout; + + @override + List get props => [ + name, + pronouns, + about, + availability, + invisible, + freeMessage, + awayMessage, + busyMessage, + avatar, + autoAway, + autoAwayTimeout + ]; } diff --git a/lib/account_manager/views/edit_account_page.dart b/lib/account_manager/views/edit_account_page.dart index 918c4ca..81b67eb 100644 --- a/lib/account_manager/views/edit_account_page.dart +++ b/lib/account_manager/views/edit_account_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:async_tools/async_tools.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -10,17 +11,18 @@ import 'package:veilid_support/veilid_support.dart'; import '../../layout/default_app_bar.dart'; import '../../notifications/notifications.dart'; -import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; import '../../tools/tools.dart'; import '../../veilid_processor/veilid_processor.dart'; import '../account_manager.dart'; import 'edit_profile_form.dart'; +const _kDoBackArrow = 'doBackArrow'; + class EditAccountPage extends StatefulWidget { const EditAccountPage( {required this.superIdentityRecordKey, - required this.existingAccount, + required this.initialValue, required this.accountRecord, super.key}); @@ -28,7 +30,7 @@ class EditAccountPage extends StatefulWidget { State createState() => _EditAccountPageState(); final TypedKey superIdentityRecordKey; - final proto.Account existingAccount; + final AccountSpec initialValue; final OwnedDHTRecordPointer accountRecord; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -36,8 +38,7 @@ class EditAccountPage extends StatefulWidget { properties ..add(DiagnosticsProperty( 'superIdentityRecordKey', superIdentityRecordKey)) - ..add(DiagnosticsProperty( - 'existingAccount', existingAccount)) + ..add(DiagnosticsProperty('initialValue', initialValue)) ..add(DiagnosticsProperty( 'accountRecord', accountRecord)); } @@ -49,36 +50,14 @@ class _EditAccountPageState extends WindowSetupState { titleBarStyle: TitleBarStyle.normal, orientationCapability: OrientationCapability.portraitOnly); - Widget _editAccountForm(BuildContext context, - {required Future Function(AccountSpec) onUpdate}) => - EditProfileForm( + EditProfileForm _editAccountForm(BuildContext context) => EditProfileForm( header: translate('edit_account_page.header'), instructions: translate('edit_account_page.instructions'), - submitText: translate('edit_account_page.update'), + submitText: translate('button.update'), submitDisabledText: translate('button.waiting_for_network'), - onUpdate: onUpdate, - initialValueCallback: (key) => switch (key) { - EditProfileForm.formFieldName => widget.existingAccount.profile.name, - EditProfileForm.formFieldPronouns => - widget.existingAccount.profile.pronouns, - EditProfileForm.formFieldAbout => - widget.existingAccount.profile.about, - EditProfileForm.formFieldAvailability => - widget.existingAccount.profile.availability, - EditProfileForm.formFieldFreeMessage => - widget.existingAccount.freeMessage, - EditProfileForm.formFieldAwayMessage => - widget.existingAccount.awayMessage, - EditProfileForm.formFieldBusyMessage => - widget.existingAccount.busyMessage, - EditProfileForm.formFieldAvatar => - widget.existingAccount.profile.avatar, - EditProfileForm.formFieldAutoAway => - widget.existingAccount.autodetectAway, - EditProfileForm.formFieldAutoAwayTimeout => - widget.existingAccount.autoAwayTimeoutMin.toString(), - String() => throw UnimplementedError(), - }, + onSubmit: _onSubmit, + onModifiedState: _onModifiedState, + initialValue: widget.initialValue, ); Future _onRemoveAccount() async { @@ -88,8 +67,7 @@ class _EditAccountPageState extends WindowSetupState { child: Column(mainAxisSize: MainAxisSize.min, children: [ Text(translate('edit_account_page.remove_account_confirm_message')) .paddingLTRB(24, 24, 24, 0), - Text(translate('edit_account_page.confirm_are_you_sure')) - .paddingAll(8), + Text(translate('confirmation.are_you_sure')).paddingAll(8), Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ElevatedButton( onPressed: () { @@ -156,8 +134,7 @@ class _EditAccountPageState extends WindowSetupState { Text(translate( 'edit_account_page.destroy_account_confirm_message_details')) .paddingLTRB(24, 24, 24, 0), - Text(translate('edit_account_page.confirm_are_you_sure')) - .paddingAll(8), + Text(translate('confirmation.are_you_sure')).paddingAll(8), Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ElevatedButton( onPressed: () { @@ -214,26 +191,51 @@ class _EditAccountPageState extends WindowSetupState { } } - Future _onUpdate(AccountSpec accountSpec) async { - // Look up account cubit for this specific account - final perAccountCollectionBlocMapCubit = - context.read(); - final accountRecordCubit = await perAccountCollectionBlocMapCubit.operate( - widget.superIdentityRecordKey, - closure: (c) async => c.accountRecordCubit); - if (accountRecordCubit == null) { - return; - } - - // Update account profile DHT record - // This triggers ConversationCubits to update - accountRecordCubit.updateAccount(accountSpec, () async { - // Update local account profile - await AccountRepository.instance - .updateLocalAccount(widget.superIdentityRecordKey, accountSpec); + void _onModifiedState(bool isModified) { + setState(() { + _isModified = isModified; }); } + Future _onSubmit(AccountSpec accountSpec) async { + try { + setState(() { + _isInAsyncCall = true; + }); + try { + // Look up account cubit for this specific account + final perAccountCollectionBlocMapCubit = + context.read(); + final accountRecordCubit = await perAccountCollectionBlocMapCubit + .operate(widget.superIdentityRecordKey, + closure: (c) async => c.accountRecordCubit); + if (accountRecordCubit == null) { + return false; + } + + // Update account profile DHT record + // This triggers ConversationCubits to update + accountRecordCubit.updateAccount(accountSpec, () async { + // Update local account profile + await AccountRepository.instance + .updateLocalAccount(widget.superIdentityRecordKey, accountSpec); + }); + + return true; + } finally { + setState(() { + _isInAsyncCall = false; + }); + } + } on Exception catch (e, st) { + if (mounted) { + await showErrorStacktraceModal( + context: context, error: e, stackTrace: st); + } + } + return false; + } + @override Widget build(BuildContext context) { final displayModalHUD = _isInAsyncCall; @@ -246,9 +248,23 @@ class _EditAccountPageState extends WindowSetupState { ? IconButton( icon: const Icon(Icons.arrow_back), onPressed: () { - Navigator.pop(context); - }, - ) + singleFuture((this, _kDoBackArrow), () async { + if (_isModified) { + final ok = await showConfirmModal( + context: context, + title: + translate('confirmation.discard_changes'), + text: translate( + 'confirmation.are_you_sure_discard')); + if (!ok) { + return; + } + } + if (context.mounted) { + Navigator.pop(context); + } + }); + }) : null, actions: [ const SignalStrengthMeterWidget(), @@ -261,10 +277,7 @@ class _EditAccountPageState extends WindowSetupState { ]), body: SingleChildScrollView( child: Column(children: [ - _editAccountForm( - context, - onUpdate: _onUpdate, - ).paddingLTRB(0, 0, 0, 32), + _editAccountForm(context).paddingLTRB(0, 0, 0, 32), OptionBox( instructions: translate('edit_account_page.remove_account_description'), @@ -286,4 +299,5 @@ class _EditAccountPageState extends WindowSetupState { //////////////////////////////////////////////////////////////////////////// bool _isInAsyncCall = false; + bool _isModified = false; } diff --git a/lib/account_manager/views/edit_profile_form.dart b/lib/account_manager/views/edit_profile_form.dart index 1774bff..d6bb504 100644 --- a/lib/account_manager/views/edit_profile_form.dart +++ b/lib/account_manager/views/edit_profile_form.dart @@ -13,7 +13,7 @@ import '../../theme/theme.dart'; import '../../veilid_processor/veilid_processor.dart'; import '../models/models.dart'; -const _kDoUpdateSubmit = 'doUpdateSubmit'; +const _kDoSubmitEditProfile = 'doSubmitEditProfile'; class EditProfileForm extends StatefulWidget { const EditProfileForm({ @@ -21,9 +21,9 @@ class EditProfileForm extends StatefulWidget { required this.instructions, required this.submitText, required this.submitDisabledText, - required this.initialValueCallback, - this.onUpdate, - this.onSubmit, + required this.initialValue, + required this.onSubmit, + this.onModifiedState, super.key, }); @@ -32,11 +32,11 @@ class EditProfileForm extends StatefulWidget { final String header; final String instructions; - final Future Function(AccountSpec)? onUpdate; - final Future Function(AccountSpec)? onSubmit; + final Future Function(AccountSpec) onSubmit; + final void Function(bool)? onModifiedState; final String submitText; final String submitDisabledText; - final Object Function(String key) initialValueCallback; + final AccountSpec initialValue; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -44,14 +44,13 @@ class EditProfileForm extends StatefulWidget { properties ..add(StringProperty('header', header)) ..add(StringProperty('instructions', instructions)) - ..add(ObjectFlagProperty Function(AccountSpec)?>.has( - 'onUpdate', onUpdate)) ..add(StringProperty('submitText', submitText)) ..add(StringProperty('submitDisabledText', submitDisabledText)) - ..add(ObjectFlagProperty.has( - 'initialValueCallback', initialValueCallback)) - ..add(ObjectFlagProperty Function(AccountSpec)?>.has( - 'onSubmit', onSubmit)); + ..add(ObjectFlagProperty Function(AccountSpec)>.has( + 'onSubmit', onSubmit)) + ..add(ObjectFlagProperty.has( + 'onModifiedState', onModifiedState)) + ..add(DiagnosticsProperty('initialValue', initialValue)); } static const String formFieldName = 'name'; @@ -71,8 +70,9 @@ class _EditProfileFormState extends State { @override void initState() { - _autoAwayEnabled = - widget.initialValueCallback(EditProfileForm.formFieldAutoAway) as bool; + _savedValue = widget.initialValue; + _currentValueName = widget.initialValue.name; + _currentValueAutoAway = widget.initialValue.autoAway; super.initState(); } @@ -82,13 +82,10 @@ class _EditProfileFormState extends State { final theme = Theme.of(context); final scale = theme.extension()!; - final initialValueX = - widget.initialValueCallback(EditProfileForm.formFieldAvailability) - as proto.Availability; final initialValue = - initialValueX == proto.Availability.AVAILABILITY_UNSPECIFIED + _savedValue.availability == proto.Availability.AVAILABILITY_UNSPECIFIED ? proto.Availability.AVAILABILITY_FREE - : initialValueX; + : _savedValue.availability; final availabilities = [ proto.Availability.AVAILABILITY_FREE, @@ -109,7 +106,7 @@ class _EditProfileFormState extends State { value: x, child: Row(mainAxisSize: MainAxisSize.min, children: [ AvailabilityWidget.availabilityIcon( - x, scale.primaryScale.primaryText), + x, scale.primaryScale.appText), Text(x == proto.Availability.AVAILABILITY_OFFLINE ? translate('availability.always_show_offline') : AvailabilityWidget.availabilityName(x)) @@ -138,6 +135,12 @@ class _EditProfileFormState extends State { .fields[EditProfileForm.formFieldAwayMessage]!.value as String; final busyMessage = _formKey.currentState! .fields[EditProfileForm.formFieldBusyMessage]!.value as String; + + const proto.DataReference? avatar = null; + // final avatar = _formKey.currentState! + // .fields[EditProfileForm.formFieldAvatar]!.value + //as proto.DataReference?; + final autoAway = _formKey .currentState!.fields[EditProfileForm.formFieldAutoAway]!.value as bool; final autoAwayTimeoutString = _formKey.currentState! @@ -153,11 +156,21 @@ class _EditProfileFormState extends State { freeMessage: freeMessage, awayMessage: awayMessage, busyMessage: busyMessage, - avatar: null, + avatar: avatar, autoAway: autoAway, autoAwayTimeout: autoAwayTimeout); } + // Check if everything is the same and update state + void _onChanged() { + final currentValue = _makeAccountSpec(); + _isModified = currentValue != _savedValue; + final onModifiedState = widget.onModifiedState; + if (onModifiedState != null) { + onModifiedState(_isModified); + } + } + Widget _editProfileForm( BuildContext context, ) { @@ -176,24 +189,32 @@ class _EditProfileFormState extends State { return FormBuilder( key: _formKey, autovalidateMode: AutovalidateMode.onUserInteraction, + onChanged: _onChanged, child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - AvatarWidget( - name: _formKey.currentState?.value[EditProfileForm.formFieldName] - as String? ?? - '?', - size: 128, - borderColor: border, - foregroundColor: scale.primaryScale.primaryText, - backgroundColor: scale.primaryScale.primary, - scaleConfig: scaleConfig, - textStyle: theme.textTheme.titleLarge!.copyWith(fontSize: 64), - ).paddingLTRB(0, 0, 0, 16), + Row(children: [ + const Spacer(), + AvatarWidget( + name: _currentValueName, + size: 128, + borderColor: border, + foregroundColor: scale.primaryScale.primaryText, + backgroundColor: scale.primaryScale.primary, + scaleConfig: scaleConfig, + textStyle: theme.textTheme.titleLarge!.copyWith(fontSize: 64), + ).paddingLTRB(0, 0, 0, 16), + const Spacer() + ]), FormBuilderTextField( autofocus: true, name: EditProfileForm.formFieldName, - initialValue: widget - .initialValueCallback(EditProfileForm.formFieldName) as String, + initialValue: _savedValue.name, + onChanged: (x) { + setState(() { + _currentValueName = x ?? ''; + }); + }, decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.always, labelText: translate('account.form_name'), @@ -204,23 +225,20 @@ class _EditProfileFormState extends State { FormBuilderValidators.required(), ]), textInputAction: TextInputAction.next, - ).onFocusChange(_onFocusChange), + ), FormBuilderTextField( name: EditProfileForm.formFieldPronouns, - initialValue: - widget.initialValueCallback(EditProfileForm.formFieldPronouns) - as String, + initialValue: _savedValue.pronouns, maxLength: 64, decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.always, labelText: translate('account.form_pronouns'), hintText: translate('account.empty_pronouns')), textInputAction: TextInputAction.next, - ).onFocusChange(_onFocusChange), + ), FormBuilderTextField( name: EditProfileForm.formFieldAbout, - initialValue: widget - .initialValueCallback(EditProfileForm.formFieldAbout) as String, + initialValue: _savedValue.about, maxLength: 1024, maxLines: 8, minLines: 1, @@ -229,74 +247,69 @@ class _EditProfileFormState extends State { labelText: translate('account.form_about'), hintText: translate('account.empty_about')), textInputAction: TextInputAction.newline, - ).onFocusChange(_onFocusChange), - _availabilityDropDown(context) - .paddingLTRB(0, 0, 0, 16) - .onFocusChange(_onFocusChange), + ), + _availabilityDropDown(context).paddingLTRB(0, 0, 0, 16), FormBuilderTextField( name: EditProfileForm.formFieldFreeMessage, - initialValue: widget.initialValueCallback( - EditProfileForm.formFieldFreeMessage) as String, + initialValue: _savedValue.freeMessage, maxLength: 128, decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.always, labelText: translate('account.form_free_message'), hintText: translate('account.empty_free_message')), textInputAction: TextInputAction.next, - ).onFocusChange(_onFocusChange), + ), FormBuilderTextField( name: EditProfileForm.formFieldAwayMessage, - initialValue: widget.initialValueCallback( - EditProfileForm.formFieldAwayMessage) as String, + initialValue: _savedValue.awayMessage, maxLength: 128, decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.always, labelText: translate('account.form_away_message'), hintText: translate('account.empty_away_message')), textInputAction: TextInputAction.next, - ).onFocusChange(_onFocusChange), + ), FormBuilderTextField( name: EditProfileForm.formFieldBusyMessage, - initialValue: widget.initialValueCallback( - EditProfileForm.formFieldBusyMessage) as String, + initialValue: _savedValue.busyMessage, maxLength: 128, decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.always, labelText: translate('account.form_busy_message'), hintText: translate('account.empty_busy_message')), textInputAction: TextInputAction.next, - ).onFocusChange(_onFocusChange), + ), FormBuilderCheckbox( name: EditProfileForm.formFieldAutoAway, - initialValue: - widget.initialValueCallback(EditProfileForm.formFieldAutoAway) - as bool, + initialValue: _savedValue.autoAway, side: BorderSide(color: scale.primaryScale.border, width: 2), + checkColor: scale.primaryScale.borderText, + activeColor: scale.primaryScale.border, title: Text(translate('account.form_auto_away'), style: textTheme.labelMedium), onChanged: (v) { setState(() { - _autoAwayEnabled = v ?? false; + _currentValueAutoAway = v ?? false; }); }, - ).onFocusChange(_onFocusChange), + ), FormBuilderTextField( name: EditProfileForm.formFieldAutoAwayTimeout, - enabled: _autoAwayEnabled, - initialValue: widget.initialValueCallback( - EditProfileForm.formFieldAutoAwayTimeout) as String, + enabled: _currentValueAutoAway, + initialValue: _savedValue.autoAwayTimeout.toString(), decoration: InputDecoration( labelText: translate('account.form_auto_away_timeout'), ), validator: FormBuilderValidators.positiveNumber(), textInputAction: TextInputAction.next, - ).onFocusChange(_onFocusChange), + ), Row(children: [ const Spacer(), Text(widget.instructions).toCenter().flexible(flex: 6), const Spacer(), - ]).paddingSymmetric(vertical: 4), - if (widget.onSubmit != null) + ]).paddingSymmetric(vertical: 16), + Row(children: [ + const Spacer(), Builder(builder: (context) { final networkReady = context .watch() @@ -307,7 +320,7 @@ class _EditProfileFormState extends State { false; return ElevatedButton( - onPressed: networkReady ? _doSubmit : null, + onPressed: (networkReady && _isModified) ? _doSubmit : null, child: Row(mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0), Text(networkReady @@ -317,36 +330,24 @@ class _EditProfileFormState extends State { ]), ); }), + const Spacer() + ]) ], ), ); } - void _onFocusChange(bool focused) { - if (!focused) { - _doUpdate(); - } - } - - void _doUpdate() { - final onUpdate = widget.onUpdate; - if (onUpdate != null) { - singleFuture((this, _kDoUpdateSubmit), () async { - if (_formKey.currentState?.saveAndValidate() ?? false) { - final aus = _makeAccountSpec(); - await onUpdate(aus); - } - }); - } - } - void _doSubmit() { final onSubmit = widget.onSubmit; - if (onSubmit != null) { - singleFuture((this, _kDoUpdateSubmit), () async { - if (_formKey.currentState?.saveAndValidate() ?? false) { - final aus = _makeAccountSpec(); - await onSubmit(aus); + if (_formKey.currentState?.saveAndValidate() ?? false) { + singleFuture((this, _kDoSubmitEditProfile), () async { + final updatedAccountSpec = _makeAccountSpec(); + final saved = await onSubmit(updatedAccountSpec); + if (saved) { + setState(() { + _savedValue = updatedAccountSpec; + }); + _onChanged(); } }); } @@ -358,5 +359,8 @@ class _EditProfileFormState extends State { ); /////////////////////////////////////////////////////////////////////////// - late bool _autoAwayEnabled; + late AccountSpec _savedValue; + late bool _currentValueAutoAway; + late String _currentValueName; + bool _isModified = false; } diff --git a/lib/account_manager/views/new_account_page.dart b/lib/account_manager/views/new_account_page.dart index ccd5b00..a739094 100644 --- a/lib/account_manager/views/new_account_page.dart +++ b/lib/account_manager/views/new_account_page.dart @@ -8,7 +8,6 @@ import 'package:go_router/go_router.dart'; import '../../layout/default_app_bar.dart'; import '../../notifications/cubits/notifications_cubit.dart'; -import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; import '../../tools/tools.dart'; import '../../veilid_processor/veilid_processor.dart'; @@ -28,33 +27,6 @@ class _NewAccountPageState extends WindowSetupState { titleBarStyle: TitleBarStyle.normal, orientationCapability: OrientationCapability.portraitOnly); - Object _defaultAccountValues(String key) { - switch (key) { - case EditProfileForm.formFieldName: - return ''; - case EditProfileForm.formFieldPronouns: - return ''; - case EditProfileForm.formFieldAbout: - return ''; - case EditProfileForm.formFieldAvailability: - return proto.Availability.AVAILABILITY_FREE; - case EditProfileForm.formFieldFreeMessage: - return ''; - case EditProfileForm.formFieldAwayMessage: - return ''; - case EditProfileForm.formFieldBusyMessage: - return ''; - // case EditProfileForm.formFieldAvatar: - // return null; - case EditProfileForm.formFieldAutoAway: - return false; - case EditProfileForm.formFieldAutoAwayTimeout: - return '15'; - default: - throw StateError('missing form element'); - } - } - Widget _newAccountForm( BuildContext context, ) => @@ -63,10 +35,10 @@ class _NewAccountPageState extends WindowSetupState { instructions: translate('new_account_page.instructions'), submitText: translate('new_account_page.create'), submitDisabledText: translate('button.waiting_for_network'), - initialValueCallback: _defaultAccountValues, + initialValue: const AccountSpec.empty(), onSubmit: _onSubmit); - Future _onSubmit(AccountSpec accountSpec) async { + Future _onSubmit(AccountSpec accountSpec) async { // dismiss the keyboard by unfocusing the textfield FocusScope.of(context).unfocus(); @@ -88,13 +60,15 @@ class _NewAccountPageState extends WindowSetupState { context.read().error( text: translate('new_account_page.network_is_offline'), title: translate('new_account_page.error')); - return; + return false; } final writableSuperIdentity = await AccountRepository.instance .createWithNewSuperIdentity(accountSpec); GoRouterHelper(context).pushReplacement('/new_account/recovery_key', extra: [writableSuperIdentity, accountSpec.name]); + + return true; } finally { if (mounted) { setState(() { @@ -108,6 +82,7 @@ class _NewAccountPageState extends WindowSetupState { context: context, error: e, stackTrace: st); } } + return false; } @override diff --git a/lib/app.dart b/lib/app.dart index 7ef0911..fade2c8 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:provider/provider.dart'; @@ -163,23 +164,34 @@ class VeilidChatApp extends StatelessWidget { scale.primaryScale.subtleBackground, ]); - return DecoratedBox( - decoration: BoxDecoration(gradient: gradient), - child: MaterialApp.router( - scrollBehavior: const ScrollBehaviorModified(), - debugShowCheckedModeBanner: false, - routerConfig: context.read().router(), - title: translate('app.title'), - theme: theme, - localizationsDelegates: [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - FormBuilderLocalizations.delegate, - localizationDelegate - ], - supportedLocales: localizationDelegate.supportedLocales, - locale: localizationDelegate.currentLocale, - )); + return Stack( + fit: StackFit.expand, + alignment: Alignment.center, + children: [ + DecoratedBox( + decoration: BoxDecoration(gradient: gradient)), + SvgPicture.asset( + 'assets/images/grid.svg', + fit: BoxFit.cover, + colorFilter: overlayFilter, + ), + MaterialApp.router( + scrollBehavior: const ScrollBehaviorModified(), + debugShowCheckedModeBanner: false, + routerConfig: context.read().router(), + title: translate('app.title'), + theme: theme, + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + FormBuilderLocalizations.delegate, + localizationDelegate + ], + supportedLocales: + localizationDelegate.supportedLocales, + locale: localizationDelegate.currentLocale, + ) + ]); })), )), ); diff --git a/lib/chat/views/chat_component_widget.dart b/lib/chat/views/chat_component_widget.dart index c4816ae..79d1f1c 100644 --- a/lib/chat/views/chat_component_widget.dart +++ b/lib/chat/views/chat_component_widget.dart @@ -147,8 +147,7 @@ class ChatComponentWidget extends StatelessWidget { ]), ), DecoratedBox( - decoration: - BoxDecoration(color: scale.primaryScale.subtleBackground), + decoration: const BoxDecoration(color: Colors.transparent), child: NotificationListener( onNotification: (notification) { if (chatComponentCubit.scrollOffset != 0) { diff --git a/lib/chat/views/no_conversation_widget.dart b/lib/chat/views/no_conversation_widget.dart index e246fee..df1b6d3 100644 --- a/lib/chat/views/no_conversation_widget.dart +++ b/lib/chat/views/no_conversation_widget.dart @@ -16,7 +16,7 @@ class NoConversationWidget extends StatelessWidget { return DecoratedBox( decoration: BoxDecoration( - color: scale.primaryScale.appBackground, + color: scale.primaryScale.appBackground.withAlpha(192), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -24,14 +24,14 @@ class NoConversationWidget extends StatelessWidget { children: [ Icon( Icons.diversity_3, - color: scale.primaryScale.subtleBorder, + color: scale.primaryScale.appText.withAlpha(127), size: 48, ), Text( textAlign: TextAlign.center, translate('chat.start_a_conversation'), style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: scale.primaryScale.subtleBorder, + color: scale.primaryScale.appText.withAlpha(127), ), ), ], 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 826fe37..3bdf645 100644 --- a/lib/chat_list/views/chat_single_contact_item_widget.dart +++ b/lib/chat_list/views/chat_single_contact_item_widget.dart @@ -44,20 +44,22 @@ class ChatSingleContactItemWidget extends StatelessWidget { : _contact.profile.availability; final scaleTileTheme = scaleTheme.tileTheme( - disabled: _disabled, - selected: selected, - scaleKind: ScaleKind.secondary); + disabled: _disabled, + selected: selected, + ); final avatar = AvatarWidget( name: name, size: 34, - borderColor: scaleTileTheme.borderColor, + borderColor: scaleTheme.config.useVisualIndicators + ? scaleTheme.scheme.primaryScale.primaryText + : scaleTheme.scheme.primaryScale.subtleBorder, foregroundColor: _disabled ? scaleTheme.scheme.grayScale.primaryText - : scaleTheme.scheme.secondaryScale.primaryText, + : scaleTheme.scheme.primaryScale.primaryText, backgroundColor: _disabled ? scaleTheme.scheme.grayScale.primary - : scaleTheme.scheme.secondaryScale.primary, + : scaleTheme.scheme.primaryScale.primary, scaleConfig: scaleTheme.config, textStyle: theme.textTheme.titleLarge!, ); @@ -66,7 +68,7 @@ class ChatSingleContactItemWidget extends StatelessWidget { key: ValueKey(_localConversationRecordKey), disabled: _disabled, selected: selected, - tileScale: ScaleKind.secondary, + tileScale: ScaleKind.primary, title: title, subtitle: subtitle, leading: avatar, diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index 959445c..768cf2f 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -59,6 +59,7 @@ class ContactInvitationListCubit {required proto.Profile profile, required EncryptionKeyType encryptionKeyType, required String encryptionKey, + required String recipient, required String message, required Timestamp? expiration}) async { final pool = DHTRecordPool.instance; @@ -154,7 +155,8 @@ class ContactInvitationListCubit ..localConversationRecordKey = localConversation.key.toProto() ..expiration = expiration?.toInt64() ?? Int64.ZERO ..invitation = signedContactInvitationBytes - ..message = message; + ..message = message + ..recipient = recipient; // Add ContactInvitationRecord to account's list await operateWriteEventual((writer) async { diff --git a/lib/contact_invitation/cubits/invitation_generator_cubit.dart b/lib/contact_invitation/cubits/invitation_generator_cubit.dart index 5c0fa15..8d2226c 100644 --- a/lib/contact_invitation/cubits/invitation_generator_cubit.dart +++ b/lib/contact_invitation/cubits/invitation_generator_cubit.dart @@ -5,5 +5,5 @@ import 'package:veilid_support/veilid_support.dart'; class InvitationGeneratorCubit extends FutureCubit<(Uint8List, TypedKey)> { InvitationGeneratorCubit(super.fut); - InvitationGeneratorCubit.value(super.v) : super.value(); + InvitationGeneratorCubit.value(super.state) : super.value(); } diff --git a/lib/contact_invitation/views/contact_invitation_display.dart b/lib/contact_invitation/views/contact_invitation_display.dart index 83b80d0..b3f048a 100644 --- a/lib/contact_invitation/views/contact_invitation_display.dart +++ b/lib/contact_invitation/views/contact_invitation_display.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:auto_size_text/auto_size_text.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:basic_utils/basic_utils.dart'; import 'package:flutter/foundation.dart'; @@ -20,11 +21,13 @@ import '../contact_invitation.dart'; class ContactInvitationDisplayDialog extends StatelessWidget { const ContactInvitationDisplayDialog._({ required this.locator, + required this.recipient, required this.message, required this.fingerprint, }); final Locator locator; + final String recipient; final String message; final String fingerprint; @@ -32,18 +35,22 @@ class ContactInvitationDisplayDialog extends StatelessWidget { void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties + ..add(StringProperty('recipient', recipient)) ..add(StringProperty('message', message)) ..add(DiagnosticsProperty('locator', locator)) ..add(StringProperty('fingerprint', fingerprint)); } - String makeTextInvite(String message, Uint8List data) { + String makeTextInvite(String recipient, String message, Uint8List data) { final invite = StringUtils.addCharAtPosition( base64UrlNoPadEncode(data), '\n', 40, repeat: true); + final to = recipient.isNotEmpty + ? '${translate('invitiation_dialog.to')}: $recipient\n' + : ''; final msg = message.isNotEmpty ? '$message\n' : ''; - - return '$msg' + return '$to' + '$msg' '--- BEGIN VEILIDCHAT CONTACT INVITE ----\n' '$invite\n' '---- END VEILIDCHAT CONTACT INVITE -----\n' @@ -62,6 +69,10 @@ class ContactInvitationDisplayDialog extends StatelessWidget { final cardsize = min(MediaQuery.of(context).size.shortestSide - 48.0, 400); + final fingerprintText = + '${translate('create_invitation_dialog.fingerprint')}\n' + '$fingerprint'; + return BlocListener( bloc: locator(), @@ -110,14 +121,21 @@ class ContactInvitationDisplayDialog extends StatelessWidget { errorCorrectLevel: QrErrorCorrectLevel.L)), ).expanded(), - Text(message, - softWrap: true, - style: textTheme.labelLarge! - .copyWith(color: Colors.black)) - .paddingAll(8), - Text( - '${translate('create_invitation_dialog.fingerprint')}\n' - '$fingerprint', + if (recipient.isNotEmpty) + AutoSizeText(recipient, + softWrap: true, + maxLines: 2, + style: textTheme.labelLarge! + .copyWith(color: Colors.black)) + .paddingAll(8), + if (message.isNotEmpty) + Text(message, + softWrap: true, + maxLines: 2, + style: textTheme.labelMedium! + .copyWith(color: Colors.black)) + .paddingAll(8), + Text(fingerprintText, softWrap: true, textAlign: TextAlign.center, style: textTheme.labelSmall!.copyWith( @@ -137,7 +155,8 @@ class ContactInvitationDisplayDialog extends StatelessWidget { text: translate('create_invitation_dialog' '.invitation_copied')); await Clipboard.setData(ClipboardData( - text: makeTextInvite(message, data.$1))); + text: makeTextInvite( + recipient, message, data.$1))); }, ).paddingAll(16), ]), @@ -148,6 +167,7 @@ class ContactInvitationDisplayDialog extends StatelessWidget { required BuildContext context, required Locator locator, required InvitationGeneratorCubit Function(BuildContext) create, + required String recipient, required String message, }) async { final fingerprint = @@ -159,6 +179,7 @@ class ContactInvitationDisplayDialog extends StatelessWidget { create: create, child: ContactInvitationDisplayDialog._( locator: locator, + recipient: recipient, message: message, fingerprint: fingerprint, ))); diff --git a/lib/contact_invitation/views/contact_invitation_item_widget.dart b/lib/contact_invitation/views/contact_invitation_item_widget.dart index 6e6dfcf..779f962 100644 --- a/lib/contact_invitation/views/contact_invitation_item_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_item_widget.dart @@ -37,14 +37,19 @@ class ContactInvitationItemWidget extends StatelessWidget { final tileDisabled = disabled || context.watch().isBusy; + var title = translate('contact_list.invitation'); + if (contactInvitationRecord.recipient.isNotEmpty) { + title = contactInvitationRecord.recipient; + } else if (contactInvitationRecord.message.isNotEmpty) { + title = contactInvitationRecord.message; + } + return SliderTile( key: ObjectKey(contactInvitationRecord), disabled: tileDisabled, selected: selected, tileScale: ScaleKind.primary, - title: contactInvitationRecord.message.isEmpty - ? translate('contact_list.invitation') - : contactInvitationRecord.message, + title: title, leading: const Icon(Icons.person_add), onTap: () async { if (!context.mounted) { @@ -53,6 +58,7 @@ class ContactInvitationItemWidget extends StatelessWidget { await ContactInvitationDisplayDialog.show( context: context, locator: context.read, + recipient: contactInvitationRecord.recipient, message: contactInvitationRecord.message, create: (context) => InvitationGeneratorCubit.value(( Uint8List.fromList(contactInvitationRecord.invitation), @@ -62,7 +68,7 @@ class ContactInvitationItemWidget extends StatelessWidget { }, endActions: [ SliderTileAction( - icon: Icons.delete, + // icon: Icons.delete, label: translate('button.delete'), actionScale: ScaleKind.tertiary, onPressed: (context) async { diff --git a/lib/contact_invitation/views/create_invitation_dialog.dart b/lib/contact_invitation/views/create_invitation_dialog.dart index f1115d7..d835de8 100644 --- a/lib/contact_invitation/views/create_invitation_dialog.dart +++ b/lib/contact_invitation/views/create_invitation_dialog.dart @@ -18,7 +18,7 @@ class CreateInvitationDialog extends StatefulWidget { const CreateInvitationDialog._({required this.locator}); @override - CreateInvitationDialogState createState() => CreateInvitationDialogState(); + State createState() => _CreateInvitationDialogState(); static Future show(BuildContext context) async { await StyledDialog.show( @@ -36,8 +36,9 @@ class CreateInvitationDialog extends StatefulWidget { } } -class CreateInvitationDialogState extends State { +class _CreateInvitationDialogState extends State { late final TextEditingController _messageTextController; + late final TextEditingController _recipientTextController; EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none; String _encryptionKey = ''; @@ -51,6 +52,7 @@ class CreateInvitationDialogState extends State { _messageTextController = TextEditingController( text: translate('create_invitation_dialog.connect_with_me', args: {'name': name})); + _recipientTextController = TextEditingController(); super.initState(); } @@ -154,6 +156,7 @@ class CreateInvitationDialogState extends State { profile: profile, encryptionKeyType: _encryptionKeyType, encryptionKey: _encryptionKey, + recipient: _recipientTextController.text, message: _messageTextController.text, expiration: _expiration); @@ -162,6 +165,7 @@ class CreateInvitationDialogState extends State { await ContactInvitationDisplayDialog.show( context: context, locator: widget.locator, + recipient: _recipientTextController.text, message: _messageTextController.text, create: (context) => InvitationGeneratorCubit(generator)); } @@ -176,6 +180,7 @@ class CreateInvitationDialogState extends State { final theme = Theme.of(context); //final scale = theme.extension()!; final textTheme = theme.textTheme; + return ConstrainedBox( constraints: BoxConstraints(maxHeight: maxDialogHeight, maxWidth: maxDialogWidth), @@ -185,19 +190,34 @@ class CreateInvitationDialogState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text( - translate('create_invitation_dialog.message_to_contact'), + TextField( + controller: _recipientTextController, + onChanged: (value) { + setState(() {}); + }, + inputFormatters: [ + LengthLimitingTextInputFormatter(128), + ], + decoration: InputDecoration( + hintText: + translate('create_invitation_dialog.recipient_hint'), + labelText: + translate('create_invitation_dialog.recipient_name'), + helperText: + translate('create_invitation_dialog.recipient_helper')), ).paddingAll(8), + const SizedBox(height: 10), TextField( controller: _messageTextController, inputFormatters: [ LengthLimitingTextInputFormatter(128), ], decoration: InputDecoration( - //border: const OutlineInputBorder(), - hintText: - translate('create_invitation_dialog.enter_message_hint'), - labelText: translate('create_invitation_dialog.message')), + hintText: translate('create_invitation_dialog.message_hint'), + labelText: + translate('create_invitation_dialog.message_label'), + helperText: + translate('create_invitation_dialog.message_helper')), ).paddingAll(8), const SizedBox(height: 10), Text(translate('create_invitation_dialog.protect_this_invitation'), @@ -228,7 +248,9 @@ class CreateInvitationDialogState extends State { Container( padding: const EdgeInsets.all(8), child: ElevatedButton( - onPressed: _onGenerateButtonPressed, + onPressed: _recipientTextController.text.isNotEmpty + ? _onGenerateButtonPressed + : null, child: Text( translate('create_invitation_dialog.generate'), ).paddingAll(16), @@ -244,11 +266,4 @@ class CreateInvitationDialogState extends State { ), ); } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty( - 'messageTextController', _messageTextController)); - } } diff --git a/lib/contact_invitation/views/new_contact_bottom_sheet.dart b/lib/contact_invitation/views/new_contact_bottom_sheet.dart deleted file mode 100644 index a79a07f..0000000 --- a/lib/contact_invitation/views/new_contact_bottom_sheet.dart +++ /dev/null @@ -1,71 +0,0 @@ -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 'create_invitation_dialog.dart'; -import 'paste_invitation_dialog.dart'; -import 'scan_invitation_dialog.dart'; - -Widget newContactBottomSheetBuilder( - BuildContext sheetContext, BuildContext context) { - final theme = Theme.of(sheetContext); - final scale = theme.extension()!; - - return KeyboardListener( - focusNode: FocusNode(), - onKeyEvent: (ke) { - if (ke.logicalKey == LogicalKeyboardKey.escape) { - Navigator.pop(sheetContext); - } - }, - child: styledBottomSheet( - context: context, - title: translate('add_contact_sheet.new_contact'), - child: SizedBox( - height: 160, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Column(children: [ - IconButton( - onPressed: () async { - Navigator.pop(sheetContext); - await CreateInvitationDialog.show(context); - }, - iconSize: 64, - icon: const Icon(Icons.contact_page), - color: scale.primaryScale.hoverBorder), - Text( - translate('add_contact_sheet.create_invite'), - ) - ]), - Column(children: [ - IconButton( - onPressed: () async { - Navigator.pop(sheetContext); - await ScanInvitationDialog.show(context); - }, - iconSize: 64, - icon: const Icon(Icons.qr_code_scanner), - color: scale.primaryScale.hoverBorder), - Text( - translate('add_contact_sheet.scan_invite'), - ) - ]), - Column(children: [ - IconButton( - onPressed: () async { - Navigator.pop(sheetContext); - await PasteInvitationDialog.show(context); - }, - iconSize: 64, - icon: const Icon(Icons.paste), - color: scale.primaryScale.hoverBorder), - Text( - translate('add_contact_sheet.paste_invite'), - ) - ]) - ]).paddingAll(16)))); -} diff --git a/lib/contact_invitation/views/views.dart b/lib/contact_invitation/views/views.dart index 726f0b9..241513d 100644 --- a/lib/contact_invitation/views/views.dart +++ b/lib/contact_invitation/views/views.dart @@ -3,6 +3,5 @@ export 'contact_invitation_item_widget.dart'; export 'contact_invitation_list_widget.dart'; export 'create_invitation_dialog.dart'; export 'invitation_dialog.dart'; -export 'new_contact_bottom_sheet.dart'; export 'paste_invitation_dialog.dart'; export 'scan_invitation_dialog.dart'; diff --git a/lib/contacts/contacts.dart b/lib/contacts/contacts.dart index 6acdd43..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 index d3c6483..df70cc0 100644 --- a/lib/contacts/cubits/contact_list_cubit.dart +++ b/lib/contacts/cubits/contact_list_cubit.dart @@ -8,6 +8,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'; ////////////////////////////////////////////////// // Mutable state for per-account contacts @@ -81,9 +82,7 @@ class ContactListCubit extends DHTShortArrayCubit { Future updateContactFields({ required TypedKey localConversationRecordKey, - String? nickname, - String? notes, - bool? showAvailability, + required ContactSpec updatedContactSpec, }) async { // Update contact's locally-modifiable fields await operateWriteEventual((writer) async { @@ -92,17 +91,7 @@ class ContactListCubit extends DHTShortArrayCubit { if (c != null && c.localConversationRecordKey.toVeilid() == localConversationRecordKey) { - final newContact = c.deepCopy(); - - if (nickname != null) { - newContact.nickname = nickname; - } - if (notes != null) { - newContact.notes = notes; - } - if (showAvailability != null) { - newContact.showAvailability = showAvailability; - } + final newContact = await updatedContactSpec.updateProto(c); final updated = await writer.tryWriteItemProtobuf( proto.Contact.fromBuffer, pos, newContact); diff --git a/lib/contacts/models/contact_spec.dart b/lib/contacts/models/contact_spec.dart new file mode 100644 index 0000000..1596434 --- /dev/null +++ b/lib/contacts/models/contact_spec.dart @@ -0,0 +1,37 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:protobuf/protobuf.dart'; + +import '../../proto/proto.dart' as proto; + +@immutable +class ContactSpec extends Equatable { + const ContactSpec({ + required this.nickname, + required this.notes, + required this.showAvailability, + }); + + ContactSpec.fromProto(proto.Contact p) + : nickname = p.nickname, + notes = p.notes, + showAvailability = p.showAvailability; + + Future updateProto(proto.Contact old) async { + final newProto = old.deepCopy() + ..nickname = nickname + ..notes = notes + ..showAvailability = showAvailability; + + return newProto; + } + + //////////////////////////////////////////////////////////////////////////// + + final String nickname; + final String notes; + final bool showAvailability; + + @override + List get props => [nickname, notes, showAvailability]; +} diff --git a/lib/contacts/models/models.dart b/lib/contacts/models/models.dart new file mode 100644 index 0000000..d489632 --- /dev/null +++ b/lib/contacts/models/models.dart @@ -0,0 +1 @@ +export 'contact_spec.dart'; diff --git a/lib/contacts/views/availability_widget.dart b/lib/contacts/views/availability_widget.dart index cf3e51a..a79f774 100644 --- a/lib/contacts/views/availability_widget.dart +++ b/lib/contacts/views/availability_widget.dart @@ -10,11 +10,11 @@ class AvailabilityWidget extends StatelessWidget { {required this.availability, required this.color, this.vertical = true, - this.iconSize = 32, + this.iconSize = 24, super.key}); static Widget availabilityIcon(proto.Availability availability, Color color, - {double size = 32}) { + {double size = 24}) { late final Widget iconData; switch (availability) { case proto.Availability.AVAILABILITY_AWAY: @@ -70,7 +70,7 @@ class AvailabilityWidget extends StatelessWidget { ]) : Row(mainAxisSize: MainAxisSize.min, children: [ icon, - Text(name, style: textTheme.labelSmall!.copyWith(color: color)) + Text(name, style: textTheme.labelLarge!.copyWith(color: color)) .paddingLTRB(8, 0, 0, 0) ]); } diff --git a/lib/contacts/views/contact_details_widget.dart b/lib/contacts/views/contact_details_widget.dart index bd4376f..7b5416e 100644 --- a/lib/contacts/views/contact_details_widget.dart +++ b/lib/contacts/views/contact_details_widget.dart @@ -1,13 +1,15 @@ 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 '../../proto/proto.dart' as proto; +import '../../tools/tools.dart'; import '../contacts.dart'; class ContactDetailsWidget extends StatefulWidget { - const ContactDetailsWidget({required this.contact, super.key}); - final proto.Contact contact; + const ContactDetailsWidget( + {required this.contact, this.onModifiedState, super.key}); @override State createState() => _ContactDetailsWidgetState(); @@ -15,8 +17,14 @@ class ContactDetailsWidget extends StatefulWidget { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('contact', contact)); + properties + ..add(DiagnosticsProperty('contact', contact)) + ..add(ObjectFlagProperty.has( + 'onModifiedState', onModifiedState)); } + + final proto.Contact contact; + final void Function(bool)? onModifiedState; } class _ContactDetailsWidgetState extends State @@ -24,18 +32,21 @@ class _ContactDetailsWidgetState extends State @override Widget build(BuildContext context) => SingleChildScrollView( child: EditContactForm( - formKey: GlobalKey(), contact: widget.contact, - onSubmit: (fbs) async { + submitText: translate('button.update'), + submitDisabledText: translate('button.waiting_for_network'), + onModifiedState: widget.onModifiedState, + onSubmit: (updatedContactSpec) async { final contactList = context.read(); - await contactList.updateContactFields( - localConversationRecordKey: - widget.contact.localConversationRecordKey.toVeilid(), - nickname: fbs.currentState - ?.value[EditContactForm.formFieldNickname] as String, - notes: fbs.currentState?.value[EditContactForm.formFieldNotes] - as String, - showAvailability: fbs.currentState - ?.value[EditContactForm.formFieldShowAvailability] as bool); + try { + await contactList.updateContactFields( + localConversationRecordKey: + widget.contact.localConversationRecordKey.toVeilid(), + updatedContactSpec: updatedContactSpec); + } on Exception catch (e) { + log.debug('error updating contact: $e', e); + return false; + } + return true; })); } diff --git a/lib/contacts/views/contact_item_widget.dart b/lib/contacts/views/contact_item_widget.dart index 4cb874d..a0f2fbc 100644 --- a/lib/contacts/views/contact_item_widget.dart +++ b/lib/contacts/views/contact_item_widget.dart @@ -40,7 +40,7 @@ class ContactItemWidget extends StatelessWidget { size: 34, borderColor: _disabled ? scale.grayScale.primaryText - : scale.primaryScale.primaryText, + : scale.primaryScale.subtleBorder, foregroundColor: _disabled ? scale.grayScale.primaryText : scale.primaryScale.primaryText, @@ -71,7 +71,7 @@ class ContactItemWidget extends StatelessWidget { endActions: [ if (_onDoubleTap != null) SliderTileAction( - icon: Icons.edit, + //icon: Icons.edit, label: translate('button.edit'), actionScale: ScaleKind.secondary, onPressed: (_context) => @@ -81,7 +81,7 @@ class ContactItemWidget extends StatelessWidget { ), if (_onDelete != null) SliderTileAction( - icon: Icons.delete, + //icon: Icons.delete, label: translate('button.delete'), actionScale: ScaleKind.tertiary, onPressed: (_context) => diff --git a/lib/contacts/views/contacts_browser.dart b/lib/contacts/views/contacts_browser.dart index 89cea88..7040af5 100644 --- a/lib/contacts/views/contacts_browser.dart +++ b/lib/contacts/views/contacts_browser.dart @@ -74,13 +74,10 @@ class _ContactsBrowserState extends State final menuIconColor = scaleConfig.preferBorders ? scale.primaryScale.hoverBorder - : scale.primaryScale.borderText; + : scale.primaryScale.hoverBorder; final menuBackgroundColor = scaleConfig.preferBorders ? scale.primaryScale.elementBackground - : scale.primaryScale.border; - // final menuHoverColor = scaleConfig.preferBorders - // ? scale.primaryScale.hoverElementBackground - // : scale.primaryScale.hoverBorder; + : scale.primaryScale.elementBackground; final menuBorderColor = scale.primaryScale.hoverBorder; @@ -149,13 +146,12 @@ class _ContactsBrowserState extends State }, iconSize: 32, icon: const Icon(Icons.contact_page), - color: scale.primaryScale.hoverBorder, + color: menuIconColor, ), Text(translate('add_contact_sheet.create_invite'), maxLines: 2, textAlign: TextAlign.center, - style: textTheme.labelSmall! - .copyWith(color: scale.primaryScale.hoverBorder)) + style: textTheme.labelSmall!.copyWith(color: menuIconColor)) ]), StarMenu( items: receiveInviteMenuItems, @@ -171,13 +167,12 @@ class _ContactsBrowserState extends State icon: ImageIcon( const AssetImage('assets/images/handshake.png'), size: 32, - color: scale.primaryScale.hoverBorder, + color: menuIconColor, )), Text(translate('add_contact_sheet.receive_invite'), maxLines: 2, textAlign: TextAlign.center, - style: textTheme.labelSmall! - .copyWith(color: scale.primaryScale.hoverBorder)) + style: textTheme.labelSmall!.copyWith(color: menuIconColor)) ]), ), ]).paddingAll(16); @@ -274,8 +269,9 @@ class _ContactsBrowserState extends State case ContactsBrowserElementKind.invitation: final invitation = element.invitation!; return invitation.message - .toLowerCase() - .contains(lowerValue); + .toLowerCase() + .contains(lowerValue) || + invitation.recipient.toLowerCase().contains(lowerValue); } }).toList() }; diff --git a/lib/contacts/views/contacts_dialog.dart b/lib/contacts/views/contacts_dialog.dart index e6e5391..ec85df3 100644 --- a/lib/contacts/views/contacts_dialog.dart +++ b/lib/contacts/views/contacts_dialog.dart @@ -1,3 +1,4 @@ +import 'package:async_tools/async_tools.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -11,6 +12,8 @@ import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; import '../contacts.dart'; +const _kDoBackArrow = 'doBackArrow'; + class ContactsDialog extends StatefulWidget { const ContactsDialog._({required this.modalContext}); @@ -44,13 +47,8 @@ class _ContactsDialogState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); - // final textTheme = theme.textTheme; final scale = theme.extension()!; - final scaleConfig = theme.extension()!; - - final appBarIconColor = scaleConfig.useVisualIndicators - ? scale.secondaryScale.border - : scale.secondaryScale.borderText; + final appBarIconColor = scale.primaryScale.borderText; final enableSplit = !isMobileWidth(context); final enableLeft = enableSplit || _selectedContact == null; @@ -63,20 +61,22 @@ class _ContactsDialogState extends State { title: Text(!enableSplit && enableRight ? translate('contacts_dialog.edit_contact') : translate('contacts_dialog.contacts')), - leading: Navigator.canPop(context) - ? IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () { - if (!enableSplit && enableRight) { - setState(() { - _selectedContact = null; - }); - } else { + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + singleFuture((this, _kDoBackArrow), () async { + final confirmed = await _onContactSelected(null); + if (!enableSplit && enableRight) { + } else { + if (confirmed) { + if (context.mounted) { Navigator.pop(context); } - }, - ) - : null, + } + } + }); + }, + ), actions: [ if (_selectedContact != null) FittedBox( @@ -85,9 +85,10 @@ class _ContactsDialogState extends State { Column(mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: const Icon(Icons.chat_bubble), + color: appBarIconColor, tooltip: translate('contacts_dialog.new_chat'), onPressed: () async { - await onChatStarted(_selectedContact!); + await _onChatStarted(_selectedContact!); }), Text(translate('contacts_dialog.new_chat'), style: theme.textTheme.labelSmall! @@ -100,10 +101,11 @@ class _ContactsDialogState extends State { Column(mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: const Icon(Icons.close), + color: appBarIconColor, tooltip: translate('contacts_dialog.close_contact'), onPressed: () async { - await onContactSelected(null); + await _onContactSelected(null); }), Text(translate('contacts_dialog.close_contact'), style: theme.textTheme.labelSmall! @@ -115,41 +117,68 @@ class _ContactsDialogState extends State { return ColoredBox( color: scale.primaryScale.appBackground, - child: Row(children: [ - Offstage( - offstage: !enableLeft, - child: SizedBox( - width: enableLeft && !enableRight - ? maxWidth - : (maxWidth / 3).clamp(200, 500), - child: DecoratedBox( - decoration: BoxDecoration( - color: scale.primaryScale.subtleBackground), - child: ContactsBrowser( - selectedContactRecordKey: _selectedContact - ?.localConversationRecordKey - .toVeilid(), - onContactSelected: onContactSelected, - onChatStarted: onChatStarted, - ).paddingLTRB(8, 0, 8, 8)))), - if (enableRight) - if (_selectedContact == null) - const NoContactWidget().expanded() - else - ContactDetailsWidget(contact: _selectedContact!) - .paddingAll(8) - .expanded(), - ])); + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Offstage( + offstage: !enableLeft, + child: SizedBox( + width: enableLeft && !enableRight + ? maxWidth + : (maxWidth / 3).clamp(200, 500), + child: DecoratedBox( + decoration: BoxDecoration( + color: scale + .primaryScale.subtleBackground), + child: ContactsBrowser( + selectedContactRecordKey: _selectedContact + ?.localConversationRecordKey + .toVeilid(), + onContactSelected: _onContactSelected, + onChatStarted: _onChatStarted, + ).paddingLTRB(8, 0, 8, 8)))), + if (enableRight && enableLeft) + Container( + constraints: const BoxConstraints( + minWidth: 1, maxWidth: 1), + color: scale.primaryScale.subtleBorder), + if (enableRight) + if (_selectedContact == null) + const NoContactWidget().expanded() + else + ContactDetailsWidget( + contact: _selectedContact!, + onModifiedState: _onModifiedState) + .paddingLTRB(16, 16, 16, 16) + .expanded(), + ])); }))); } - Future onContactSelected(proto.Contact? contact) async { + void _onModifiedState(bool isModified) { setState(() { - _selectedContact = contact; + _isModified = isModified; }); } - Future onChatStarted(proto.Contact contact) async { + Future _onContactSelected(proto.Contact? contact) async { + if (contact != _selectedContact && _isModified) { + final ok = await showConfirmModal( + context: context, + title: translate('confirmation.discard_changes'), + text: translate('confirmation.are_you_sure_discard')); + if (!ok) { + return false; + } + } + setState(() { + _selectedContact = contact; + _isModified = false; + }); + return true; + } + + Future _onChatStarted(proto.Contact contact) async { final chatListCubit = context.read(); await chatListCubit.getOrCreateChatSingleContact(contact: contact); @@ -163,4 +192,5 @@ class _ContactsDialogState extends State { } proto.Contact? _selectedContact; + bool _isModified = false; } diff --git a/lib/contacts/views/edit_contact_form.dart b/lib/contacts/views/edit_contact_form.dart index 7803ab2..5477c60 100644 --- a/lib/contacts/views/edit_contact_form.dart +++ b/lib/contacts/views/edit_contact_form.dart @@ -1,3 +1,4 @@ +import 'package:async_tools/async_tools.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -6,13 +7,18 @@ import 'package:flutter_translate/flutter_translate.dart'; import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; +import '../models/contact_spec.dart'; import 'availability_widget.dart'; +const _kDoSubmitEditContact = 'doSubmitEditContact'; + class EditContactForm extends StatefulWidget { const EditContactForm({ - required this.formKey, required this.contact, - this.onSubmit, + required this.onSubmit, + required this.submitText, + required this.submitDisabledText, + this.onModifiedState, super.key, }); @@ -20,19 +26,22 @@ class EditContactForm extends StatefulWidget { State createState() => _EditContactFormState(); final proto.Contact contact; - final Future Function(GlobalKey)? onSubmit; - final GlobalKey formKey; + final String submitText; + final String submitDisabledText; + final Future Function(ContactSpec) onSubmit; + final void Function(bool)? onModifiedState; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties - ..add(ObjectFlagProperty< - Future Function( - GlobalKey p1)?>.has('onSubmit', onSubmit)) + ..add(ObjectFlagProperty Function(ContactSpec p1)>.has( + 'onSubmit', onSubmit)) + ..add(ObjectFlagProperty.has( + 'onModifiedState', onModifiedState)) ..add(DiagnosticsProperty('contact', contact)) - ..add( - DiagnosticsProperty>('formKey', formKey)); + ..add(StringProperty('submitText', submitText)) + ..add(StringProperty('submitDisabledText', submitDisabledText)); } static const String formFieldNickname = 'nickname'; @@ -41,16 +50,46 @@ class EditContactForm extends StatefulWidget { } class _EditContactFormState extends State { + final _formKey = GlobalKey(); + @override void initState() { + _savedValue = ContactSpec.fromProto(widget.contact); + _currentValueNickname = _savedValue.nickname; + super.initState(); } - Widget _availabilityWidget(proto.Availability availability, Color color) => - AvailabilityWidget(availability: availability, color: color); + ContactSpec _makeContactSpec() { + final nickname = _formKey.currentState! + .fields[EditContactForm.formFieldNickname]!.value as String; + final notes = _formKey + .currentState!.fields[EditContactForm.formFieldNotes]!.value as String; + final showAvailability = _formKey.currentState! + .fields[EditContactForm.formFieldShowAvailability]!.value as bool; - @override - Widget build(BuildContext context) { + return ContactSpec( + nickname: nickname, notes: notes, showAvailability: showAvailability); + } + + // Check if everything is the same and update state + void _onChanged() { + final currentValue = _makeContactSpec(); + _isModified = currentValue != _savedValue; + final onModifiedState = widget.onModifiedState; + if (onModifiedState != null) { + onModifiedState(_isModified); + } + } + + Widget _availabilityWidget(proto.Availability availability, Color color) => + AvailabilityWidget( + availability: availability, + color: color, + vertical: false, + ); + + Widget _editContactForm(BuildContext context) { final theme = Theme.of(context); final scale = theme.extension()!; final scaleConfig = theme.extension()!; @@ -60,75 +99,94 @@ class _EditContactFormState extends State { if (scaleConfig.useVisualIndicators && !scaleConfig.preferBorders) { border = scale.primaryScale.elementBackground; } else { - border = scale.primaryScale.border; + border = scale.primaryScale.subtleBorder; } return FormBuilder( - key: widget.formKey, + key: _formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + onChanged: _onChanged, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - AvatarWidget( - name: widget.contact.profile.name, - size: 128, - borderColor: border, - foregroundColor: scale.primaryScale.primaryText, - backgroundColor: scale.primaryScale.primary, - scaleConfig: scaleConfig, - textStyle: theme.textTheme.titleLarge!.copyWith(fontSize: 64), - ).paddingLTRB(0, 0, 0, 16), - SelectableText(widget.contact.profile.name, - style: textTheme.headlineMedium) - .noEditDecoratorLabel( - context, - translate('contact_form.form_name'), - scale: scale.secondaryScale, - ) - .paddingSymmetric(vertical: 4), - SelectableText(widget.contact.profile.pronouns, - style: textTheme.headlineSmall) - .noEditDecoratorLabel( - context, - translate('contact_form.form_pronouns'), - scale: scale.secondaryScale, - ) - .paddingSymmetric(vertical: 4), - Row(mainAxisSize: MainAxisSize.min, children: [ - _availabilityWidget(widget.contact.profile.availability, - scale.primaryScale.primaryText), - SelectableText(widget.contact.profile.status, - style: textTheme.bodyMedium) - .paddingSymmetric(horizontal: 8) - ]) - .noEditDecoratorLabel( - context, - translate('contact_form.form_status'), - scale: scale.secondaryScale, - ) - .paddingSymmetric(vertical: 4), - SelectableText(widget.contact.profile.about, - minLines: 1, maxLines: 8, style: textTheme.bodyMedium) - .noEditDecoratorLabel( - context, - translate('contact_form.form_about'), - scale: scale.secondaryScale, - ) - .paddingSymmetric(vertical: 4), - SelectableText( - widget.contact.identityPublicKey.value.toVeilid().toString(), - style: textTheme.labelMedium! - .copyWith(fontFamily: 'Source Code Pro')) - .noEditDecoratorLabel( - context, - translate('contact_form.form_fingerprint'), - scale: scale.secondaryScale, - ) - .paddingSymmetric(vertical: 4), - Divider(color: border).paddingLTRB(8, 0, 8, 8), + styledCard( + context: context, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row(children: [ + const Spacer(), + AvatarWidget( + name: _currentValueNickname.isNotEmpty + ? _currentValueNickname + : widget.contact.profile.name, + size: 128, + borderColor: border, + foregroundColor: scale.primaryScale.primaryText, + backgroundColor: scale.primaryScale.primary, + scaleConfig: scaleConfig, + textStyle: theme.textTheme.titleLarge! + .copyWith(fontSize: 64), + ).paddingLTRB(0, 0, 0, 16), + const Spacer() + ]), + SelectableText(widget.contact.profile.name, + style: textTheme.bodyLarge) + .noEditDecoratorLabel( + context, + translate('contact_form.form_name'), + ) + .paddingSymmetric(vertical: 4), + SelectableText(widget.contact.profile.pronouns, + style: textTheme.bodyLarge) + .noEditDecoratorLabel( + context, + translate('contact_form.form_pronouns'), + ) + .paddingSymmetric(vertical: 4), + Row(mainAxisSize: MainAxisSize.min, children: [ + _availabilityWidget( + widget.contact.profile.availability, + scale.primaryScale.appText), + SelectableText(widget.contact.profile.status, + style: textTheme.bodyMedium) + .paddingSymmetric(horizontal: 8) + ]) + .noEditDecoratorLabel( + context, + translate('contact_form.form_status'), + ) + .paddingSymmetric(vertical: 4), + SelectableText(widget.contact.profile.about, + minLines: 1, + maxLines: 8, + style: textTheme.bodyMedium) + .noEditDecoratorLabel( + context, + translate('contact_form.form_about'), + ) + .paddingSymmetric(vertical: 4), + SelectableText( + widget.contact.identityPublicKey.value + .toVeilid() + .toString(), + style: textTheme.bodyMedium! + .copyWith(fontFamily: 'Source Code Pro')) + .noEditDecoratorLabel( + context, + translate('contact_form.form_fingerprint'), + ) + .paddingSymmetric(vertical: 4), + ]).paddingAll(16)) + .paddingLTRB(0, 0, 0, 16), FormBuilderTextField( - //autofocus: true, name: EditContactForm.formFieldNickname, - initialValue: widget.contact.nickname, + initialValue: _currentValueNickname, + onChanged: (x) { + setState(() { + _currentValueNickname = x ?? ''; + }); + }, decoration: InputDecoration( labelText: translate('contact_form.form_nickname')), maxLength: 64, @@ -136,14 +194,16 @@ class _EditContactFormState extends State { ), FormBuilderCheckbox( name: EditContactForm.formFieldShowAvailability, - initialValue: widget.contact.showAvailability, + initialValue: _savedValue.showAvailability, side: BorderSide(color: scale.primaryScale.border, width: 2), + checkColor: scale.primaryScale.borderText, + activeColor: scale.primaryScale.border, title: Text(translate('contact_form.form_show_availability'), style: textTheme.labelMedium), ), FormBuilderTextField( name: EditContactForm.formFieldNotes, - initialValue: widget.contact.notes, + initialValue: _savedValue.notes, minLines: 1, maxLines: 8, maxLength: 1024, @@ -152,24 +212,38 @@ class _EditContactFormState extends State { textInputAction: TextInputAction.newline, ), ElevatedButton( - onPressed: widget.onSubmit == null - ? null - : () async { - if (widget.formKey.currentState?.saveAndValidate() ?? - false) { - await widget.onSubmit!(widget.formKey); - } - }, + onPressed: _isModified ? _doSubmit : null, child: Row(mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0), - Text((widget.onSubmit == null) - ? translate('contact_form.save') - : translate('contact_form.save')) - .paddingLTRB(0, 0, 4, 0) + Text(widget.submitText).paddingLTRB(0, 0, 4, 0) ]), - ).paddingSymmetric(vertical: 4).alignAtCenterRight(), + ).paddingSymmetric(vertical: 4).alignAtCenter(), ], ), ); } + + void _doSubmit() { + final onSubmit = widget.onSubmit; + if (_formKey.currentState?.saveAndValidate() ?? false) { + singleFuture((this, _kDoSubmitEditContact), () async { + final updatedContactSpec = _makeContactSpec(); + final saved = await onSubmit(updatedContactSpec); + if (saved) { + setState(() { + _savedValue = updatedContactSpec; + }); + _onChanged(); + } + }); + } + } + + @override + Widget build(BuildContext context) => _editContactForm(context); + + /////////////////////////////////////////////////////////////////////////// + late ContactSpec _savedValue; + late String _currentValueNickname; + bool _isModified = false; } diff --git a/lib/layout/home/drawer_menu/drawer_menu.dart b/lib/layout/home/drawer_menu/drawer_menu.dart index 0821bbb..b56d437 100644 --- a/lib/layout/home/drawer_menu/drawer_menu.dart +++ b/lib/layout/home/drawer_menu/drawer_menu.dart @@ -9,12 +9,13 @@ 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 '../../../theme/theme.dart'; import '../../../tools/tools.dart'; import '../../../veilid_processor/veilid_processor.dart'; import 'menu_item_widget.dart'; +const _scaleKind = ScaleKind.secondary; + class DrawerMenu extends StatefulWidget { const DrawerMenu({super.key}); @@ -40,7 +41,7 @@ class _DrawerMenuState extends State { } void _doEditClick(TypedKey superIdentityRecordKey, - proto.Account existingAccount, OwnedDHTRecordPointer accountRecord) { + AccountSpec existingAccount, OwnedDHTRecordPointer accountRecord) { singleFuture(this, () async { await GoRouterHelper(context).push('/edit_account', extra: [superIdentityRecordKey, existingAccount, accountRecord]); @@ -58,45 +59,6 @@ class _DrawerMenuState extends State { borderRadius: BorderRadius.circular(borderRadius))), child: child); - Widget _makeAvatarWidget({ - required String name, - required double size, - required Color borderColor, - required Color foregroundColor, - required Color backgroundColor, - required ScaleConfig scaleConfig, - required TextStyle textStyle, - ImageProvider? imageProvider, - }) { - final abbrev = name.split(' ').map((s) => s.isEmpty ? '' : s[0]).join(); - late final String shortname; - if (abbrev.length >= 3) { - shortname = abbrev[0] + abbrev[1] + abbrev[abbrev.length - 1]; - } else { - shortname = abbrev; - } - - return Container( - height: size, - width: size, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: scaleConfig.preferBorders - ? Border.all( - color: borderColor, - width: 2 * (size ~/ 32 + 1), - strokeAlign: BorderSide.strokeAlignOutside) - : null, - color: Colors.blue, - ), - child: AvatarImage( - //size: 32, - backgroundImage: imageProvider, - backgroundColor: backgroundColor, - foregroundColor: foregroundColor, - child: Text(shortname, style: textStyle))); - } - Widget _makeAccountWidget( {required String name, required bool selected, @@ -173,6 +135,7 @@ class _DrawerMenuState extends State { footerButtonIconColor: border, footerButtonIconHoverColor: hoverBackground, footerButtonIconFocusColor: activeBackground, + minHeight: 48, )); } @@ -184,6 +147,7 @@ class _DrawerMenuState extends State { final theme = Theme.of(context); final scaleScheme = theme.extension()!; final scaleConfig = theme.extension()!; + final scale = scaleScheme.scale(_scaleKind); final loggedInAccounts = []; final loggedOutAccounts = []; @@ -197,9 +161,6 @@ class _DrawerMenuState extends State { final avAccountRecordState = perAccountState?.avAccountRecordState; if (perAccountState != null && avAccountRecordState != null) { // Account is logged in - final scale = scaleConfig.useVisualIndicators - ? theme.extension()!.primaryScale - : theme.extension()!.tertiaryScale; final loggedInAccount = avAccountRecordState.when( data: (value) => _makeAccountWidget( name: value.profile.name, @@ -213,7 +174,7 @@ class _DrawerMenuState extends State { footerCallback: () { _doEditClick( superIdentityRecordKey, - value, + AccountSpec.fromProto(value), perAccountState.accountInfo.userLogin!.accountRecordInfo .accountRecord); }), @@ -311,13 +272,14 @@ class _DrawerMenuState extends State { Widget _getBottomButtons() { final theme = Theme.of(context); - final scale = theme.extension()!; + final scaleScheme = theme.extension()!; final scaleConfig = theme.extension()!; + final scale = scaleScheme.scale(_scaleKind); final settingsButton = _getButton( icon: const Icon(Icons.settings), tooltip: translate('menu.settings_tooltip'), - scale: scale.tertiaryScale, + scale: scale, scaleConfig: scaleConfig, onPressed: () async { await GoRouterHelper(context).push('/settings'); @@ -326,7 +288,7 @@ class _DrawerMenuState extends State { final addButton = _getButton( icon: const Icon(Icons.add), tooltip: translate('menu.add_account_tooltip'), - scale: scale.tertiaryScale, + scale: scale, scaleConfig: scaleConfig, onPressed: () async { await GoRouterHelper(context).push('/new_account'); @@ -340,8 +302,9 @@ class _DrawerMenuState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final scale = theme.extension()!; + final scaleScheme = theme.extension()!; final scaleConfig = theme.extension()!; + final scale = scaleScheme.scale(_scaleKind); //final textTheme = theme.textTheme; final localAccounts = context.watch().state; final perAccountCollectionBlocMapState = @@ -351,8 +314,8 @@ class _DrawerMenuState extends State { begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ - scale.tertiaryScale.border, - scale.tertiaryScale.subtleBorder, + scale.border, + scale.subtleBorder, ]); return DecoratedBox( @@ -360,34 +323,35 @@ class _DrawerMenuState extends State { shadows: [ if (scaleConfig.useVisualIndicators && !scaleConfig.preferBorders) BoxShadow( - color: scale.tertiaryScale.primary.darken(80), + color: scale.primary.darken(60), spreadRadius: 2, ) else if (scaleConfig.useVisualIndicators && scaleConfig.preferBorders) BoxShadow( - color: scale.tertiaryScale.border, + color: scale.border, spreadRadius: 2, ) else BoxShadow( - color: scale.tertiaryScale.primary.darken(40), - blurRadius: 6, + color: scale.appBackground.darken(60).withAlpha(0x3F), + blurRadius: 16, + spreadRadius: 2, offset: const Offset( 0, - 4, + 2, ), ), ], gradient: scaleConfig.useVisualIndicators ? null : gradient, color: scaleConfig.useVisualIndicators ? (scaleConfig.preferBorders - ? scale.tertiaryScale.appBackground - : scale.tertiaryScale.subtleBorder) + ? scale.appBackground + : scale.subtleBorder) : null, shape: RoundedRectangleBorder( side: scaleConfig.preferBorders - ? BorderSide(color: scale.tertiaryScale.primary, width: 2) + ? BorderSide(color: scale.primary, width: 2) : BorderSide.none, borderRadius: BorderRadius.only( topRight: Radius.circular(16 * scaleConfig.borderRadiusScale), @@ -399,31 +363,31 @@ class _DrawerMenuState extends State { child: ColorFiltered( colorFilter: ColorFilter.mode( theme.brightness == Brightness.light - ? scale.tertiaryScale.primary - : scale.tertiaryScale.border, + ? scale.primary + : scale.border, scaleConfig.preferBorders ? BlendMode.modulate : BlendMode.dst), child: Row(children: [ - SvgPicture.asset( - height: 48, - 'assets/images/icon.svg', - colorFilter: scaleConfig.useVisualIndicators - ? grayColorFilter - : null) - .paddingLTRB(0, 0, 16, 0), + // SvgPicture.asset( + // height: 48, + // 'assets/images/icon.svg', + // colorFilter: scaleConfig.useVisualIndicators + // ? grayColorFilter + // : null) + // .paddingLTRB(0, 0, 16, 0), SvgPicture.asset( height: 48, 'assets/images/title.svg', colorFilter: scaleConfig.useVisualIndicators ? grayColorFilter - : null), + : dodgeFilter), ]))), Text(translate('menu.accounts'), style: theme.textTheme.titleMedium!.copyWith( color: scaleConfig.preferBorders - ? scale.tertiaryScale.border - : scale.tertiaryScale.borderText)) + ? scale.border + : scale.borderText)) .paddingLTRB(0, 16, 0, 16), ListView( shrinkWrap: true, @@ -438,16 +402,16 @@ class _DrawerMenuState extends State { Text('${translate('menu.version')} $packageInfoVersion', style: theme.textTheme.labelMedium!.copyWith( color: scaleConfig.preferBorders - ? scale.tertiaryScale.hoverBorder - : scale.tertiaryScale.subtleBackground)), + ? scale.hoverBorder + : scale.subtleBackground)), const Spacer(), SignalStrengthMeterWidget( color: scaleConfig.preferBorders - ? scale.tertiaryScale.hoverBorder - : scale.tertiaryScale.subtleBackground, + ? scale.hoverBorder + : scale.subtleBackground, inactiveColor: scaleConfig.preferBorders - ? scale.tertiaryScale.border - : scale.tertiaryScale.elementBackground, + ? scale.border + : scale.elementBackground, ), ]) ]).paddingAll(16), diff --git a/lib/layout/home/drawer_menu/menu_item_widget.dart b/lib/layout/home/drawer_menu/menu_item_widget.dart index 8529411..a786010 100644 --- a/lib/layout/home/drawer_menu/menu_item_widget.dart +++ b/lib/layout/home/drawer_menu/menu_item_widget.dart @@ -22,39 +22,42 @@ class MenuItemWidget extends StatelessWidget { this.footerButtonIconHoverColor, this.footerButtonIconFocusColor, this.footerCallback, + this.minHeight = 0, super.key, }); @override Widget build(BuildContext context) => TextButton( - onPressed: callback, - style: TextButton.styleFrom(foregroundColor: foregroundColor).copyWith( - backgroundColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.hovered)) { - return backgroundHoverColor; - } - if (states.contains(WidgetState.focused)) { - return backgroundFocusColor; - } - return backgroundColor; - }), - side: WidgetStateBorderSide.resolveWith((states) { - if (states.contains(WidgetState.hovered)) { - return borderColor != null - ? BorderSide(width: 2, color: borderHoverColor!) - : null; - } - if (states.contains(WidgetState.focused)) { - return borderColor != null - ? BorderSide(width: 2, color: borderFocusColor!) - : null; - } + onPressed: callback, + style: TextButton.styleFrom(foregroundColor: foregroundColor).copyWith( + backgroundColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.hovered)) { + return backgroundHoverColor; + } + if (states.contains(WidgetState.focused)) { + return backgroundFocusColor; + } + return backgroundColor; + }), + side: WidgetStateBorderSide.resolveWith((states) { + if (states.contains(WidgetState.hovered)) { return borderColor != null - ? BorderSide(width: 2, color: borderColor!) + ? BorderSide(width: 2, color: borderHoverColor!) : null; - }), - shape: WidgetStateProperty.all(RoundedRectangleBorder( - borderRadius: BorderRadius.circular(borderRadius ?? 0)))), + } + if (states.contains(WidgetState.focused)) { + return borderColor != null + ? BorderSide(width: 2, color: borderFocusColor!) + : null; + } + return borderColor != null + ? BorderSide(width: 2, color: borderColor!) + : null; + }), + shape: WidgetStateProperty.all(RoundedRectangleBorder( + borderRadius: BorderRadius.circular(borderRadius ?? 0)))), + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: minHeight), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -81,7 +84,7 @@ class MenuItemWidget extends StatelessWidget { onPressed: footerCallback), ], ).paddingAll(2), - ); + )); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -106,7 +109,8 @@ class MenuItemWidget extends StatelessWidget { ..add(ColorProperty('borderColor', borderColor)) ..add(DoubleProperty('borderRadius', borderRadius)) ..add(ColorProperty('borderHoverColor', borderHoverColor)) - ..add(ColorProperty('borderFocusColor', borderFocusColor)); + ..add(ColorProperty('borderFocusColor', borderFocusColor)) + ..add(DoubleProperty('minHeight', minHeight)); } //////////////////////////////////////////////////////////////////////////// @@ -129,4 +133,5 @@ class MenuItemWidget extends StatelessWidget { final Color? footerButtonIconColor; final Color? footerButtonIconHoverColor; final Color? footerButtonIconFocusColor; + final double minHeight; } diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index f226717..3e8e98b 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -96,7 +96,7 @@ class HomeScreenState extends State ), Row(mainAxisSize: MainAxisSize.min, children: [ StatefulBuilder( - builder: (context, setState) => Checkbox.adaptive( + builder: (context, setState) => Checkbox( value: displayBetaWarning, onChanged: (value) { setState(() { @@ -213,7 +213,6 @@ class HomeScreenState extends State style: theme.textTheme.bodySmall!, child: ZoomDrawer( controller: _zoomDrawerController, - //menuBackgroundColor: Colors.transparent, menuScreen: Builder(builder: (context) { final zoomDrawer = ZoomDrawer.of(context); zoomDrawer!.stateNotifier.addListener(() { @@ -228,7 +227,7 @@ class HomeScreenState extends State child: Builder(builder: _buildAccountPageView)), borderRadius: 0, angle: 0, - mainScreenOverlayColor: theme.shadowColor.withAlpha(0x2F), + //mainScreenOverlayColor: theme.shadowColor.withAlpha(0x2F), openCurve: Curves.fastEaseInToSlowEaseOut, // duration: const Duration(milliseconds: 250), // reverseDuration: const Duration(milliseconds: 250), diff --git a/lib/notifications/views/notifications_preferences.dart b/lib/notifications/views/notifications_preferences.dart index 5967522..95d4a1e 100644 --- a/lib/notifications/views/notifications_preferences.dart +++ b/lib/notifications/views/notifications_preferences.dart @@ -130,6 +130,8 @@ Widget buildSettingsPageNotificationPreferences( FormBuilderCheckbox( name: formFieldDisplayBetaWarning, side: BorderSide(color: scale.primaryScale.border, width: 2), + checkColor: scale.primaryScale.borderText, + activeColor: scale.primaryScale.border, title: Text(translate('settings_page.display_beta_warning'), style: textTheme.labelMedium), initialValue: notificationsPreference.displayBetaWarning, @@ -146,6 +148,8 @@ Widget buildSettingsPageNotificationPreferences( FormBuilderCheckbox( name: formFieldEnableBadge, side: BorderSide(color: scale.primaryScale.border, width: 2), + checkColor: scale.primaryScale.borderText, + activeColor: scale.primaryScale.border, title: Text(translate('settings_page.enable_badge'), style: textTheme.labelMedium), initialValue: notificationsPreference.enableBadge, @@ -161,6 +165,8 @@ Widget buildSettingsPageNotificationPreferences( FormBuilderCheckbox( name: formFieldEnableNotifications, side: BorderSide(color: scale.primaryScale.border, width: 2), + checkColor: scale.primaryScale.borderText, + activeColor: scale.primaryScale.border, title: Text(translate('settings_page.enable_notifications'), style: textTheme.labelMedium), initialValue: notificationsPreference.enableNotifications, diff --git a/lib/proto/veilidchat.pb.dart b/lib/proto/veilidchat.pb.dart index 3947baf..245f9f3 100644 --- a/lib/proto/veilidchat.pb.dart +++ b/lib/proto/veilidchat.pb.dart @@ -3024,6 +3024,7 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { $fixnum.Int64? expiration, $core.List<$core.int>? invitation, $core.String? message, + $core.String? recipient, }) { final $result = create(); if (contactRequestInbox != null) { @@ -3047,6 +3048,9 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { if (message != null) { $result.message = message; } + if (recipient != null) { + $result.recipient = recipient; + } return $result; } ContactInvitationRecord._() : super(); @@ -3061,6 +3065,7 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { ..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') + ..aOS(8, _omitFieldNames ? '' : 'recipient') ..hasRequiredFields = false ; @@ -3162,6 +3167,16 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { $core.bool hasMessage() => $_has(6); @$pb.TagNumber(7) void clearMessage() => clearField(7); + + /// The recipient sent along with the invitation + @$pb.TagNumber(8) + $core.String get recipient => $_getSZ(7); + @$pb.TagNumber(8) + set recipient($core.String v) { $_setString(7, v); } + @$pb.TagNumber(8) + $core.bool hasRecipient() => $_has(7); + @$pb.TagNumber(8) + void clearRecipient() => clearField(8); } diff --git a/lib/proto/veilidchat.pbjson.dart b/lib/proto/veilidchat.pbjson.dart index e102d40..81bf741 100644 --- a/lib/proto/veilidchat.pbjson.dart +++ b/lib/proto/veilidchat.pbjson.dart @@ -628,6 +628,7 @@ const ContactInvitationRecord$json = { {'1': 'expiration', '3': 5, '4': 1, '5': 4, '10': 'expiration'}, {'1': 'invitation', '3': 6, '4': 1, '5': 12, '10': 'invitation'}, {'1': 'message', '3': 7, '4': 1, '5': 9, '10': 'message'}, + {'1': 'recipient', '3': 8, '4': 1, '5': 9, '10': 'recipient'}, ], }; @@ -639,5 +640,6 @@ final $typed_data.Uint8List contactInvitationRecordDescriptor = $convert.base64D 'NlY3JldBgDIAEoCzIRLnZlaWxpZC5DcnlwdG9LZXlSDHdyaXRlclNlY3JldBJTCh1sb2NhbF9j' 'b252ZXJzYXRpb25fcmVjb3JkX2tleRgEIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIabG9jYWxDb2' '52ZXJzYXRpb25SZWNvcmRLZXkSHgoKZXhwaXJhdGlvbhgFIAEoBFIKZXhwaXJhdGlvbhIeCgpp' - 'bnZpdGF0aW9uGAYgASgMUgppbnZpdGF0aW9uEhgKB21lc3NhZ2UYByABKAlSB21lc3NhZ2U='); + 'bnZpdGF0aW9uGAYgASgMUgppbnZpdGF0aW9uEhgKB21lc3NhZ2UYByABKAlSB21lc3NhZ2USHA' + 'oJcmVjaXBpZW50GAggASgJUglyZWNpcGllbnQ='); diff --git a/lib/proto/veilidchat.proto b/lib/proto/veilidchat.proto index 0d4ca0a..e669959 100644 --- a/lib/proto/veilidchat.proto +++ b/lib/proto/veilidchat.proto @@ -478,4 +478,6 @@ message ContactInvitationRecord { bytes invitation = 6; // The message sent along with the invitation string message = 7; + // The recipient sent along with the invitation + string recipient = 8; } \ No newline at end of file diff --git a/lib/router/cubits/router_cubit.dart b/lib/router/cubits/router_cubit.dart index 974319a..48bf95f 100644 --- a/lib/router/cubits/router_cubit.dart +++ b/lib/router/cubits/router_cubit.dart @@ -11,7 +11,6 @@ import 'package:veilid_support/veilid_support.dart'; import '../../../account_manager/account_manager.dart'; import '../../layout/layout.dart'; -import '../../proto/proto.dart' as proto; import '../../settings/settings.dart'; import '../../tools/tools.dart'; import '../../veilid_processor/views/developer.dart'; @@ -43,10 +42,8 @@ class RouterCubit extends Cubit { case AccountRepositoryChange.localAccounts: emit(state.copyWith( hasAnyAccount: accountRepository.getLocalAccounts().isNotEmpty)); - break; case AccountRepositoryChange.userLogins: case AccountRepositoryChange.activeLocalAccount: - break; } }); } @@ -72,7 +69,7 @@ class RouterCubit extends Cubit { final extra = state.extra! as List; return EditAccountPage( superIdentityRecordKey: extra[0]! as TypedKey, - existingAccount: extra[1]! as proto.Account, + initialValue: extra[1]! as AccountSpec, accountRecord: extra[2]! as OwnedDHTRecordPointer, ); }, diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart index 94606aa..05ba514 100644 --- a/lib/settings/settings_page.dart +++ b/lib/settings/settings_page.dart @@ -45,6 +45,7 @@ class SettingsPageState extends State { child: FormBuilder( key: _formKey, child: ListView( + padding: const EdgeInsets.all(8), children: [ buildSettingsPageColorPreferences( context: context, @@ -56,6 +57,6 @@ class SettingsPageState extends State { context: context, onChanged: () => setState(() {})), ].map((x) => x.paddingLTRB(0, 0, 0, 8)).toList(), ), - ).paddingSymmetric(horizontal: 24, vertical: 16), + ).paddingSymmetric(horizontal: 8, vertical: 8), ))); } diff --git a/lib/theme/models/chat_theme.dart b/lib/theme/models/chat_theme.dart index a7039e5..cd0b9ce 100644 --- a/lib/theme/models/chat_theme.dart +++ b/lib/theme/models/chat_theme.dart @@ -14,7 +14,7 @@ ChatTheme makeChatTheme( secondaryColor: scaleConfig.preferBorders ? scale.secondaryScale.calloutText : scale.secondaryScale.calloutBackground, - backgroundColor: scale.grayScale.appBackground, + backgroundColor: scale.grayScale.appBackground.withAlpha(192), messageBorderRadius: scaleConfig.borderRadiusScale * 16, bubbleBorderSide: scaleConfig.preferBorders ? BorderSide( diff --git a/lib/theme/models/contrast_generator.dart b/lib/theme/models/contrast_generator.dart index b71ebea..861b052 100644 --- a/lib/theme/models/contrast_generator.dart +++ b/lib/theme/models/contrast_generator.dart @@ -1,9 +1,6 @@ import 'package:flutter/material.dart'; import 'radix_generator.dart'; -import 'scale_theme/scale_color.dart'; -import 'scale_theme/scale_input_decorator_theme.dart'; -import 'scale_theme/scale_scheme.dart'; import 'scale_theme/scale_theme.dart'; ScaleColor _contrastScaleColor( @@ -29,6 +26,7 @@ ScaleColor _contrastScaleColor( primaryText: front, borderText: back, dialogBorder: front, + dialogBorderText: back, calloutBackground: front, calloutText: back, ); @@ -246,7 +244,7 @@ ThemeData contrastGenerator({ TextTheme? customTextTheme, }) { final textTheme = customTextTheme ?? makeRadixTextTheme(brightness); - final scaleScheme = _contrastScaleScheme( + final scheme = _contrastScaleScheme( brightness: brightness, primaryFront: primaryFront, primaryBack: primaryBack, @@ -259,55 +257,51 @@ ThemeData contrastGenerator({ errorFront: errorFront, errorBack: errorBack, ); - final colorScheme = scaleScheme.toColorScheme( - brightness, - ); - final scaleTheme = ScaleTheme( - textTheme: textTheme, scheme: scaleScheme, config: scaleConfig); - final baseThemeData = ThemeData.from( - colorScheme: colorScheme, textTheme: textTheme, useMaterial3: true); + final scaleTheme = + ScaleTheme(textTheme: textTheme, scheme: scheme, config: scaleConfig); + + final baseThemeData = scaleTheme.toThemeData(brightness); + + final elevatedButtonTheme = ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: scheme.primaryScale.elementBackground, + foregroundColor: scheme.primaryScale.appText, + disabledBackgroundColor: + scheme.grayScale.elementBackground.withAlpha(0x7F), + disabledForegroundColor: scheme.grayScale.appText.withAlpha(0x7F), + shape: RoundedRectangleBorder( + side: BorderSide(color: scheme.primaryScale.border), + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale))) + .copyWith(side: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.disabled)) { + return BorderSide(color: scheme.grayScale.border.withAlpha(0x7F)); + } else if (states.contains(WidgetState.pressed)) { + return BorderSide( + color: scheme.primaryScale.border, + strokeAlign: BorderSide.strokeAlignOutside); + } else if (states.contains(WidgetState.hovered)) { + return BorderSide(color: scheme.primaryScale.hoverBorder); + } else if (states.contains(WidgetState.focused)) { + return BorderSide(color: scheme.primaryScale.hoverBorder, width: 2); + } + return BorderSide(color: scheme.primaryScale.border); + }))); + final themeData = baseThemeData.copyWith( - appBarTheme: baseThemeData.appBarTheme.copyWith( - backgroundColor: scaleScheme.primaryScale.border, - foregroundColor: scaleScheme.primaryScale.borderText), - bottomSheetTheme: baseThemeData.bottomSheetTheme.copyWith( - elevation: 0, - modalElevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16 * scaleConfig.borderRadiusScale), - topRight: - Radius.circular(16 * scaleConfig.borderRadiusScale)))), - canvasColor: scaleScheme.primaryScale.subtleBackground, - chipTheme: baseThemeData.chipTheme.copyWith( - backgroundColor: scaleScheme.primaryScale.elementBackground, - selectedColor: scaleScheme.primaryScale.activeElementBackground, - surfaceTintColor: scaleScheme.primaryScale.hoverElementBackground, - checkmarkColor: scaleScheme.primaryScale.border, - side: BorderSide(color: scaleScheme.primaryScale.border)), - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - backgroundColor: scaleScheme.primaryScale.elementBackground, - foregroundColor: scaleScheme.primaryScale.appText, - disabledBackgroundColor: scaleScheme.grayScale.elementBackground, - disabledForegroundColor: scaleScheme.grayScale.appText, - shape: RoundedRectangleBorder( - side: BorderSide(color: scaleScheme.primaryScale.border), - borderRadius: - BorderRadius.circular(8 * scaleConfig.borderRadiusScale))), - ), + // chipTheme: baseThemeData.chipTheme.copyWith( + // backgroundColor: scaleScheme.primaryScale.elementBackground, + // selectedColor: scaleScheme.primaryScale.activeElementBackground, + // surfaceTintColor: scaleScheme.primaryScale.hoverElementBackground, + // checkmarkColor: scaleScheme.primaryScale.border, + // side: BorderSide(color: scaleScheme.primaryScale.border)), + elevatedButtonTheme: elevatedButtonTheme, textSelectionTheme: TextSelectionThemeData( - cursorColor: scaleScheme.primaryScale.appText, - selectionColor: scaleScheme.primaryScale.appText.withAlpha(0x7F), - selectionHandleColor: scaleScheme.primaryScale.appText), - inputDecorationTheme: - ScaleInputDecoratorTheme(scaleScheme, scaleConfig, textTheme), - extensions: >[ - scaleScheme, - scaleConfig, - scaleTheme - ]); + cursorColor: scheme.primaryScale.appText, + selectionColor: scheme.primaryScale.appText.withAlpha(0x7F), + selectionHandleColor: scheme.primaryScale.appText), + extensions: >[scheme, scaleConfig, scaleTheme]); return themeData; } diff --git a/lib/theme/models/radix_generator.dart b/lib/theme/models/radix_generator.dart index 3dac7bc..a5c5f87 100644 --- a/lib/theme/models/radix_generator.dart +++ b/lib/theme/models/radix_generator.dart @@ -5,9 +5,6 @@ import 'package:flutter/material.dart'; import 'package:radix_colors/radix_colors.dart'; import '../../tools/tools.dart'; -import 'scale_theme/scale_color.dart'; -import 'scale_theme/scale_input_decorator_theme.dart'; -import 'scale_theme/scale_scheme.dart'; import 'scale_theme/scale_theme.dart'; enum RadixThemeColor { @@ -291,6 +288,7 @@ extension ToScaleColor on RadixColor { primaryText: scaleExtra.foregroundText, borderText: step12, dialogBorder: step9, + dialogBorderText: scaleExtra.foregroundText, calloutBackground: step9, calloutText: scaleExtra.foregroundText, ); @@ -609,7 +607,6 @@ ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) { final textTheme = makeRadixTextTheme(brightness); final radix = _radixScheme(brightness, themeColor); final scaleScheme = radix.toScale(); - final colorScheme = scaleScheme.toColorScheme(brightness); final scaleConfig = ScaleConfig( useVisualIndicators: false, preferBorders: false, @@ -619,68 +616,7 @@ ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) { final scaleTheme = ScaleTheme( textTheme: textTheme, scheme: scaleScheme, config: scaleConfig); - final baseThemeData = ThemeData.from( - colorScheme: colorScheme, textTheme: textTheme, useMaterial3: true); - - final themeData = baseThemeData.copyWith( - scrollbarTheme: baseThemeData.scrollbarTheme.copyWith( - thumbColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.pressed)) { - return scaleScheme.primaryScale.border; - } else if (states.contains(WidgetState.hovered)) { - return scaleScheme.primaryScale.hoverBorder; - } - return scaleScheme.primaryScale.subtleBorder; - }), trackColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.pressed)) { - return scaleScheme.primaryScale.activeElementBackground; - } else if (states.contains(WidgetState.hovered)) { - return scaleScheme.primaryScale.hoverElementBackground; - } - return scaleScheme.primaryScale.elementBackground; - }), trackBorderColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.pressed)) { - return scaleScheme.primaryScale.subtleBorder; - } else if (states.contains(WidgetState.hovered)) { - return scaleScheme.primaryScale.subtleBorder; - } - return scaleScheme.primaryScale.subtleBorder; - })), - appBarTheme: baseThemeData.appBarTheme.copyWith( - backgroundColor: scaleScheme.primaryScale.border, - foregroundColor: scaleScheme.primaryScale.borderText), - bottomSheetTheme: baseThemeData.bottomSheetTheme.copyWith( - elevation: 0, - modalElevation: 0, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16)))), - canvasColor: scaleScheme.primaryScale.subtleBackground, - chipTheme: baseThemeData.chipTheme.copyWith( - backgroundColor: scaleScheme.primaryScale.elementBackground, - selectedColor: scaleScheme.primaryScale.activeElementBackground, - surfaceTintColor: scaleScheme.primaryScale.hoverElementBackground, - checkmarkColor: scaleScheme.primaryScale.primary, - side: BorderSide(color: scaleScheme.primaryScale.border)), - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - backgroundColor: scaleScheme.primaryScale.elementBackground, - foregroundColor: scaleScheme.primaryScale.primary, - disabledBackgroundColor: scaleScheme.grayScale.elementBackground, - disabledForegroundColor: scaleScheme.grayScale.primary, - shape: RoundedRectangleBorder( - side: BorderSide(color: scaleScheme.primaryScale.border), - borderRadius: - BorderRadius.circular(8 * scaleConfig.borderRadiusScale))), - ), - inputDecorationTheme: - ScaleInputDecoratorTheme(scaleScheme, scaleConfig, textTheme), - extensions: >[ - scaleScheme, - scaleConfig, - scaleTheme - ]); + final themeData = scaleTheme.toThemeData(brightness); return themeData; } diff --git a/lib/theme/models/scale_theme/scale_color.dart b/lib/theme/models/scale_theme/scale_color.dart index 244f6a3..e50a01b 100644 --- a/lib/theme/models/scale_theme/scale_color.dart +++ b/lib/theme/models/scale_theme/scale_color.dart @@ -17,6 +17,7 @@ class ScaleColor { required this.primaryText, required this.borderText, required this.dialogBorder, + required this.dialogBorderText, required this.calloutBackground, required this.calloutText, }); @@ -36,6 +37,7 @@ class ScaleColor { Color primaryText; Color borderText; Color dialogBorder; + Color dialogBorderText; Color calloutBackground; Color calloutText; @@ -55,6 +57,7 @@ class ScaleColor { Color? foregroundText, Color? borderText, Color? dialogBorder, + Color? dialogBorderText, Color? calloutBackground, Color? calloutText, }) => @@ -76,6 +79,7 @@ class ScaleColor { primaryText: foregroundText ?? this.primaryText, borderText: borderText ?? this.borderText, dialogBorder: dialogBorder ?? this.dialogBorder, + dialogBorderText: dialogBorderText ?? this.dialogBorderText, calloutBackground: calloutBackground ?? this.calloutBackground, calloutText: calloutText ?? this.calloutText); @@ -112,6 +116,9 @@ class ScaleColor { const Color(0x00000000), dialogBorder: Color.lerp(a.dialogBorder, b.dialogBorder, t) ?? const Color(0x00000000), + dialogBorderText: + Color.lerp(a.dialogBorderText, b.dialogBorderText, t) ?? + const Color(0x00000000), calloutBackground: Color.lerp(a.calloutBackground, b.calloutBackground, t) ?? const Color(0x00000000), diff --git a/lib/theme/models/scale_theme/scale_custom_dropdown_theme.dart b/lib/theme/models/scale_theme/scale_custom_dropdown_theme.dart index 94764a5..692ec85 100644 --- a/lib/theme/models/scale_theme/scale_custom_dropdown_theme.dart +++ b/lib/theme/models/scale_theme/scale_custom_dropdown_theme.dart @@ -1,7 +1,6 @@ import 'package:animated_custom_dropdown/custom_dropdown.dart'; import 'package:flutter/material.dart'; -import 'scale_scheme.dart'; import 'scale_theme.dart'; class ScaleCustomDropdownTheme { diff --git a/lib/theme/models/scale_theme/scale_input_decorator_theme.dart b/lib/theme/models/scale_theme/scale_input_decorator_theme.dart index 3af1f15..1fb26a4 100644 --- a/lib/theme/models/scale_theme/scale_input_decorator_theme.dart +++ b/lib/theme/models/scale_theme/scale_input_decorator_theme.dart @@ -1,36 +1,61 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'scale_scheme.dart'; import 'scale_theme.dart'; class ScaleInputDecoratorTheme extends InputDecorationTheme { ScaleInputDecoratorTheme( this._scaleScheme, ScaleConfig scaleConfig, this._textTheme) - : super( - border: OutlineInputBorder( - borderSide: BorderSide(color: _scaleScheme.primaryScale.border), - borderRadius: - BorderRadius.circular(8 * scaleConfig.borderRadiusScale)), - contentPadding: const EdgeInsets.all(8), - labelStyle: TextStyle( - color: _scaleScheme.primaryScale.subtleText.withAlpha(127)), - floatingLabelStyle: - TextStyle(color: _scaleScheme.primaryScale.subtleText), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: _scaleScheme.primaryScale.hoverBorder, width: 2), - borderRadius: - BorderRadius.circular(8 * scaleConfig.borderRadiusScale))); + : hintAlpha = scaleConfig.preferBorders ? 127 : 255, + super( + contentPadding: const EdgeInsets.all(8), + labelStyle: TextStyle(color: _scaleScheme.primaryScale.subtleText), + floatingLabelStyle: + TextStyle(color: _scaleScheme.primaryScale.subtleText), + border: OutlineInputBorder( + borderSide: BorderSide(color: _scaleScheme.primaryScale.border), + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale)), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: _scaleScheme.primaryScale.border), + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale)), + disabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: _scaleScheme.grayScale.border.withAlpha(0x7F)), + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale)), + errorBorder: OutlineInputBorder( + borderSide: BorderSide(color: _scaleScheme.errorScale.border), + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale)), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: _scaleScheme.primaryScale.hoverBorder, width: 2), + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale)), + hoverColor: + _scaleScheme.primaryScale.hoverElementBackground.withAlpha(0x7F), + filled: true, + focusedErrorBorder: OutlineInputBorder( + borderSide: + BorderSide(color: _scaleScheme.errorScale.border, width: 2), + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale)), + ); final ScaleScheme _scaleScheme; final TextTheme _textTheme; + final int hintAlpha; + final int disabledAlpha = 127; @override TextStyle? get hintStyle => WidgetStateTextStyle.resolveWith((states) { if (states.contains(WidgetState.disabled)) { return TextStyle(color: _scaleScheme.grayScale.border); } - return TextStyle(color: _scaleScheme.primaryScale.border); + return TextStyle( + color: _scaleScheme.primaryScale.border.withAlpha(hintAlpha)); }); @override @@ -46,7 +71,7 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme { WidgetStateBorderSide.resolveWith((states) { if (states.contains(WidgetState.disabled)) { return BorderSide( - color: _scaleScheme.grayScale.border.withAlpha(127)); + color: _scaleScheme.grayScale.border.withAlpha(disabledAlpha)); } if (states.contains(WidgetState.error)) { if (states.contains(WidgetState.hovered)) { @@ -71,7 +96,7 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme { BorderSide? get outlineBorder => WidgetStateBorderSide.resolveWith((states) { if (states.contains(WidgetState.disabled)) { return BorderSide( - color: _scaleScheme.grayScale.border.withAlpha(127)); + color: _scaleScheme.grayScale.border.withAlpha(disabledAlpha)); } if (states.contains(WidgetState.error)) { if (states.contains(WidgetState.hovered)) { @@ -97,7 +122,7 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme { @override Color? get prefixIconColor => WidgetStateColor.resolveWith((states) { if (states.contains(WidgetState.disabled)) { - return _scaleScheme.primaryScale.primary.withAlpha(127); + return _scaleScheme.primaryScale.primary.withAlpha(disabledAlpha); } if (states.contains(WidgetState.error)) { return _scaleScheme.errorScale.primary; @@ -108,7 +133,7 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme { @override Color? get suffixIconColor => WidgetStateColor.resolveWith((states) { if (states.contains(WidgetState.disabled)) { - return _scaleScheme.primaryScale.primary.withAlpha(127); + return _scaleScheme.primaryScale.primary.withAlpha(disabledAlpha); } if (states.contains(WidgetState.error)) { return _scaleScheme.errorScale.primary; @@ -121,7 +146,39 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme { final textStyle = _textTheme.bodyLarge ?? const TextStyle(); if (states.contains(WidgetState.disabled)) { return textStyle.copyWith( - color: _scaleScheme.grayScale.border.withAlpha(127)); + color: _scaleScheme.grayScale.border.withAlpha(disabledAlpha)); + } + if (states.contains(WidgetState.error)) { + if (states.contains(WidgetState.hovered)) { + return textStyle.copyWith( + color: _scaleScheme.errorScale.hoverBorder); + } + if (states.contains(WidgetState.focused)) { + return textStyle.copyWith( + color: _scaleScheme.errorScale.hoverBorder); + } + return textStyle.copyWith( + color: _scaleScheme.errorScale.subtleBorder); + } + if (states.contains(WidgetState.hovered)) { + return textStyle.copyWith( + color: _scaleScheme.primaryScale.hoverBorder); + } + if (states.contains(WidgetState.focused)) { + return textStyle.copyWith( + color: _scaleScheme.primaryScale.hoverBorder); + } + return textStyle.copyWith( + color: _scaleScheme.primaryScale.border.withAlpha(hintAlpha)); + }); + + @override + TextStyle? get floatingLabelStyle => + WidgetStateTextStyle.resolveWith((states) { + final textStyle = _textTheme.bodyLarge ?? const TextStyle(); + if (states.contains(WidgetState.disabled)) { + return textStyle.copyWith( + color: _scaleScheme.grayScale.border.withAlpha(disabledAlpha)); } if (states.contains(WidgetState.error)) { if (states.contains(WidgetState.hovered)) { @@ -146,18 +203,14 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme { return textStyle.copyWith(color: _scaleScheme.primaryScale.border); }); - @override - TextStyle? get floatingLabelStyle => labelStyle; - @override TextStyle? get helperStyle => WidgetStateTextStyle.resolveWith((states) { final textStyle = _textTheme.bodySmall ?? const TextStyle(); if (states.contains(WidgetState.disabled)) { - return textStyle.copyWith( - color: _scaleScheme.grayScale.border.withAlpha(127)); + return textStyle.copyWith(color: _scaleScheme.grayScale.border); } return textStyle.copyWith( - color: _scaleScheme.secondaryScale.border.withAlpha(127)); + color: _scaleScheme.primaryScale.border.withAlpha(hintAlpha)); }); @override @@ -165,6 +218,14 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme { final textStyle = _textTheme.bodySmall ?? const TextStyle(); return textStyle.copyWith(color: _scaleScheme.errorScale.primary); }); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(IntProperty('disabledAlpha', disabledAlpha)) + ..add(IntProperty('hintAlpha', hintAlpha)); + } } extension ScaleInputDecoratorThemeExt on ScaleTheme { diff --git a/lib/theme/models/scale_theme/scale_scheme.dart b/lib/theme/models/scale_theme/scale_scheme.dart index ac266bc..8c4a6b8 100644 --- a/lib/theme/models/scale_theme/scale_scheme.dart +++ b/lib/theme/models/scale_theme/scale_scheme.dart @@ -76,8 +76,8 @@ class ScaleScheme extends ThemeExtension { ColorScheme toColorScheme(Brightness brightness) => ColorScheme( brightness: brightness, - primary: primaryScale.primary, // reviewed - onPrimary: primaryScale.primaryText, // reviewed + primary: primaryScale.primary, + onPrimary: primaryScale.primaryText, // primaryContainer: primaryScale.hoverElementBackground, // onPrimaryContainer: primaryScale.subtleText, secondary: secondaryScale.primary, @@ -92,15 +92,12 @@ class ScaleScheme extends ThemeExtension { onError: errorScale.primaryText, // errorContainer: errorScale.hoverElementBackground, // onErrorContainer: errorScale.subtleText, - background: grayScale.appBackground, // reviewed - onBackground: grayScale.appText, // reviewed - surface: primaryScale.appBackground, // reviewed - onSurface: primaryScale.appText, // reviewed - surfaceVariant: secondaryScale.appBackground, + surface: primaryScale.appBackground, + onSurface: primaryScale.appText, onSurfaceVariant: secondaryScale.appText, outline: primaryScale.border, outlineVariant: secondaryScale.border, - shadow: primaryScale.primary.darken(80), + shadow: primaryScale.appBackground.darken(60), //scrim: primaryScale.background, // inverseSurface: primaryScale.subtleText, // onInverseSurface: primaryScale.subtleBackground, diff --git a/lib/theme/models/scale_theme/scale_theme.dart b/lib/theme/models/scale_theme/scale_theme.dart index d539c86..4bfc438 100644 --- a/lib/theme/models/scale_theme/scale_theme.dart +++ b/lib/theme/models/scale_theme/scale_theme.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'scale_input_decorator_theme.dart'; import 'scale_scheme.dart'; export 'scale_color.dart'; @@ -41,4 +42,86 @@ class ScaleTheme extends ThemeExtension { scheme: scheme.lerp(other.scheme, t), config: config.lerp(other.config, t)); } + + ThemeData toThemeData(Brightness brightness) { + final colorScheme = scheme.toColorScheme(brightness); + + final baseThemeData = ThemeData.from( + colorScheme: colorScheme, textTheme: textTheme, useMaterial3: true); + + final elevatedButtonTheme = ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: scheme.primaryScale.elementBackground, + foregroundColor: scheme.primaryScale.appText, + disabledBackgroundColor: + scheme.grayScale.elementBackground.withAlpha(0x7F), + disabledForegroundColor: + scheme.grayScale.primary.withAlpha(0x7F), + shape: RoundedRectangleBorder( + side: BorderSide(color: scheme.primaryScale.border), + borderRadius: + BorderRadius.circular(8 * config.borderRadiusScale))) + .copyWith(side: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.disabled)) { + return BorderSide(color: scheme.grayScale.border.withAlpha(0x7F)); + } else if (states.contains(WidgetState.pressed)) { + return BorderSide( + color: scheme.primaryScale.border, + strokeAlign: BorderSide.strokeAlignOutside); + } else if (states.contains(WidgetState.hovered)) { + return BorderSide(color: scheme.primaryScale.hoverBorder); + } else if (states.contains(WidgetState.focused)) { + return BorderSide(color: scheme.primaryScale.hoverBorder, width: 2); + } + return BorderSide(color: scheme.primaryScale.border); + }))); + + final themeData = baseThemeData.copyWith( + scrollbarTheme: baseThemeData.scrollbarTheme.copyWith( + thumbColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.pressed)) { + return scheme.primaryScale.border; + } else if (states.contains(WidgetState.hovered)) { + return scheme.primaryScale.hoverBorder; + } + return scheme.primaryScale.subtleBorder; + }), trackColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.pressed)) { + return scheme.primaryScale.activeElementBackground; + } else if (states.contains(WidgetState.hovered)) { + return scheme.primaryScale.hoverElementBackground; + } + return scheme.primaryScale.elementBackground; + }), trackBorderColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.pressed)) { + return scheme.primaryScale.subtleBorder; + } else if (states.contains(WidgetState.hovered)) { + return scheme.primaryScale.subtleBorder; + } + return scheme.primaryScale.subtleBorder; + })), + appBarTheme: baseThemeData.appBarTheme.copyWith( + backgroundColor: scheme.primaryScale.border, + foregroundColor: scheme.primaryScale.borderText), + bottomSheetTheme: baseThemeData.bottomSheetTheme.copyWith( + elevation: 0, + modalElevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16 * config.borderRadiusScale), + topRight: Radius.circular(16 * config.borderRadiusScale)))), + canvasColor: scheme.primaryScale.subtleBackground, + chipTheme: baseThemeData.chipTheme.copyWith( + backgroundColor: scheme.primaryScale.elementBackground, + selectedColor: scheme.primaryScale.activeElementBackground, + surfaceTintColor: scheme.primaryScale.hoverElementBackground, + checkmarkColor: scheme.primaryScale.primary, + side: BorderSide(color: scheme.primaryScale.border)), + elevatedButtonTheme: elevatedButtonTheme, + inputDecorationTheme: + ScaleInputDecoratorTheme(scheme, config, textTheme), + extensions: >[scheme, config, this]); + + return themeData; + } } diff --git a/lib/theme/models/scale_theme/scale_tile_theme.dart b/lib/theme/models/scale_theme/scale_tile_theme.dart index e7339d1..da2c3cd 100644 --- a/lib/theme/models/scale_theme/scale_tile_theme.dart +++ b/lib/theme/models/scale_theme/scale_tile_theme.dart @@ -40,7 +40,10 @@ extension ScaleTileThemeExt on ScaleTheme { final shapeBorder = RoundedRectangleBorder( side: config.useVisualIndicators - ? BorderSide(width: 2, color: borderColor, strokeAlign: 0) + ? BorderSide( + width: 2, + color: borderColor, + ) : BorderSide.none, borderRadius: BorderRadius.circular(8 * config.borderRadiusScale)); diff --git a/lib/theme/models/scale_theme/scale_toast_theme.dart b/lib/theme/models/scale_theme/scale_toast_theme.dart index de310d4..61f119d 100644 --- a/lib/theme/models/scale_theme/scale_toast_theme.dart +++ b/lib/theme/models/scale_theme/scale_toast_theme.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'scale_scheme.dart'; import 'scale_theme.dart'; enum ScaleToastKind { @@ -35,14 +34,6 @@ extension ScaleToastThemeExt on ScaleTheme { ScaleToastTheme toastTheme(ScaleToastKind kind) { final toastScaleColor = scheme.scale(ScaleKind.tertiary); - Icon icon; - switch (kind) { - case ScaleToastKind.info: - icon = const Icon(Icons.info, size: 32); - case ScaleToastKind.error: - icon = const Icon(Icons.dangerous, size: 32); - } - final primaryColor = toastScaleColor.calloutText; final borderColor = toastScaleColor.border; final backgroundColor = config.useVisualIndicators @@ -54,6 +45,13 @@ extension ScaleToastThemeExt on ScaleTheme { final titleColor = config.useVisualIndicators ? toastScaleColor.calloutBackground : toastScaleColor.calloutText; + Icon icon; + switch (kind) { + case ScaleToastKind.info: + icon = Icon(Icons.info, size: 32, color: primaryColor); + case ScaleToastKind.error: + icon = Icon(Icons.dangerous, size: 32, color: primaryColor); + } return ScaleToastTheme( primaryColor: primaryColor, diff --git a/lib/theme/views/avatar_widget.dart b/lib/theme/views/avatar_widget.dart index 7a1f610..42bea11 100644 --- a/lib/theme/views/avatar_widget.dart +++ b/lib/theme/views/avatar_widget.dart @@ -5,7 +5,7 @@ import 'package:flutter/widgets.dart'; import '../theme.dart'; class AvatarWidget extends StatelessWidget { - AvatarWidget({ + const AvatarWidget({ required String name, required double size, required Color borderColor, @@ -38,15 +38,11 @@ class AvatarWidget extends StatelessWidget { height: _size, width: _size, decoration: BoxDecoration( - shape: BoxShape.circle, - border: _scaleConfig.useVisualIndicators - ? Border.all( - color: _borderColor, - width: 1 * (_size ~/ 32 + 1), - strokeAlign: BorderSide.strokeAlignOutside) - : null, - color: _borderColor, - ), + shape: BoxShape.circle, + border: Border.all( + color: _borderColor, + width: 1 * (_size ~/ 32 + 1), + strokeAlign: BorderSide.strokeAlignOutside)), child: AvatarImage( //size: 32, backgroundImage: _imageProvider, @@ -55,14 +51,15 @@ class AvatarWidget extends StatelessWidget { ? _foregroundColor : _backgroundColor, child: Text( - shortname, + shortname.isNotEmpty ? shortname : '?', + softWrap: false, style: _textStyle.copyWith( color: _scaleConfig.useVisualIndicators && !_scaleConfig.preferBorders ? _backgroundColor : _foregroundColor, ), - ))); + ).fit().paddingAll(_size / 16))); } //////////////////////////////////////////////////////////////////////////// diff --git a/lib/theme/views/slider_tile.dart b/lib/theme/views/slider_tile.dart index 9b6957a..d293fa6 100644 --- a/lib/theme/views/slider_tile.dart +++ b/lib/theme/views/slider_tile.dart @@ -125,16 +125,21 @@ class SliderTile extends StatelessWidget { child: ListTile( onTap: onTap, dense: true, - visualDensity: const VisualDensity(vertical: -4), + visualDensity: + const VisualDensity(horizontal: -4, vertical: -4), title: Text( title, overflow: TextOverflow.fade, softWrap: false, ), subtitle: subtitle.isNotEmpty ? Text(subtitle) : null, + minTileHeight: 48, iconColor: scaleTileTheme.textColor, textColor: scaleTileTheme.textColor, - leading: FittedBox(child: leading), - trailing: FittedBox(child: trailing)))))); + leading: + leading != null ? FittedBox(child: leading) : null, + trailing: trailing != null + ? FittedBox(child: trailing) + : null))))); } } diff --git a/lib/theme/views/styled_alert.dart b/lib/theme/views/styled_alert.dart index 39fa6f2..82218e8 100644 --- a/lib/theme/views/styled_alert.dart +++ b/lib/theme/views/styled_alert.dart @@ -94,16 +94,11 @@ Future showErrorModal( {required BuildContext context, required String title, required String text}) async { - // final theme = Theme.of(context); - // final scale = theme.extension()!; - // final scaleConfig = theme.extension()!; - await Alert( context: context, style: _alertStyle(context), useRootNavigator: false, type: AlertType.error, - //style: AlertStyle(), title: title, desc: text, buttons: [ @@ -122,10 +117,6 @@ Future showErrorModal( ), ) ], - - //backgroundColor: Colors.black, - //titleColor: Colors.white, - //textColor: Colors.white, ).show(); } @@ -144,16 +135,11 @@ Future showWarningModal( {required BuildContext context, required String title, required String text}) async { - // final theme = Theme.of(context); - // final scale = theme.extension()!; - // final scaleConfig = theme.extension()!; - await Alert( context: context, style: _alertStyle(context), useRootNavigator: false, type: AlertType.warning, - //style: AlertStyle(), title: title, desc: text, buttons: [ @@ -172,10 +158,6 @@ Future showWarningModal( ), ) ], - - //backgroundColor: Colors.black, - //titleColor: Colors.white, - //textColor: Colors.white, ).show(); } @@ -183,16 +165,11 @@ Future showWarningWidgetModal( {required BuildContext context, required String title, required Widget child}) async { - // final theme = Theme.of(context); - // final scale = theme.extension()!; - // final scaleConfig = theme.extension()!; - await Alert( context: context, style: _alertStyle(context), useRootNavigator: false, type: AlertType.warning, - //style: AlertStyle(), title: title, content: child, buttons: [ @@ -211,10 +188,6 @@ Future showWarningWidgetModal( ), ) ], - - //backgroundColor: Colors.black, - //titleColor: Colors.white, - //textColor: Colors.white, ).show(); } @@ -222,10 +195,6 @@ Future showConfirmModal( {required BuildContext context, required String title, required String text}) async { - // final theme = Theme.of(context); - // final scale = theme.extension()!; - // final scaleConfig = theme.extension()!; - var confirm = false; await Alert( @@ -266,10 +235,6 @@ Future showConfirmModal( ), ) ], - - //backgroundColor: Colors.black, - //titleColor: Colors.white, - //textColor: Colors.white, ).show(); return confirm; diff --git a/lib/theme/views/styled_dialog.dart b/lib/theme/views/styled_dialog.dart index 4e4bd50..75a0f6b 100644 --- a/lib/theme/views/styled_dialog.dart +++ b/lib/theme/views/styled_dialog.dart @@ -21,7 +21,7 @@ class StyledDialog extends StatelessWidget { Radius.circular(16 * scaleConfig.borderRadiusScale)), ), contentPadding: const EdgeInsets.all(4), - backgroundColor: scale.primaryScale.dialogBorder, + backgroundColor: scale.primaryScale.border, title: Text( title, style: textTheme.titleMedium! diff --git a/lib/theme/views/widget_helpers.dart b/lib/theme/views/widget_helpers.dart index 1b9e80f..f66af4b 100644 --- a/lib/theme/views/widget_helpers.dart +++ b/lib/theme/views/widget_helpers.dart @@ -114,14 +114,13 @@ extension LabelExt on Widget { {ScaleColor? scale}) { final theme = Theme.of(context); final scaleScheme = theme.extension()!; - // final scaleConfig = theme.extension()!; scale = scale ?? scaleScheme.primaryScale; - return Wrap(crossAxisAlignment: WrapCrossAlignment.center, children: [ + return Wrap(crossAxisAlignment: WrapCrossAlignment.end, children: [ Text( '$label:', - style: theme.textTheme.titleLarge!.copyWith(color: scale.border), - ).paddingLTRB(0, 0, 8, 8), + style: theme.textTheme.bodyLarge!.copyWith(color: scale.hoverBorder), + ).paddingLTRB(0, 0, 8, 0), this ]); } @@ -431,6 +430,31 @@ Widget styledTitleContainer({ ])); } +Widget styledCard({ + required BuildContext context, + required Widget child, + Color? borderColor, + Color? backgroundColor, + Color? titleColor, +}) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + + return DecoratedBox( + decoration: ShapeDecoration( + color: backgroundColor ?? scale.primaryScale.elementBackground, + shape: RoundedRectangleBorder( + side: (scaleConfig.useVisualIndicators || scaleConfig.preferBorders) + ? BorderSide( + color: borderColor ?? scale.primaryScale.border, width: 2) + : BorderSide.none, + borderRadius: + BorderRadius.circular(12 * scaleConfig.borderRadiusScale), + )), + child: child.paddingAll(4)); +} + Widget styledBottomSheet({ required BuildContext context, required String title, @@ -500,6 +524,12 @@ const grayColorFilter = ColorFilter.matrix([ 0, ]); +const dodgeFilter = + ColorFilter.mode(Color.fromARGB(96, 255, 255, 255), BlendMode.srcIn); + +const overlayFilter = + ColorFilter.mode(Color.fromARGB(127, 255, 255, 255), BlendMode.dstIn); + Container clipBorder({ required bool clipEnabled, required bool borderEnabled, @@ -510,16 +540,17 @@ Container clipBorder({ // ignore: avoid_unnecessary_containers, use_decorated_box Container( decoration: ShapeDecoration( - color: borderColor, shape: RoundedRectangleBorder( - borderRadius: clipEnabled - ? BorderRadius.circular(borderRadius) - : BorderRadius.zero, - )), + side: borderEnabled && clipEnabled + ? BorderSide(color: borderColor, width: 2) + : BorderSide.none, + borderRadius: clipEnabled + ? BorderRadius.circular(borderRadius) + : BorderRadius.zero, + )), child: ClipRRect( - clipBehavior: Clip.hardEdge, - borderRadius: clipEnabled - ? BorderRadius.circular(borderRadius) - : BorderRadius.zero, - child: child) - .paddingAll(clipEnabled && borderEnabled ? 2 : 0)); + clipBehavior: Clip.antiAliasWithSaveLayer, + borderRadius: clipEnabled + ? BorderRadius.circular(borderRadius - 2) + : BorderRadius.zero, + child: child)); diff --git a/lib/veilid_processor/views/developer.dart b/lib/veilid_processor/views/developer.dart index f561f07..1899c34 100644 --- a/lib/veilid_processor/views/developer.dart +++ b/lib/veilid_processor/views/developer.dart @@ -216,7 +216,7 @@ class _DeveloperPageState extends State { onPressed: () async { final confirm = await showConfirmModal( context: context, - title: translate('toast.confirm'), + title: translate('confirmation.confirm'), text: translate('developer.are_you_sure_clear'), ); if (confirm && context.mounted) { @@ -224,7 +224,7 @@ class _DeveloperPageState extends State { } }), SizedBox.fromSize( - size: const Size(120, 48), + size: const Size(140, 48), child: CustomDropdown( items: _logLevelDropdownItems, initialItem: _logLevelDropdownItems diff --git a/pubspec.lock b/pubspec.lock index b98424d..349a58b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -96,6 +96,14 @@ packages: relative: true source: path version: "0.1.7" + auto_size_text: + dependency: "direct main" + description: + name: auto_size_text + sha256: "3f5261cd3fb5f2a9ab4e2fc3fba84fd9fcaac8821f20a1d4e71f557521b22599" + url: "https://pub.dev" + source: hosted + version: "3.0.0" awesome_extensions: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 1e0a526..94f8952 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: ansicolor: ^2.0.3 archive: ^4.0.4 async_tools: ^0.1.7 + auto_size_text: ^3.0.0 awesome_extensions: ^2.0.21 badges: ^3.1.2 basic_utils: ^5.8.2 @@ -158,14 +159,16 @@ flutter: - assets/i18n/en.json # Launcher icon - assets/launcher/icon.png - # Images - - assets/images/splash.svg + # Vector Images + - assets/images/grid.svg - assets/images/icon.svg + - assets/images/splash.svg - assets/images/title.svg - assets/images/vlogo.svg + # Raster Images - assets/images/ellet.png - - assets/images/toilet.png - assets/images/handshake.png + - assets/images/toilet.png # Printing - assets/js/pdf/3.2.146/pdf.min.js # Sounds diff --git a/build.bat b/update_generated_files.bat similarity index 100% rename from build.bat rename to update_generated_files.bat diff --git a/build.sh b/update_generated_files.sh similarity index 100% rename from build.sh rename to update_generated_files.sh From 6bd60207d826dd5c6235186606ed64cf05887d68 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 17 Mar 2025 20:30:20 -0400 Subject: [PATCH 210/270] wallpapers and ui cleanup --- assets/i18n/en.json | 1 + assets/images/wallpaper/arctic.svg | 13 + assets/images/wallpaper/babydoll.svg | 663 ++ assets/images/wallpaper/eggplant.svg | 495 + assets/images/wallpaper/elite.svg | 6 + assets/images/wallpaper/forest.svg | 6888 ++++++++++++++ assets/images/wallpaper/garden.svg | 8182 +++++++++++++++++ assets/images/wallpaper/gold.svg | 26 + assets/images/wallpaper/grim.svg | 928 ++ assets/images/wallpaper/lapis.svg | 39 + assets/images/wallpaper/lime.svg | 25 + assets/images/wallpaper/scarlet.svg | 349 + assets/images/wallpaper/vapor.svg | 25 + lib/app.dart | 44 +- lib/chat/views/chat_component_widget.dart | 109 +- lib/chat/views/no_conversation_widget.dart | 10 +- lib/init.dart | 13 +- lib/layout/home/drawer_menu/drawer_menu.dart | 26 +- lib/layout/home/home_account_ready.dart | 35 +- lib/layout/home/home_screen.dart | 1 + lib/settings/settings_page.dart | 2 + lib/theme/models/chat_theme.dart | 8 +- lib/theme/models/radix_generator.dart | 28 + .../models/scale_theme/scale_scheme.dart | 21 +- lib/theme/models/theme_preference.dart | 24 +- .../models/theme_preference.freezed.dart | 38 +- lib/theme/models/theme_preference.g.dart | 2 + lib/theme/views/styled_scaffold.dart | 9 +- lib/theme/views/views.dart | 1 + lib/theme/views/wallpaper_preferences.dart | 32 + lib/theme/views/widget_helpers.dart | 40 +- pubspec.yaml | 14 +- 32 files changed, 17947 insertions(+), 150 deletions(-) create mode 100644 assets/images/wallpaper/arctic.svg create mode 100644 assets/images/wallpaper/babydoll.svg create mode 100644 assets/images/wallpaper/eggplant.svg create mode 100644 assets/images/wallpaper/elite.svg create mode 100644 assets/images/wallpaper/forest.svg create mode 100644 assets/images/wallpaper/garden.svg create mode 100644 assets/images/wallpaper/gold.svg create mode 100644 assets/images/wallpaper/grim.svg create mode 100644 assets/images/wallpaper/lapis.svg create mode 100644 assets/images/wallpaper/lime.svg create mode 100644 assets/images/wallpaper/scarlet.svg create mode 100644 assets/images/wallpaper/vapor.svg create mode 100644 lib/theme/views/wallpaper_preferences.dart diff --git a/assets/i18n/en.json b/assets/i18n/en.json index ef4c44c..31316d3 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -285,6 +285,7 @@ "delivery": "Delivery", "enable_badge": "Enable icon 'badge' bubble", "enable_notifications": "Enable notifications", + "enable_wallpaper": "Enable wallpaper", "message_notification_content": "Message notification content", "invitation_accepted": "On invitation accept/reject", "message_received": "On message received", diff --git a/assets/images/wallpaper/arctic.svg b/assets/images/wallpaper/arctic.svg new file mode 100644 index 0000000..ebcaee4 --- /dev/null +++ b/assets/images/wallpaper/arctic.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/images/wallpaper/babydoll.svg b/assets/images/wallpaper/babydoll.svg new file mode 100644 index 0000000..55f28a3 --- /dev/null +++ b/assets/images/wallpaper/babydoll.svg @@ -0,0 +1,663 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/wallpaper/eggplant.svg b/assets/images/wallpaper/eggplant.svg new file mode 100644 index 0000000..48e6ad3 --- /dev/null +++ b/assets/images/wallpaper/eggplant.svg @@ -0,0 +1,495 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/wallpaper/elite.svg b/assets/images/wallpaper/elite.svg new file mode 100644 index 0000000..606d21f --- /dev/null +++ b/assets/images/wallpaper/elite.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/images/wallpaper/forest.svg b/assets/images/wallpaper/forest.svg new file mode 100644 index 0000000..fb61069 --- /dev/null +++ b/assets/images/wallpaper/forest.svg @@ -0,0 +1,6888 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/wallpaper/garden.svg b/assets/images/wallpaper/garden.svg new file mode 100644 index 0000000..f4e6372 --- /dev/null +++ b/assets/images/wallpaper/garden.svg @@ -0,0 +1,8182 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/wallpaper/gold.svg b/assets/images/wallpaper/gold.svg new file mode 100644 index 0000000..16e5ab5 --- /dev/null +++ b/assets/images/wallpaper/gold.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/wallpaper/grim.svg b/assets/images/wallpaper/grim.svg new file mode 100644 index 0000000..7c3968f --- /dev/null +++ b/assets/images/wallpaper/grim.svg @@ -0,0 +1,928 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/wallpaper/lapis.svg b/assets/images/wallpaper/lapis.svg new file mode 100644 index 0000000..c78fa58 --- /dev/null +++ b/assets/images/wallpaper/lapis.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/wallpaper/lime.svg b/assets/images/wallpaper/lime.svg new file mode 100644 index 0000000..d65222e --- /dev/null +++ b/assets/images/wallpaper/lime.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/wallpaper/scarlet.svg b/assets/images/wallpaper/scarlet.svg new file mode 100644 index 0000000..7047ca7 --- /dev/null +++ b/assets/images/wallpaper/scarlet.svg @@ -0,0 +1,349 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/wallpaper/vapor.svg b/assets/images/wallpaper/vapor.svg new file mode 100644 index 0000000..34bfe59 --- /dev/null +++ b/assets/images/wallpaper/vapor.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/app.dart b/lib/app.dart index fade2c8..72d845f 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:provider/provider.dart'; @@ -49,19 +48,24 @@ class VeilidChatApp extends StatelessWidget { final ThemeData initialThemeData; void _reloadTheme(BuildContext context) { - log.info('Reloading theme'); - final theme = - PreferencesRepository.instance.value.themePreference.themeData(); - ThemeSwitcher.of(context).changeTheme(theme: theme); - - // Hack to reload translations - final localizationDelegate = LocalizedApp.of(context).delegate; singleFuture(this, () async { - await LocalizationDelegate.create( - fallbackLocale: localizationDelegate.fallbackLocale.toString(), - supportedLocales: localizationDelegate.supportedLocales - .map((x) => x.toString()) - .toList()); + log.info('Reloading theme'); + + await VeilidChatGlobalInit.loadAssetManifest(); + + final theme = + PreferencesRepository.instance.value.themePreference.themeData(); + if (context.mounted) { + ThemeSwitcher.of(context).changeTheme(theme: theme); + + // Hack to reload translations + final localizationDelegate = LocalizedApp.of(context).delegate; + await LocalizationDelegate.create( + fallbackLocale: localizationDelegate.fallbackLocale.toString(), + supportedLocales: localizationDelegate.supportedLocales + .map((x) => x.toString()) + .toList()); + } }); } @@ -164,17 +168,17 @@ class VeilidChatApp extends StatelessWidget { scale.primaryScale.subtleBackground, ]); + final wallpaper = PreferencesRepository + .instance.value.themePreference + .wallpaper(); + return Stack( fit: StackFit.expand, alignment: Alignment.center, children: [ - DecoratedBox( - decoration: BoxDecoration(gradient: gradient)), - SvgPicture.asset( - 'assets/images/grid.svg', - fit: BoxFit.cover, - colorFilter: overlayFilter, - ), + wallpaper ?? + DecoratedBox( + decoration: BoxDecoration(gradient: gradient)), MaterialApp.router( scrollBehavior: const ScrollBehaviorModified(), debugShowCheckedModeBanner: false, diff --git a/lib/chat/views/chat_component_widget.dart b/lib/chat/views/chat_component_widget.dart index 79d1f1c..8e43299 100644 --- a/lib/chat/views/chat_component_widget.dart +++ b/lib/chat/views/chat_component_widget.dart @@ -82,15 +82,16 @@ class ChatComponentWidget extends StatelessWidget { Widget _buildChatComponent(BuildContext context) { final theme = Theme.of(context); - final scale = theme.extension()!; + final scaleScheme = theme.extension()!; final scaleConfig = theme.extension()!; + final scale = scaleScheme.scale(ScaleKind.primary); final textTheme = theme.textTheme; - final chatTheme = makeChatTheme(scale, scaleConfig, textTheme); + final chatTheme = makeChatTheme(scaleScheme, scaleConfig, textTheme); final errorChatTheme = (ChatThemeEditor(chatTheme) - ..inputTextColor = scale.errorScale.primary + ..inputTextColor = scaleScheme.errorScale.primary ..sendButtonIcon = Image.asset( 'assets/icon-send.png', - color: scale.errorScale.primary, + color: scaleScheme.errorScale.primary, package: 'flutter_chat_ui', )) .commit(); @@ -126,7 +127,7 @@ class ChatComponentWidget extends StatelessWidget { Container( height: 48, decoration: BoxDecoration( - color: scale.primaryScale.subtleBorder, + color: scale.border, ), child: Row(children: [ Align( @@ -136,12 +137,11 @@ class ChatComponentWidget extends StatelessWidget { child: Text(title, textAlign: TextAlign.start, style: textTheme.titleMedium! - .copyWith(color: scale.primaryScale.borderText)), + .copyWith(color: scale.borderText)), )), const Spacer(), IconButton( - icon: - Icon(Icons.close, color: scale.primaryScale.borderText), + icon: Icon(Icons.close, color: scale.borderText), onPressed: _onClose) .paddingLTRB(16, 0, 16, 0) ]), @@ -201,54 +201,51 @@ class ChatComponentWidget extends StatelessWidget { 2048; return Chat( - key: chatComponentState.chatKey, - theme: - messageIsValid ? chatTheme : errorChatTheme, - messages: messageWindow.window.toList(), - scrollToBottomOnSend: isFirstPage, - scrollController: - chatComponentState.scrollController, - inputOptions: InputOptions( - inputClearMode: messageIsValid - ? InputClearMode.always - : InputClearMode.never, - textEditingController: - chatComponentState.textEditingController), - // isLastPage: isLastPage, - // onEndReached: () async { - // await _handlePageBackward( - // chatComponentCubit, messageWindow); - // }, - //onEndReachedThreshold: onEndReachedThreshold, - //onAttachmentPressed: _handleAttachmentPressed, - //onMessageTap: _handleMessageTap, - //onPreviewDataFetched: _handlePreviewDataFetched, - usePreviewData: false, // - onSendPressed: (pt) { - try { - if (!messageIsValid) { - context.read().error( - text: - translate('chat.message_too_long')); - return; - } - _handleSendPressed(chatComponentCubit, pt); - } on FormatException { - context.read().error( - text: translate('chat.message_too_long')); - } - }, - listBottomWidget: messageIsValid - ? null - : Text(translate('chat.message_too_long'), - style: TextStyle( - color: scale.errorScale.primary)) - .toCenter(), - //showUserAvatars: false, - //showUserNames: true, - user: localUser, - emptyState: const EmptyChatWidget()) - .paddingLTRB(0, 2, 0, 0); + key: chatComponentState.chatKey, + theme: messageIsValid ? chatTheme : errorChatTheme, + messages: messageWindow.window.toList(), + scrollToBottomOnSend: isFirstPage, + scrollController: chatComponentState.scrollController, + inputOptions: InputOptions( + inputClearMode: messageIsValid + ? InputClearMode.always + : InputClearMode.never, + textEditingController: + chatComponentState.textEditingController), + // isLastPage: isLastPage, + // onEndReached: () async { + // await _handlePageBackward( + // chatComponentCubit, messageWindow); + // }, + //onEndReachedThreshold: onEndReachedThreshold, + //onAttachmentPressed: _handleAttachmentPressed, + //onMessageTap: _handleMessageTap, + //onPreviewDataFetched: _handlePreviewDataFetched, + usePreviewData: false, // + onSendPressed: (pt) { + try { + if (!messageIsValid) { + context.read().error( + text: translate('chat.message_too_long')); + return; + } + _handleSendPressed(chatComponentCubit, pt); + } on FormatException { + context.read().error( + text: translate('chat.message_too_long')); + } + }, + listBottomWidget: messageIsValid + ? null + : Text(translate('chat.message_too_long'), + style: TextStyle( + color: + scaleScheme.errorScale.primary)) + .toCenter(), + //showUserAvatars: false, + //showUserNames: true, + user: localUser, + emptyState: const EmptyChatWidget()); }))).expanded(), ], ); diff --git a/lib/chat/views/no_conversation_widget.dart b/lib/chat/views/no_conversation_widget.dart index df1b6d3..830e0d6 100644 --- a/lib/chat/views/no_conversation_widget.dart +++ b/lib/chat/views/no_conversation_widget.dart @@ -12,11 +12,13 @@ class NoConversationWidget extends StatelessWidget { BuildContext context, ) { final theme = Theme.of(context); - final scale = theme.extension()!; + final scaleScheme = theme.extension()!; + final scaleConfig = theme.extension()!; + final scale = scaleScheme.scale(ScaleKind.primary); return DecoratedBox( decoration: BoxDecoration( - color: scale.primaryScale.appBackground.withAlpha(192), + color: scale.appBackground.withAlpha(scaleConfig.wallpaperAlpha), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -24,14 +26,14 @@ class NoConversationWidget extends StatelessWidget { children: [ Icon( Icons.diversity_3, - color: scale.primaryScale.appText.withAlpha(127), + color: scale.appText.withAlpha(127), size: 48, ), Text( textAlign: TextAlign.center, translate('chat.start_a_conversation'), style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: scale.primaryScale.appText.withAlpha(127), + color: scale.appText.withAlpha(127), ), ), ], diff --git a/lib/init.dart b/lib/init.dart index d2744c7..8c80bf9 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:veilid_support/veilid_support.dart'; import 'account_manager/account_manager.dart'; @@ -8,6 +9,8 @@ import 'app.dart'; import 'tools/tools.dart'; import 'veilid_processor/veilid_processor.dart'; +List rootAssets = []; + class VeilidChatGlobalInit { VeilidChatGlobalInit._(); @@ -28,14 +31,22 @@ class VeilidChatGlobalInit { logger: (message) => log.debug('DHTRecordPool: $message')); } -// Initialize repositories + // Initialize repositories Future _initializeRepositories() async { await AccountRepository.instance.init(); } + // Initialize asset manifest + static Future loadAssetManifest() async { + final assetManifest = await AssetManifest.loadFromAssetBundle(rootBundle); + rootAssets = assetManifest.listAssets(); + } + static Future initialize() async { final veilidChatGlobalInit = VeilidChatGlobalInit._(); + await loadAssetManifest(); + log.info('Initializing Veilid'); await veilidChatGlobalInit._initializeVeilid(); log.info('Initializing Repositories'); diff --git a/lib/layout/home/drawer_menu/drawer_menu.dart b/lib/layout/home/drawer_menu/drawer_menu.dart index b56d437..f88a888 100644 --- a/lib/layout/home/drawer_menu/drawer_menu.dart +++ b/lib/layout/home/drawer_menu/drawer_menu.dart @@ -320,29 +320,7 @@ class _DrawerMenuState extends State { return DecoratedBox( decoration: ShapeDecoration( - shadows: [ - if (scaleConfig.useVisualIndicators && !scaleConfig.preferBorders) - BoxShadow( - color: scale.primary.darken(60), - spreadRadius: 2, - ) - else if (scaleConfig.useVisualIndicators && - scaleConfig.preferBorders) - BoxShadow( - color: scale.border, - spreadRadius: 2, - ) - else - BoxShadow( - color: scale.appBackground.darken(60).withAlpha(0x3F), - blurRadius: 16, - spreadRadius: 2, - offset: const Offset( - 0, - 2, - ), - ), - ], + shadows: themedShadow(scaleConfig, scale), gradient: scaleConfig.useVisualIndicators ? null : gradient, color: scaleConfig.useVisualIndicators ? (scaleConfig.preferBorders @@ -381,7 +359,7 @@ class _DrawerMenuState extends State { 'assets/images/title.svg', colorFilter: scaleConfig.useVisualIndicators ? grayColorFilter - : dodgeFilter), + : src96StencilFilter), ]))), Text(translate('menu.accounts'), style: theme.textTheme.titleMedium!.copyWith( diff --git a/lib/layout/home/home_account_ready.dart b/lib/layout/home/home_account_ready.dart index 7b67c0f..8710eea 100644 --- a/lib/layout/home/home_account_ready.dart +++ b/lib/layout/home/home_account_ready.dart @@ -146,8 +146,9 @@ class _HomeAccountReadyState extends State { ); final theme = Theme.of(context); - final scale = theme.extension()!; + final scaleScheme = theme.extension()!; final scaleConfig = theme.extension()!; + final scale = scaleScheme.scale(ScaleKind.primary); final activeChat = context.watch().state; final hasActiveChat = activeChat != null; @@ -163,7 +164,9 @@ class _HomeAccountReadyState extends State { visibleLeft = true; visibleRight = true; leftWidth = leftColumnSize; - rightWidth = constraints.maxWidth - leftColumnSize - 2; + rightWidth = constraints.maxWidth - + leftColumnSize - + (scaleConfig.useVisualIndicators ? 2 : 0); } else { if (hasActiveChat) { visibleLeft = false; @@ -180,19 +183,21 @@ class _HomeAccountReadyState extends State { return Row(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Offstage( - offstage: !visibleLeft, - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: leftWidth), - child: buildLeftPane(context))), - Offstage( - offstage: !(visibleLeft && visibleRight), - child: SizedBox( - width: 2, - height: double.infinity, - child: ColoredBox( - color: scaleConfig.preferBorders - ? scale.primaryScale.subtleBorder - : scale.primaryScale.subtleBackground))), + offstage: !visibleLeft, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: leftWidth), + child: buildLeftPane(context))) + .withThemedShadow(scaleConfig, scale), + if (scaleConfig.useVisualIndicators) + Offstage( + offstage: !(visibleLeft && visibleRight), + child: SizedBox( + width: 2, + height: double.infinity, + child: ColoredBox( + color: scaleConfig.preferBorders + ? scale.subtleBorder + : scale.subtleBackground))), Offstage( offstage: !visibleRight, child: ConstrainedBox( diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index 3e8e98b..0ec1f26 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -229,6 +229,7 @@ class HomeScreenState extends State angle: 0, //mainScreenOverlayColor: theme.shadowColor.withAlpha(0x2F), openCurve: Curves.fastEaseInToSlowEaseOut, + closeCurve: Curves.fastEaseInToSlowEaseOut, // duration: const Duration(milliseconds: 250), // reverseDuration: const Duration(milliseconds: 250), menuScreenTapClose: canClose, diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart index 05ba514..f164992 100644 --- a/lib/settings/settings_page.dart +++ b/lib/settings/settings_page.dart @@ -53,6 +53,8 @@ class SettingsPageState extends State { .paddingLTRB(0, 8, 0, 0), buildSettingsPageBrightnessPreferences( context: context, onChanged: () => setState(() {})), + buildSettingsPageWallpaperPreferences( + context: context, onChanged: () => setState(() {})), buildSettingsPageNotificationPreferences( context: context, onChanged: () => setState(() {})), ].map((x) => x.paddingLTRB(0, 0, 0, 8)).toList(), diff --git a/lib/theme/models/chat_theme.dart b/lib/theme/models/chat_theme.dart index cd0b9ce..5552979 100644 --- a/lib/theme/models/chat_theme.dart +++ b/lib/theme/models/chat_theme.dart @@ -14,14 +14,15 @@ ChatTheme makeChatTheme( secondaryColor: scaleConfig.preferBorders ? scale.secondaryScale.calloutText : scale.secondaryScale.calloutBackground, - backgroundColor: scale.grayScale.appBackground.withAlpha(192), + backgroundColor: + scale.grayScale.appBackground.withAlpha(scaleConfig.wallpaperAlpha), messageBorderRadius: scaleConfig.borderRadiusScale * 16, bubbleBorderSide: scaleConfig.preferBorders ? BorderSide( color: scale.primaryScale.calloutBackground, width: 2, ) - : null, + : BorderSide(width: 2, color: Colors.black.withAlpha(96)), sendButtonIcon: Image.asset( 'assets/icon-send.png', color: scaleConfig.preferBorders @@ -86,7 +87,8 @@ ChatTheme makeChatTheme( receivedEmojiMessageTextStyle: const TextStyle( color: Colors.white, fontSize: 64, - )); + ), + dateDividerTextStyle: textTheme.labelSmall!); class EditedChatTheme extends ChatTheme { const EditedChatTheme({ diff --git a/lib/theme/models/radix_generator.dart b/lib/theme/models/radix_generator.dart index a5c5f87..3e3b0e6 100644 --- a/lib/theme/models/radix_generator.dart +++ b/lib/theme/models/radix_generator.dart @@ -603,6 +603,33 @@ TextTheme makeRadixTextTheme(Brightness brightness) { return textTheme; } +double wallpaperAlpha(Brightness brightness, RadixThemeColor themeColor) { + switch (themeColor) { + case RadixThemeColor.scarlet: + return 64; + case RadixThemeColor.babydoll: + return 192; + case RadixThemeColor.vapor: + return 192; + case RadixThemeColor.gold: + return 192; + case RadixThemeColor.garden: + return brightness == Brightness.dark ? 192 : 128; + case RadixThemeColor.forest: + return 192; + case RadixThemeColor.arctic: + return brightness == Brightness.dark ? 208 : 180; + case RadixThemeColor.lapis: + return brightness == Brightness.dark ? 128 : 192; + case RadixThemeColor.eggplant: + return brightness == Brightness.dark ? 192 : 192; + case RadixThemeColor.lime: + return brightness == Brightness.dark ? 192 : 128; + case RadixThemeColor.grim: + return brightness == Brightness.dark ? 240 : 224; + } +} + ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) { final textTheme = makeRadixTextTheme(brightness); final radix = _radixScheme(brightness, themeColor); @@ -611,6 +638,7 @@ ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) { useVisualIndicators: false, preferBorders: false, borderRadiusScale: 1, + wallpaperAlpha: wallpaperAlpha(brightness, themeColor), ); final scaleTheme = ScaleTheme( diff --git a/lib/theme/models/scale_theme/scale_scheme.dart b/lib/theme/models/scale_theme/scale_scheme.dart index 8c4a6b8..6bdae25 100644 --- a/lib/theme/models/scale_theme/scale_scheme.dart +++ b/lib/theme/models/scale_theme/scale_scheme.dart @@ -111,22 +111,27 @@ class ScaleConfig extends ThemeExtension { required this.useVisualIndicators, required this.preferBorders, required this.borderRadiusScale, - }); + required double wallpaperAlpha, + }) : _wallpaperAlpha = wallpaperAlpha; final bool useVisualIndicators; final bool preferBorders; final double borderRadiusScale; + final double _wallpaperAlpha; + + int get wallpaperAlpha => _wallpaperAlpha.toInt(); @override - ScaleConfig copyWith({ - bool? useVisualIndicators, - bool? preferBorders, - double? borderRadiusScale, - }) => + ScaleConfig copyWith( + {bool? useVisualIndicators, + bool? preferBorders, + double? borderRadiusScale, + double? wallpaperAlpha}) => ScaleConfig( useVisualIndicators: useVisualIndicators ?? this.useVisualIndicators, preferBorders: preferBorders ?? this.preferBorders, borderRadiusScale: borderRadiusScale ?? this.borderRadiusScale, + wallpaperAlpha: wallpaperAlpha ?? this._wallpaperAlpha, ); @override @@ -139,6 +144,8 @@ class ScaleConfig extends ThemeExtension { t < .5 ? useVisualIndicators : other.useVisualIndicators, preferBorders: t < .5 ? preferBorders : other.preferBorders, borderRadiusScale: - lerpDouble(borderRadiusScale, other.borderRadiusScale, t) ?? 1); + lerpDouble(borderRadiusScale, other.borderRadiusScale, t) ?? 1, + wallpaperAlpha: + lerpDouble(_wallpaperAlpha, other._wallpaperAlpha, t) ?? 1); } } diff --git a/lib/theme/models/theme_preference.dart b/lib/theme/models/theme_preference.dart index 9d6f6dc..dc2e082 100644 --- a/lib/theme/models/theme_preference.dart +++ b/lib/theme/models/theme_preference.dart @@ -1,7 +1,10 @@ import 'package:change_case/change_case.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import '../../init.dart'; import '../views/widget_helpers.dart'; import 'contrast_generator.dart'; import 'radix_generator.dart'; @@ -53,6 +56,7 @@ class ThemePreferences with _$ThemePreferences { BrightnessPreference brightnessPreference, @Default(ColorPreference.vapor) ColorPreference colorPreference, @Default(1) double displayScale, + @Default(true) bool enableWallpaper, }) = _ThemePreferences; factory ThemePreferences.fromJson(dynamic json) => @@ -62,6 +66,17 @@ class ThemePreferences with _$ThemePreferences { } extension ThemePreferencesExt on ThemePreferences { + /// Get wallpaper for existing theme + Widget? wallpaper() { + if (enableWallpaper) { + final assetName = 'assets/images/wallpaper/${colorPreference.name}.svg'; + if (rootAssets.contains(assetName)) { + return SvgPicture.asset(assetName, fit: BoxFit.cover); + } + } + return null; + } + /// Get material 'ThemeData' for existing theme ThemeData themeData() { late final Brightness brightness; @@ -87,7 +102,8 @@ extension ThemePreferencesExt on ThemePreferences { scaleConfig: ScaleConfig( useVisualIndicators: true, preferBorders: false, - borderRadiusScale: 1), + borderRadiusScale: 1, + wallpaperAlpha: 255), primaryFront: Colors.black, primaryBack: Colors.white, secondaryFront: Colors.black, @@ -106,7 +122,8 @@ extension ThemePreferencesExt on ThemePreferences { scaleConfig: ScaleConfig( useVisualIndicators: true, preferBorders: true, - borderRadiusScale: 0.2), + borderRadiusScale: 0.2, + wallpaperAlpha: 208), primaryFront: const Color(0xFF000000), primaryBack: const Color(0xFF00FF00), secondaryFront: const Color(0xFF000000), @@ -123,7 +140,8 @@ extension ThemePreferencesExt on ThemePreferences { scaleConfig: ScaleConfig( useVisualIndicators: true, preferBorders: true, - borderRadiusScale: 0.2), + borderRadiusScale: 0.2, + wallpaperAlpha: 192), primaryFront: const Color(0xFF000000), primaryBack: const Color(0xFF00FF00), secondaryFront: const Color(0xFF000000), diff --git a/lib/theme/models/theme_preference.freezed.dart b/lib/theme/models/theme_preference.freezed.dart index 657d021..d96ed38 100644 --- a/lib/theme/models/theme_preference.freezed.dart +++ b/lib/theme/models/theme_preference.freezed.dart @@ -24,6 +24,7 @@ mixin _$ThemePreferences { throw _privateConstructorUsedError; ColorPreference get colorPreference => throw _privateConstructorUsedError; double get displayScale => throw _privateConstructorUsedError; + bool get enableWallpaper => throw _privateConstructorUsedError; /// Serializes this ThemePreferences to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -44,7 +45,8 @@ abstract class $ThemePreferencesCopyWith<$Res> { $Res call( {BrightnessPreference brightnessPreference, ColorPreference colorPreference, - double displayScale}); + double displayScale, + bool enableWallpaper}); } /// @nodoc @@ -65,6 +67,7 @@ class _$ThemePreferencesCopyWithImpl<$Res, $Val extends ThemePreferences> Object? brightnessPreference = null, Object? colorPreference = null, Object? displayScale = null, + Object? enableWallpaper = null, }) { return _then(_value.copyWith( brightnessPreference: null == brightnessPreference @@ -79,6 +82,10 @@ class _$ThemePreferencesCopyWithImpl<$Res, $Val extends ThemePreferences> ? _value.displayScale : displayScale // ignore: cast_nullable_to_non_nullable as double, + enableWallpaper: null == enableWallpaper + ? _value.enableWallpaper + : enableWallpaper // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val); } } @@ -94,7 +101,8 @@ abstract class _$$ThemePreferencesImplCopyWith<$Res> $Res call( {BrightnessPreference brightnessPreference, ColorPreference colorPreference, - double displayScale}); + double displayScale, + bool enableWallpaper}); } /// @nodoc @@ -113,6 +121,7 @@ class __$$ThemePreferencesImplCopyWithImpl<$Res> Object? brightnessPreference = null, Object? colorPreference = null, Object? displayScale = null, + Object? enableWallpaper = null, }) { return _then(_$ThemePreferencesImpl( brightnessPreference: null == brightnessPreference @@ -127,6 +136,10 @@ class __$$ThemePreferencesImplCopyWithImpl<$Res> ? _value.displayScale : displayScale // ignore: cast_nullable_to_non_nullable as double, + enableWallpaper: null == enableWallpaper + ? _value.enableWallpaper + : enableWallpaper // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -137,7 +150,8 @@ class _$ThemePreferencesImpl implements _ThemePreferences { const _$ThemePreferencesImpl( {this.brightnessPreference = BrightnessPreference.system, this.colorPreference = ColorPreference.vapor, - this.displayScale = 1}); + this.displayScale = 1, + this.enableWallpaper = true}); factory _$ThemePreferencesImpl.fromJson(Map json) => _$$ThemePreferencesImplFromJson(json); @@ -151,10 +165,13 @@ class _$ThemePreferencesImpl implements _ThemePreferences { @override @JsonKey() final double displayScale; + @override + @JsonKey() + final bool enableWallpaper; @override String toString() { - return 'ThemePreferences(brightnessPreference: $brightnessPreference, colorPreference: $colorPreference, displayScale: $displayScale)'; + return 'ThemePreferences(brightnessPreference: $brightnessPreference, colorPreference: $colorPreference, displayScale: $displayScale, enableWallpaper: $enableWallpaper)'; } @override @@ -167,13 +184,15 @@ class _$ThemePreferencesImpl implements _ThemePreferences { (identical(other.colorPreference, colorPreference) || other.colorPreference == colorPreference) && (identical(other.displayScale, displayScale) || - other.displayScale == displayScale)); + other.displayScale == displayScale) && + (identical(other.enableWallpaper, enableWallpaper) || + other.enableWallpaper == enableWallpaper)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash( - runtimeType, brightnessPreference, colorPreference, displayScale); + int get hashCode => Object.hash(runtimeType, brightnessPreference, + colorPreference, displayScale, enableWallpaper); /// Create a copy of ThemePreferences /// with the given fields replaced by the non-null parameter values. @@ -196,7 +215,8 @@ abstract class _ThemePreferences implements ThemePreferences { const factory _ThemePreferences( {final BrightnessPreference brightnessPreference, final ColorPreference colorPreference, - final double displayScale}) = _$ThemePreferencesImpl; + final double displayScale, + final bool enableWallpaper}) = _$ThemePreferencesImpl; factory _ThemePreferences.fromJson(Map json) = _$ThemePreferencesImpl.fromJson; @@ -207,6 +227,8 @@ abstract class _ThemePreferences implements ThemePreferences { ColorPreference get colorPreference; @override double get displayScale; + @override + bool get enableWallpaper; /// Create a copy of ThemePreferences /// with the given fields replaced by the non-null parameter values. diff --git a/lib/theme/models/theme_preference.g.dart b/lib/theme/models/theme_preference.g.dart index 4cb2d71..23c3d38 100644 --- a/lib/theme/models/theme_preference.g.dart +++ b/lib/theme/models/theme_preference.g.dart @@ -16,6 +16,7 @@ _$ThemePreferencesImpl _$$ThemePreferencesImplFromJson( ? ColorPreference.vapor : ColorPreference.fromJson(json['color_preference']), displayScale: (json['display_scale'] as num?)?.toDouble() ?? 1, + enableWallpaper: json['enable_wallpaper'] as bool? ?? true, ); Map _$$ThemePreferencesImplToJson( @@ -24,4 +25,5 @@ Map _$$ThemePreferencesImplToJson( 'brightness_preference': instance.brightnessPreference.toJson(), 'color_preference': instance.colorPreference.toJson(), 'display_scale': instance.displayScale, + 'enable_wallpaper': instance.enableWallpaper, }; diff --git a/lib/theme/views/styled_scaffold.dart b/lib/theme/views/styled_scaffold.dart index 9ee3d36..9d02fab 100644 --- a/lib/theme/views/styled_scaffold.dart +++ b/lib/theme/views/styled_scaffold.dart @@ -9,8 +9,9 @@ class StyledScaffold extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final scale = theme.extension()!; + final scaleScheme = theme.extension()!; final scaleConfig = theme.extension()!; + final scale = scaleScheme.scale(ScaleKind.primary); final enableBorder = !isMobileSize(context); @@ -18,13 +19,11 @@ class StyledScaffold extends StatelessWidget { clipEnabled: enableBorder, borderEnabled: scaleConfig.useVisualIndicators, borderRadius: 16 * scaleConfig.borderRadiusScale, - borderColor: scale.primaryScale.border, + borderColor: scale.border, child: Scaffold(appBar: appBar, body: body, key: key)); if (!scaleConfig.useVisualIndicators) { - scaffold = scaffold.withShadow( - offset: const Offset(0, 16), - shadowColor: scale.primaryScale.primary.withAlpha(0x3F).darken(60)); + scaffold = scaffold.withThemedShadow(scaleConfig, scale); } return GestureDetector( diff --git a/lib/theme/views/views.dart b/lib/theme/views/views.dart index b5aa809..88f4a4a 100644 --- a/lib/theme/views/views.dart +++ b/lib/theme/views/views.dart @@ -12,4 +12,5 @@ export 'slider_tile.dart'; export 'styled_alert.dart'; export 'styled_dialog.dart'; export 'styled_scaffold.dart'; +export 'wallpaper_preferences.dart'; export 'widget_helpers.dart'; diff --git a/lib/theme/views/wallpaper_preferences.dart b/lib/theme/views/wallpaper_preferences.dart new file mode 100644 index 0000000..1c2e9de --- /dev/null +++ b/lib/theme/views/wallpaper_preferences.dart @@ -0,0 +1,32 @@ +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 formFieldEnableWallpaper = 'enable_wallpaper'; + +Widget buildSettingsPageWallpaperPreferences( + {required BuildContext context, required void Function() onChanged}) { + final preferencesRepository = PreferencesRepository.instance; + final themePreferences = preferencesRepository.value.themePreference; + return ThemeSwitcher.withTheme( + builder: (_, switcher, theme) => FormBuilderCheckbox( + name: formFieldEnableWallpaper, + title: Text(translate('settings_page.enable_wallpaper')), + initialValue: themePreferences.enableWallpaper, + onChanged: (value) async { + if (value != null) { + final newThemePrefs = + themePreferences.copyWith(enableWallpaper: value); + final newPrefs = preferencesRepository.value + .copyWith(themePreference: newThemePrefs); + + await preferencesRepository.set(newPrefs); + switcher.changeTheme(theme: newThemePrefs.themeData()); + onChanged(); + } + })); +} diff --git a/lib/theme/views/widget_helpers.dart b/lib/theme/views/widget_helpers.dart index f66af4b..1b768dd 100644 --- a/lib/theme/views/widget_helpers.dart +++ b/lib/theme/views/widget_helpers.dart @@ -19,6 +19,40 @@ extension BorderExt on Widget { child: this); } +extension ShadowExt on Widget { + Container withThemedShadow(ScaleConfig scaleConfig, ScaleColor scale) => + // ignore: use_decorated_box + Container( + decoration: BoxDecoration( + boxShadow: themedShadow(scaleConfig, scale), + ), + child: this, + ); +} + +List themedShadow(ScaleConfig scaleConfig, ScaleColor scale) => [ + if (scaleConfig.useVisualIndicators && !scaleConfig.preferBorders) + BoxShadow( + color: scale.primary.darken(60), + spreadRadius: 2, + ) + else if (scaleConfig.useVisualIndicators && scaleConfig.preferBorders) + BoxShadow( + color: scale.border, + spreadRadius: 2, + ) + else + BoxShadow( + color: scale.primary.darken(60).withAlpha(0x7F), + blurRadius: 16, + spreadRadius: 2, + offset: const Offset( + 0, + 2, + ), + ), + ]; + extension SizeToFixExt on Widget { FittedBox fit({BoxFit? fit, Key? key}) => FittedBox( key: key, @@ -524,10 +558,10 @@ const grayColorFilter = ColorFilter.matrix([ 0, ]); -const dodgeFilter = +const src96StencilFilter = ColorFilter.mode(Color.fromARGB(96, 255, 255, 255), BlendMode.srcIn); -const overlayFilter = +const dst127StencilFilter = ColorFilter.mode(Color.fromARGB(127, 255, 255, 255), BlendMode.dstIn); Container clipBorder({ @@ -551,6 +585,6 @@ Container clipBorder({ child: ClipRRect( clipBehavior: Clip.antiAliasWithSaveLayer, borderRadius: clipEnabled - ? BorderRadius.circular(borderRadius - 2) + ? BorderRadius.circular(borderRadius - 4) : BorderRadius.zero, child: child)); diff --git a/pubspec.yaml b/pubspec.yaml index 94f8952..5f924ba 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -159,8 +159,20 @@ flutter: - assets/i18n/en.json # Launcher icon - assets/launcher/icon.png + # Theme wallpaper + - assets/images/wallpaper/arctic.svg + - assets/images/wallpaper/babydoll.svg + - assets/images/wallpaper/eggplant.svg + - assets/images/wallpaper/elite.svg + - assets/images/wallpaper/forest.svg + - assets/images/wallpaper/garden.svg + - assets/images/wallpaper/gold.svg + - assets/images/wallpaper/grim.svg + - assets/images/wallpaper/lapis.svg + - assets/images/wallpaper/lime.svg + - assets/images/wallpaper/scarlet.svg + - assets/images/wallpaper/vapor.svg # Vector Images - - assets/images/grid.svg - assets/images/icon.svg - assets/images/splash.svg - assets/images/title.svg From ef1ded4494743d66ebef9e6b645180c38772c450 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 17 Mar 2025 22:00:26 -0400 Subject: [PATCH 211/270] updates and cleanup --- assets/i18n/en.json | 5 +- lib/chat/cubits/chat_component_cubit.dart | 15 +++-- lib/contacts/views/contact_item_widget.dart | 16 ++++- lib/contacts/views/contacts_browser.dart | 14 ++--- lib/contacts/views/contacts_dialog.dart | 2 +- lib/settings/settings_page.dart | 66 ++++++++++++--------- lib/theme/views/brightness_preferences.dart | 35 +++++------ lib/theme/views/color_preferences.dart | 39 ++++++------ lib/theme/views/wallpaper_preferences.dart | 35 +++++------ pubspec.lock | 13 ++-- pubspec.yaml | 10 ++-- 11 files changed, 142 insertions(+), 108 deletions(-) diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 31316d3..61bb802 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -100,7 +100,8 @@ "yes": "Yes", "no": "No", "update": "Update", - "waiting_for_network": "Waiting For Network" + "waiting_for_network": "Waiting For Network", + "chat": "Chat" }, "toast": { "error": "Error", @@ -127,7 +128,7 @@ "contacts": "Contacts", "edit_contact": "Edit Contact", "invitations": "Invitations", - "no_contact_selected": "Double-click a contact to edit it", + "no_contact_selected": "Select a contact to view or edit", "new_chat": "Open Chat", "close_contact": "Close Contact" }, diff --git a/lib/chat/cubits/chat_component_cubit.dart b/lib/chat/cubits/chat_component_cubit.dart index 326a597..9e50e02 100644 --- a/lib/chat/cubits/chat_component_cubit.dart +++ b/lib/chat/cubits/chat_component_cubit.dart @@ -50,8 +50,11 @@ class ChatComponentCubit extends Cubit { messageWindow: const AsyncLoading(), title: '', )) { + // Immediate Init + _init(); + // Async Init - _initWait.add(_init); + _initWait.add(_initAsync); } factory ChatComponentCubit.singleContact( @@ -68,7 +71,7 @@ class ChatComponentCubit extends Cubit { messagesCubit: messagesCubit, ); - Future _init(Completer _cancel) async { + void _init() { // Get local user info and account record cubit _localUserIdentityKey = _accountInfo.identityTypedPublicKey; @@ -77,9 +80,6 @@ class ChatComponentCubit extends Cubit { _accountRecordCubit.stream.listen(_onChangedAccountRecord); _onChangedAccountRecord(_accountRecordCubit.state); - // Subscribe to remote user info - await _updateConversationSubscriptions(); - // Subscribe to messages _messagesSubscription = _messagesCubit.stream.listen(_onChangedMessages); _onChangedMessages(_messagesCubit.state); @@ -90,6 +90,11 @@ class ChatComponentCubit extends Cubit { _onChangedContacts(_contactListCubit.state); } + Future _initAsync(Completer _cancel) async { + // Subscribe to remote user info + await _updateConversationSubscriptions(); + } + @override Future close() async { await _initWait(); diff --git a/lib/contacts/views/contact_item_widget.dart b/lib/contacts/views/contact_item_widget.dart index a0f2fbc..4614f27 100644 --- a/lib/contacts/views/contact_item_widget.dart +++ b/lib/contacts/views/contact_item_widget.dart @@ -68,15 +68,27 @@ class ContactItemWidget extends StatelessWidget { : () => singleFuture((this, _kOnTap), () async { await _onTap(_contact); }), - endActions: [ + startActions: [ if (_onDoubleTap != null) + SliderTileAction( + //icon: Icons.edit, + label: translate('button.chat'), + actionScale: ScaleKind.secondary, + onPressed: (_context) => + singleFuture((this, _kOnTap), () async { + await _onDoubleTap(_contact); + }), + ), + ], + endActions: [ + if (_onTap != null) SliderTileAction( //icon: Icons.edit, label: translate('button.edit'), actionScale: ScaleKind.secondary, onPressed: (_context) => singleFuture((this, _kOnTap), () async { - await _onDoubleTap(_contact); + await _onTap(_contact); }), ), if (_onDelete != null) diff --git a/lib/contacts/views/contacts_browser.dart b/lib/contacts/views/contacts_browser.dart index 7040af5..c5a6b22 100644 --- a/lib/contacts/views/contacts_browser.dart +++ b/lib/contacts/views/contacts_browser.dart @@ -39,14 +39,14 @@ class ContactsBrowserElement { class ContactsBrowser extends StatefulWidget { const ContactsBrowser( {required this.onContactSelected, - required this.onChatStarted, + required this.onStartChat, this.selectedContactRecordKey, super.key}); @override State createState() => _ContactsBrowserState(); final Future Function(proto.Contact? contact) onContactSelected; - final Future Function(proto.Contact contact) onChatStarted; + final Future Function(proto.Contact contact) onStartChat; final TypedKey? selectedContactRecordKey; @override @@ -60,7 +60,7 @@ class ContactsBrowser extends StatefulWidget { 'onContactSelected', onContactSelected)) ..add( ObjectFlagProperty Function(proto.Contact contact)>.has( - 'onChatStarted', onChatStarted)); + 'onStartChat', onStartChat)); } } @@ -238,8 +238,8 @@ class _ContactsBrowserState extends State selected: widget.selectedContactRecordKey == contact.localConversationRecordKey.toVeilid(), disabled: false, - onDoubleTap: _onTapContact, - onTap: _onStartChat, + onDoubleTap: _onStartChat, + onTap: _onSelectContact, onDelete: _onDeleteContact) .paddingLTRB(0, 4, 0, 0); case ContactsBrowserElementKind.invitation: @@ -293,12 +293,12 @@ class _ContactsBrowserState extends State ]); } - Future _onTapContact(proto.Contact contact) async { + Future _onSelectContact(proto.Contact contact) async { await widget.onContactSelected(contact); } Future _onStartChat(proto.Contact contact) async { - await widget.onChatStarted(contact); + await widget.onStartChat(contact); } Future _onDeleteContact(proto.Contact contact) async { diff --git a/lib/contacts/views/contacts_dialog.dart b/lib/contacts/views/contacts_dialog.dart index ec85df3..721043e 100644 --- a/lib/contacts/views/contacts_dialog.dart +++ b/lib/contacts/views/contacts_dialog.dart @@ -135,7 +135,7 @@ class _ContactsDialogState extends State { ?.localConversationRecordKey .toVeilid(), onContactSelected: _onContactSelected, - onChatStarted: _onChatStarted, + onStartChat: _onChatStarted, ).paddingLTRB(8, 0, 8, 8)))), if (enableRight && enableLeft) Container( diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart index f164992..2a05f08 100644 --- a/lib/settings/settings_page.dart +++ b/lib/settings/settings_page.dart @@ -31,34 +31,42 @@ class SettingsPageState extends State { @override Widget build(BuildContext context) => AsyncBlocBuilder( - builder: (context, state) => StyledScaffold( - appBar: DefaultAppBar( - title: Text(translate('settings_page.titlebar')), - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => GoRouterHelper(context).pop(), - ), - actions: [ - const SignalStrengthMeterWidget().paddingLTRB(16, 0, 16, 0), - ]), - body: ThemeSwitchingArea( - child: FormBuilder( - key: _formKey, - child: ListView( - padding: const EdgeInsets.all(8), - children: [ - buildSettingsPageColorPreferences( + builder: (context, state) => ThemeSwitcher.withTheme( + builder: (_, switcher, theme) => StyledScaffold( + appBar: DefaultAppBar( + title: Text(translate('settings_page.titlebar')), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => GoRouterHelper(context).pop(), + ), + actions: [ + const SignalStrengthMeterWidget() + .paddingLTRB(16, 0, 16, 0), + ]), + body: ThemeSwitchingArea( + child: FormBuilder( + key: _formKey, + child: ListView( + padding: const EdgeInsets.all(8), + children: [ + buildSettingsPageColorPreferences( + context: context, + switcher: switcher, + onChanged: () => setState(() {})) + .paddingLTRB(0, 8, 0, 0), + buildSettingsPageBrightnessPreferences( context: context, - onChanged: () => setState(() {})) - .paddingLTRB(0, 8, 0, 0), - buildSettingsPageBrightnessPreferences( - context: context, onChanged: () => setState(() {})), - buildSettingsPageWallpaperPreferences( - context: context, onChanged: () => setState(() {})), - buildSettingsPageNotificationPreferences( - context: context, onChanged: () => setState(() {})), - ].map((x) => x.paddingLTRB(0, 0, 0, 8)).toList(), - ), - ).paddingSymmetric(horizontal: 8, vertical: 8), - ))); + switcher: switcher, + onChanged: () => setState(() {})), + buildSettingsPageWallpaperPreferences( + context: context, + switcher: switcher, + onChanged: () => setState(() {})), + buildSettingsPageNotificationPreferences( + context: context, + onChanged: () => setState(() {})), + ].map((x) => x.paddingLTRB(0, 0, 0, 8)).toList(), + ), + ).paddingSymmetric(horizontal: 8, vertical: 8), + )))); } diff --git a/lib/theme/views/brightness_preferences.dart b/lib/theme/views/brightness_preferences.dart index 0c39976..7a1bb1d 100644 --- a/lib/theme/views/brightness_preferences.dart +++ b/lib/theme/views/brightness_preferences.dart @@ -22,24 +22,25 @@ List> _getBrightnessDropdownItems() { } Widget buildSettingsPageBrightnessPreferences( - {required BuildContext context, required void Function() onChanged}) { + {required BuildContext context, + required void Function() onChanged, + required ThemeSwitcherState switcher}) { final preferencesRepository = PreferencesRepository.instance; final themePreferences = preferencesRepository.value.themePreference; - 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(themePreference: newThemePrefs); + return 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(themePreference: newThemePrefs); - await preferencesRepository.set(newPrefs); - switcher.changeTheme(theme: newThemePrefs.themeData()); - onChanged(); - })); + 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 index ce03c0a..a9a8841 100644 --- a/lib/theme/views/color_preferences.dart +++ b/lib/theme/views/color_preferences.dart @@ -1,4 +1,5 @@ import 'package:animated_theme_switcher/animated_theme_switcher.dart'; +import 'package:async_tools/async_tools.dart'; import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_translate/flutter_translate.dart'; @@ -7,6 +8,7 @@ import '../../settings/settings.dart'; import '../models/models.dart'; const String formFieldTheme = 'theme'; +const String _kSwitchTheme = 'switchTheme'; List> _getThemeDropdownItems() { const colorPrefs = ColorPreference.values; @@ -32,24 +34,27 @@ List> _getThemeDropdownItems() { } Widget buildSettingsPageColorPreferences( - {required BuildContext context, required void Function() onChanged}) { + {required BuildContext context, + required void Function() onChanged, + required ThemeSwitcherState switcher}) { final preferencesRepository = PreferencesRepository.instance; final themePreferences = preferencesRepository.value.themePreference; - 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(themePreference: newThemePrefs); + return FormBuilderDropdown( + name: formFieldTheme, + decoration: + InputDecoration(label: Text(translate('settings_page.color_theme'))), + items: _getThemeDropdownItems(), + initialValue: themePreferences.colorPreference, + onChanged: (value) { + singleFuture(_kSwitchTheme, () async { + final newThemePrefs = themePreferences.copyWith( + colorPreference: value as ColorPreference); + final newPrefs = preferencesRepository.value + .copyWith(themePreference: newThemePrefs); - await preferencesRepository.set(newPrefs); - switcher.changeTheme(theme: newThemePrefs.themeData()); - onChanged(); - })); + await preferencesRepository.set(newPrefs); + switcher.changeTheme(theme: newThemePrefs.themeData()); + onChanged(); + }); + }); } diff --git a/lib/theme/views/wallpaper_preferences.dart b/lib/theme/views/wallpaper_preferences.dart index 1c2e9de..48f0a6a 100644 --- a/lib/theme/views/wallpaper_preferences.dart +++ b/lib/theme/views/wallpaper_preferences.dart @@ -9,24 +9,25 @@ import '../models/models.dart'; const String formFieldEnableWallpaper = 'enable_wallpaper'; Widget buildSettingsPageWallpaperPreferences( - {required BuildContext context, required void Function() onChanged}) { + {required BuildContext context, + required void Function() onChanged, + required ThemeSwitcherState switcher}) { final preferencesRepository = PreferencesRepository.instance; final themePreferences = preferencesRepository.value.themePreference; - return ThemeSwitcher.withTheme( - builder: (_, switcher, theme) => FormBuilderCheckbox( - name: formFieldEnableWallpaper, - title: Text(translate('settings_page.enable_wallpaper')), - initialValue: themePreferences.enableWallpaper, - onChanged: (value) async { - if (value != null) { - final newThemePrefs = - themePreferences.copyWith(enableWallpaper: value); - final newPrefs = preferencesRepository.value - .copyWith(themePreference: newThemePrefs); + return FormBuilderCheckbox( + name: formFieldEnableWallpaper, + title: Text(translate('settings_page.enable_wallpaper')), + initialValue: themePreferences.enableWallpaper, + onChanged: (value) async { + if (value != null) { + final newThemePrefs = + themePreferences.copyWith(enableWallpaper: value); + final newPrefs = preferencesRepository.value + .copyWith(themePreference: newThemePrefs); - await preferencesRepository.set(newPrefs); - switcher.changeTheme(theme: newThemePrefs.themeData()); - onChanged(); - } - })); + await preferencesRepository.set(newPrefs); + switcher.changeTheme(theme: newThemePrefs.themeData()); + onChanged(); + } + }); } diff --git a/pubspec.lock b/pubspec.lock index 349a58b..93fdce8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -92,10 +92,11 @@ packages: async_tools: dependency: "direct main" description: - path: "../dart_async_tools" - relative: true - source: path - version: "0.1.7" + name: async_tools + sha256: a258558160d6adc18612d0c635ce0d18ceabc022f7933ce78ca4806075d79578 + url: "https://pub.dev" + source: hosted + version: "0.1.8" auto_size_text: dependency: "direct main" description: @@ -156,10 +157,10 @@ packages: dependency: "direct main" description: name: bloc_advanced_tools - sha256: d8a680d8a0469456399fb26bae9f7a1d2a1420b5bdf75e204e0fadab9edb0811 + sha256: "977f3c7e3f9a19aec2f2c734ae99c8f0799c1b78f9fd7e4dce91a2dbf773e11b" url: "https://pub.dev" source: hosted - version: "0.1.8" + version: "0.1.9" blurry_modal_progress_hud: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 5f924ba..8b4a0a7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,13 +15,13 @@ dependencies: animated_theme_switcher: ^2.0.10 ansicolor: ^2.0.3 archive: ^4.0.4 - async_tools: ^0.1.7 + async_tools: ^0.1.8 auto_size_text: ^3.0.0 awesome_extensions: ^2.0.21 badges: ^3.1.2 basic_utils: ^5.8.2 bloc: ^8.1.4 - bloc_advanced_tools: ^0.1.8 + bloc_advanced_tools: ^0.1.9 blurry_modal_progress_hud: ^1.1.1 change_case: ^2.2.0 charcode: ^1.4.0 @@ -112,9 +112,9 @@ dependencies: xterm: ^4.0.0 zxing2: ^0.2.3 -dependency_overrides: - async_tools: - path: ../dart_async_tools +#dependency_overrides: +# async_tools: +# path: ../dart_async_tools # bloc_advanced_tools: # path: ../bloc_advanced_tools # searchable_listview: From debb475bdcdcade379d23b4d5c345696575720af Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 17 Mar 2025 22:05:06 -0400 Subject: [PATCH 212/270] pod --- ios/Podfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c00efec..edf99c6 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -151,7 +151,7 @@ SPEC CHECKSUMS: camera_avfoundation: 04b44aeb14070126c6529e5ab82cc7c9fca107cf file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_native_splash: 6cad9122ea0fad137d23137dd14b937f3e90b145 + flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318 GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 From 0d888363ff35be0a5d5feb60a293b4b69e91b008 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 17 Mar 2025 22:51:34 -0400 Subject: [PATCH 213/270] native device orientation work --- lib/app.dart | 182 ++++++++++--------- lib/layout/home/drawer_menu/drawer_menu.dart | 10 +- lib/layout/home/home_screen.dart | 67 ++++--- 3 files changed, 141 insertions(+), 118 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index 72d845f..683a450 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -8,6 +8,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; +import 'package:native_device_orientation/native_device_orientation.dart'; import 'package:provider/provider.dart'; import 'package:veilid_support/veilid_support.dart'; @@ -100,6 +101,94 @@ class VeilidChatApp extends StatelessWidget { onInvoke: (intent) => _attachDetach(context)), }, child: Focus(autofocus: true, child: builder(context))))); + Widget appBuilder( + BuildContext context, LocalizationDelegate localizationDelegate) => + ThemeProvider( + initTheme: initialThemeData, + builder: (context, theme) => LocalizationProvider( + state: LocalizationProvider.of(context).state, + child: MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => + PreferencesCubit(PreferencesRepository.instance), + ), + BlocProvider( + create: (context) => NotificationsCubit( + const NotificationsState(queue: IList.empty()))), + BlocProvider( + create: (context) => + ConnectionStateCubit(ProcessorRepository.instance)), + BlocProvider( + create: (context) => RouterCubit(AccountRepository.instance), + ), + BlocProvider( + create: (context) => + LocalAccountsCubit(AccountRepository.instance), + ), + BlocProvider( + create: (context) => + UserLoginsCubit(AccountRepository.instance), + ), + BlocProvider( + create: (context) => + ActiveLocalAccountCubit(AccountRepository.instance), + ), + BlocProvider( + create: (context) => PerAccountCollectionBlocMapCubit( + accountRepository: AccountRepository.instance, + locator: context.read)), + ], + child: + BackgroundTicker(child: _buildShortcuts(builder: (context) { + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + + final gradient = LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: scaleConfig.preferBorders && + theme.brightness == Brightness.light + ? [ + scale.grayScale.hoverElementBackground, + scale.grayScale.subtleBackground, + ] + : [ + scale.primaryScale.hoverElementBackground, + scale.primaryScale.subtleBackground, + ]); + + final wallpaper = PreferencesRepository + .instance.value.themePreference + .wallpaper(); + + return Stack( + fit: StackFit.expand, + alignment: Alignment.center, + children: [ + wallpaper ?? + DecoratedBox( + decoration: BoxDecoration(gradient: gradient)), + MaterialApp.router( + scrollBehavior: const ScrollBehaviorModified(), + debugShowCheckedModeBanner: false, + routerConfig: context.read().router(), + title: translate('app.title'), + theme: theme, + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + FormBuilderLocalizations.delegate, + localizationDelegate + ], + supportedLocales: localizationDelegate.supportedLocales, + locale: localizationDelegate.currentLocale, + ) + ]); + })), + )), + ); + @override Widget build(BuildContext context) => FutureProvider( initialData: null, @@ -112,93 +201,14 @@ class VeilidChatApp extends StatelessWidget { } // Once init is done, we proceed with the app final localizationDelegate = LocalizedApp.of(context).delegate; - return ThemeProvider( - initTheme: initialThemeData, - builder: (context, theme) => LocalizationProvider( - state: LocalizationProvider.of(context).state, - child: MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => - PreferencesCubit(PreferencesRepository.instance), - ), - BlocProvider( - create: (context) => NotificationsCubit( - const NotificationsState(queue: IList.empty()))), - BlocProvider( - create: (context) => - ConnectionStateCubit(ProcessorRepository.instance)), - BlocProvider( - create: (context) => - RouterCubit(AccountRepository.instance), - ), - BlocProvider( - create: (context) => - LocalAccountsCubit(AccountRepository.instance), - ), - BlocProvider( - create: (context) => - UserLoginsCubit(AccountRepository.instance), - ), - BlocProvider( - create: (context) => - ActiveLocalAccountCubit(AccountRepository.instance), - ), - BlocProvider( - create: (context) => PerAccountCollectionBlocMapCubit( - accountRepository: AccountRepository.instance, - locator: context.read)), - ], - child: - BackgroundTicker(child: _buildShortcuts(builder: (context) { - final scale = theme.extension()!; - final scaleConfig = theme.extension()!; - final gradient = LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: scaleConfig.preferBorders && - theme.brightness == Brightness.light - ? [ - scale.grayScale.hoverElementBackground, - scale.grayScale.subtleBackground, - ] - : [ - scale.primaryScale.hoverElementBackground, - scale.primaryScale.subtleBackground, - ]); - - final wallpaper = PreferencesRepository - .instance.value.themePreference - .wallpaper(); - - return Stack( - fit: StackFit.expand, - alignment: Alignment.center, - children: [ - wallpaper ?? - DecoratedBox( - decoration: BoxDecoration(gradient: gradient)), - MaterialApp.router( - scrollBehavior: const ScrollBehaviorModified(), - debugShowCheckedModeBanner: false, - routerConfig: context.read().router(), - title: translate('app.title'), - theme: theme, - localizationsDelegates: [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - FormBuilderLocalizations.delegate, - localizationDelegate - ], - supportedLocales: - localizationDelegate.supportedLocales, - locale: localizationDelegate.currentLocale, - ) - ]); - })), - )), - ); + if (isiOS || isAndroid) { + return NativeDeviceOrientationReader( + //useSensor: false, + builder: (context) => appBuilder(context, localizationDelegate)); + } else { + return appBuilder(context, localizationDelegate); + } }); @override diff --git a/lib/layout/home/drawer_menu/drawer_menu.dart b/lib/layout/home/drawer_menu/drawer_menu.dart index f88a888..6aa817f 100644 --- a/lib/layout/home/drawer_menu/drawer_menu.dart +++ b/lib/layout/home/drawer_menu/drawer_menu.dart @@ -318,7 +318,7 @@ class _DrawerMenuState extends State { scale.subtleBorder, ]); - return DecoratedBox( + Widget menu = DecoratedBox( decoration: ShapeDecoration( shadows: themedShadow(scaleConfig, scale), gradient: scaleConfig.useVisualIndicators ? null : gradient, @@ -393,6 +393,12 @@ class _DrawerMenuState extends State { ), ]) ]).paddingAll(16), - ).paddingLTRB(0, 2, 2, 2); + ); + + if (scaleConfig.preferBorders || scaleConfig.useVisualIndicators) { + menu = menu.paddingLTRB(0, 2, 2, 2); + } + + return menu; } } diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index 0ec1f26..56f4c02 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -13,6 +13,7 @@ import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../settings/settings.dart'; import '../../theme/theme.dart'; +import '../../tools/native_safe_area.dart'; import 'drawer_menu/drawer_menu.dart'; import 'home_account_invalid.dart'; import 'home_account_locked.dart'; @@ -208,36 +209,42 @@ class HomeScreenState extends State .indexWhere((x) => x.superIdentity.recordKey == activeLocalAccount); final canClose = activeIndex != -1; - return SafeArea( - child: DefaultTextStyle( - style: theme.textTheme.bodySmall!, - child: ZoomDrawer( - controller: _zoomDrawerController, - menuScreen: Builder(builder: (context) { - final zoomDrawer = ZoomDrawer.of(context); - zoomDrawer!.stateNotifier.addListener(() { - if (zoomDrawer.isOpen()) { - FocusManager.instance.primaryFocus?.unfocus(); - } - }); - return const DrawerMenu(); - }), - mainScreen: Provider.value( - value: _zoomDrawerController, - child: Builder(builder: _buildAccountPageView)), - borderRadius: 0, - angle: 0, - //mainScreenOverlayColor: theme.shadowColor.withAlpha(0x2F), - openCurve: Curves.fastEaseInToSlowEaseOut, - closeCurve: Curves.fastEaseInToSlowEaseOut, - // duration: const Duration(milliseconds: 250), - // reverseDuration: const Duration(milliseconds: 250), - menuScreenTapClose: canClose, - mainScreenTapClose: canClose, - disableDragGesture: !canClose, - mainScreenScale: .25, - slideWidth: min(360, MediaQuery.of(context).size.width * 0.9), - ))); + Widget homeWidget = DefaultTextStyle( + style: theme.textTheme.bodySmall!, + child: ZoomDrawer( + controller: _zoomDrawerController, + menuScreen: Builder(builder: (context) { + final zoomDrawer = ZoomDrawer.of(context); + zoomDrawer!.stateNotifier.addListener(() { + if (zoomDrawer.isOpen()) { + FocusManager.instance.primaryFocus?.unfocus(); + } + }); + return const DrawerMenu(); + }), + mainScreen: Provider.value( + value: _zoomDrawerController, + child: Builder(builder: _buildAccountPageView)), + borderRadius: 0, + angle: 0, + //mainScreenOverlayColor: theme.shadowColor.withAlpha(0x2F), + openCurve: Curves.fastEaseInToSlowEaseOut, + closeCurve: Curves.fastEaseInToSlowEaseOut, + // duration: const Duration(milliseconds: 250), + // reverseDuration: const Duration(milliseconds: 250), + menuScreenTapClose: canClose, + mainScreenTapClose: canClose, + disableDragGesture: !canClose, + mainScreenScale: .25, + slideWidth: min(360, MediaQuery.of(context).size.width * 0.9), + )); + + if (isiOS || isAndroid) { + homeWidget = NativeSafeArea( + bottom: false, left: false, right: false, child: homeWidget); + } + + return homeWidget; } //////////////////////////////////////////////////////////////////////////// From 3c95c9d1a36a908835738e53f0ec7044ce49edf0 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 18 Mar 2025 15:34:39 -0400 Subject: [PATCH 214/270] fix safeareas --- ios/Podfile.lock | 6 -- lib/app.dart | 9 +- lib/chat/views/no_conversation_widget.dart | 44 ++++---- lib/layout/home/home_account_ready.dart | 51 +++++----- lib/layout/home/home_screen.dart | 10 +- lib/tools/native_safe_area.dart | 111 --------------------- lib/veilid_processor/views/developer.dart | 5 +- pubspec.lock | 8 -- pubspec.yaml | 1 - 9 files changed, 51 insertions(+), 194 deletions(-) delete mode 100644 lib/tools/native_safe_area.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index edf99c6..2528d2d 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -54,8 +54,6 @@ PODS: - nanopb/encode (= 3.30910.0) - nanopb/decode (3.30910.0) - nanopb/encode (3.30910.0) - - native_device_orientation (0.0.1): - - Flutter - package_info_plus (0.4.5): - Flutter - pasteboard (0.0.1): @@ -87,7 +85,6 @@ DEPENDENCIES: - Flutter (from `Flutter`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`) - - native_device_orientation (from `.symlinks/plugins/native_device_orientation/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - pasteboard (from `.symlinks/plugins/pasteboard/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) @@ -124,8 +121,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_native_splash/ios" mobile_scanner: :path: ".symlinks/plugins/mobile_scanner/ios" - native_device_orientation: - :path: ".symlinks/plugins/native_device_orientation/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" pasteboard: @@ -163,7 +158,6 @@ SPEC CHECKSUMS: MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - native_device_orientation: e3580675687d5034770da198f6839ebf2122ef94 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 diff --git a/lib/app.dart b/lib/app.dart index 683a450..519f2bb 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -8,7 +8,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; -import 'package:native_device_orientation/native_device_orientation.dart'; import 'package:provider/provider.dart'; import 'package:veilid_support/veilid_support.dart'; @@ -202,13 +201,7 @@ class VeilidChatApp extends StatelessWidget { // Once init is done, we proceed with the app final localizationDelegate = LocalizedApp.of(context).delegate; - if (isiOS || isAndroid) { - return NativeDeviceOrientationReader( - //useSensor: false, - builder: (context) => appBuilder(context, localizationDelegate)); - } else { - return appBuilder(context, localizationDelegate); - } + return SafeArea(child: appBuilder(context, localizationDelegate)); }); @override diff --git a/lib/chat/views/no_conversation_widget.dart b/lib/chat/views/no_conversation_widget.dart index 830e0d6..13c4cc6 100644 --- a/lib/chat/views/no_conversation_widget.dart +++ b/lib/chat/views/no_conversation_widget.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_translate/flutter_translate.dart'; import '../../theme/models/scale_theme/scale_scheme.dart'; +import '../../theme/views/views.dart'; class NoConversationWidget extends StatelessWidget { const NoConversationWidget({super.key}); @@ -17,27 +18,26 @@ class NoConversationWidget extends StatelessWidget { final scale = scaleScheme.scale(ScaleKind.primary); return DecoratedBox( - decoration: BoxDecoration( - color: scale.appBackground.withAlpha(scaleConfig.wallpaperAlpha), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.diversity_3, - color: scale.appText.withAlpha(127), - size: 48, - ), - Text( - textAlign: TextAlign.center, - translate('chat.start_a_conversation'), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: scale.appText.withAlpha(127), - ), - ), - ], - ), - ); + decoration: BoxDecoration( + color: scale.appBackground.withAlpha(scaleConfig.wallpaperAlpha), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.diversity_3, + color: scale.appText.withAlpha(127), + size: 48, + ), + Text( + textAlign: TextAlign.center, + translate('chat.start_a_conversation'), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: scale.appText.withAlpha(127), + ), + ), + ], + )); } } diff --git a/lib/layout/home/home_account_ready.dart b/lib/layout/home/home_account_ready.dart index 8710eea..9109b78 100644 --- a/lib/layout/home/home_account_ready.dart +++ b/lib/layout/home/home_account_ready.dart @@ -93,33 +93,32 @@ class _HomeAccountReadyState extends State { }); }); - Widget buildUserPanel() => Builder(builder: (context) { - final profile = context.select( - (c) => c.state.asData!.value.profile); - final theme = Theme.of(context); - final scale = theme.extension()!; - final scaleConfig = theme.extension()!; - - return ColoredBox( - color: scaleConfig.preferBorders - ? scale.primaryScale.subtleBackground - : scale.primaryScale.subtleBorder, - child: Column(children: [ - Row(children: [ - buildMenuButton().paddingLTRB(0, 0, 8, 0), - ProfileWidget( - profile: profile, - showPronouns: false, - ).expanded(), - buildContactsButton().paddingLTRB(8, 0, 0, 0), - ]).paddingAll(8), - const ChatListWidget().expanded() - ])); - }); - Widget buildLeftPane(BuildContext context) => Builder( - builder: (context) => - Material(color: Colors.transparent, child: buildUserPanel())); + builder: (context) => Material( + color: Colors.transparent, + child: Builder(builder: (context) { + final profile = context.select( + (c) => c.state.asData!.value.profile); + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + + return ColoredBox( + color: scaleConfig.preferBorders + ? scale.primaryScale.subtleBackground + : scale.primaryScale.subtleBorder, + child: Column(children: [ + Row(children: [ + buildMenuButton().paddingLTRB(0, 0, 8, 0), + ProfileWidget( + profile: profile, + showPronouns: false, + ).expanded(), + buildContactsButton().paddingLTRB(8, 0, 0, 0), + ]).paddingAll(8), + const ChatListWidget().expanded() + ])); + }))); Widget buildRightPane(BuildContext context) { final activeChatCubit = context.watch(); diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index 56f4c02..63e2f19 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -13,7 +13,6 @@ import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../settings/settings.dart'; import '../../theme/theme.dart'; -import '../../tools/native_safe_area.dart'; import 'drawer_menu/drawer_menu.dart'; import 'home_account_invalid.dart'; import 'home_account_locked.dart'; @@ -209,7 +208,7 @@ class HomeScreenState extends State .indexWhere((x) => x.superIdentity.recordKey == activeLocalAccount); final canClose = activeIndex != -1; - Widget homeWidget = DefaultTextStyle( + return DefaultTextStyle( style: theme.textTheme.bodySmall!, child: ZoomDrawer( controller: _zoomDrawerController, @@ -238,13 +237,6 @@ class HomeScreenState extends State mainScreenScale: .25, slideWidth: min(360, MediaQuery.of(context).size.width * 0.9), )); - - if (isiOS || isAndroid) { - homeWidget = NativeSafeArea( - bottom: false, left: false, right: false, child: homeWidget); - } - - return homeWidget; } //////////////////////////////////////////////////////////////////////////// diff --git a/lib/tools/native_safe_area.dart b/lib/tools/native_safe_area.dart deleted file mode 100644 index ed3746e..0000000 --- a/lib/tools/native_safe_area.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:native_device_orientation/native_device_orientation.dart'; - -class NativeSafeArea extends StatelessWidget { - const NativeSafeArea({ - required this.child, - this.left = true, - this.top = true, - this.right = true, - this.bottom = true, - this.minimum = EdgeInsets.zero, - this.maintainBottomViewPadding = false, - super.key, - }); - - /// Whether to avoid system intrusions on the left. - final bool left; - - /// Whether to avoid system intrusions at the top of the screen, typically the - /// system status bar. - final bool top; - - /// Whether to avoid system intrusions on the right. - final bool right; - - /// Whether to avoid system intrusions on the bottom side of the screen. - final bool bottom; - - /// This minimum padding to apply. - /// - /// The greater of the minimum insets and the media padding will be applied. - final EdgeInsets minimum; - - /// Specifies whether the [SafeArea] should maintain the bottom - /// [MediaQueryData.viewPadding] instead of the bottom - /// [MediaQueryData.padding], defaults to false. - /// - /// For example, if there is an onscreen keyboard displayed above the - /// SafeArea, the padding can be maintained below the obstruction rather than - /// being consumed. This can be helpful in cases where your layout contains - /// flexible widgets, which could visibly move when opening a software - /// keyboard due to the change in the padding value. Setting this to true will - /// avoid the UI shift. - final bool maintainBottomViewPadding; - - /// The widget below this widget in the tree. - /// - /// The padding on the [MediaQuery] for the [child] will be suitably adjusted - /// to zero out any sides that were avoided by this widget. - /// - /// {@macro flutter.widgets.ProxyWidget.child} - final Widget child; - - @override - Widget build(BuildContext context) { - final nativeOrientation = - NativeDeviceOrientationReader.orientation(context); - - late final bool realLeft; - late final bool realRight; - late final bool realTop; - late final bool realBottom; - - switch (nativeOrientation) { - case NativeDeviceOrientation.unknown: - case NativeDeviceOrientation.portraitUp: - realLeft = left; - realRight = right; - realTop = top; - realBottom = bottom; - case NativeDeviceOrientation.portraitDown: - realLeft = right; - realRight = left; - realTop = bottom; - realBottom = top; - case NativeDeviceOrientation.landscapeRight: - realLeft = bottom; - realRight = top; - realTop = left; - realBottom = right; - case NativeDeviceOrientation.landscapeLeft: - realLeft = top; - realRight = bottom; - realTop = right; - realBottom = left; - } - - return SafeArea( - left: realLeft, - right: realRight, - top: realTop, - bottom: realBottom, - minimum: minimum, - maintainBottomViewPadding: maintainBottomViewPadding, - child: child); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('left', left)) - ..add(DiagnosticsProperty('top', top)) - ..add(DiagnosticsProperty('right', right)) - ..add(DiagnosticsProperty('bottom', bottom)) - ..add(DiagnosticsProperty('minimum', minimum)) - ..add(DiagnosticsProperty( - 'maintainBottomViewPadding', maintainBottomViewPadding)); - } -} diff --git a/lib/veilid_processor/views/developer.dart b/lib/veilid_processor/views/developer.dart index 1899c34..cb64d3d 100644 --- a/lib/veilid_processor/views/developer.dart +++ b/lib/veilid_processor/views/developer.dart @@ -260,8 +260,7 @@ class _DeveloperPageState extends State { ), body: GestureDetector( onTap: () => FocusScope.of(context).unfocus(), - child: SafeArea( - child: Column(children: [ + child: Column(children: [ Stack(alignment: AlignmentDirectional.center, children: [ Image.asset('assets/images/ellet.png'), TerminalView(globalDebugTerminal, @@ -333,7 +332,7 @@ class _DeveloperPageState extends State { } }, ).paddingAll(4) - ])))); + ]))); } //////////////////////////////////////////////////////////////////////////// diff --git a/pubspec.lock b/pubspec.lock index 93fdce8..529ae6c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -937,14 +937,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.7" - native_device_orientation: - dependency: "direct main" - description: - name: native_device_orientation - sha256: "0c330c068575e4be72cce5968ca479a3f8d5d1e5dfce7d89d5c13a1e943b338c" - url: "https://pub.dev" - source: hosted - version: "2.0.3" nested: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8b4a0a7..5617c1c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,7 +63,6 @@ dependencies: loggy: ^2.0.3 meta: ^1.16.0 mobile_scanner: ^6.0.7 - native_device_orientation: ^2.0.3 package_info_plus: ^8.3.0 pasteboard: ^0.3.0 path: ^1.9.1 From ae841ec42afed4fef448b1041282b4b21840dcf3 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 19 Mar 2025 23:28:09 -0400 Subject: [PATCH 215/270] theming work, revamp contact invitation --- assets/i18n/en.json | 3 +- .../cubits/active_local_account_cubit.dart | 1 - .../per_account_collection_state.dart | 12 +- lib/chat/views/chat_component_widget.dart | 10 +- lib/chat/views/new_chat_bottom_sheet.dart | 31 -- lib/chat/views/no_conversation_widget.dart | 3 +- lib/chat/views/views.dart | 1 - .../views/contact_invitation_item_widget.dart | 2 +- lib/contacts/views/contacts_browser.dart | 324 ++++++++---------- lib/contacts/views/contacts_dialog.dart | 196 ----------- lib/contacts/views/contacts_page.dart | 171 +++++++++ lib/contacts/views/views.dart | 2 +- lib/layout/home/drawer_menu/drawer_menu.dart | 18 +- .../home/drawer_menu/menu_item_widget.dart | 2 + lib/layout/home/home_account_ready.dart | 27 +- lib/layout/home/home_screen.dart | 14 +- lib/router/cubits/router_cubit.dart | 52 ++- lib/theme/models/chat_theme.dart | 8 +- lib/theme/models/contrast_generator.dart | 47 ++- .../models/scale_theme/scale_scheme.dart | 2 +- lib/theme/models/scale_theme/scale_theme.dart | 66 +++- lib/theme/views/pop_control.dart | 3 +- lib/theme/views/styled_scaffold.dart | 5 +- lib/tools/window_control.dart | 1 - pubspec.lock | 4 +- pubspec.yaml | 6 +- 26 files changed, 504 insertions(+), 507 deletions(-) delete mode 100644 lib/chat/views/new_chat_bottom_sheet.dart delete mode 100644 lib/contacts/views/contacts_dialog.dart create mode 100644 lib/contacts/views/contacts_page.dart diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 61bb802..9192851 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -158,9 +158,8 @@ "away": "Away" }, "add_contact_sheet": { - "new_contact": "New Contact", + "add_contact": "Add Contact", "create_invite": "Create\nInvitation", - "receive_invite": "Receive\nInvitation", "scan_invite": "Scan\nInvitation", "paste_invite": "Paste\nInvitation" }, diff --git a/lib/account_manager/cubits/active_local_account_cubit.dart b/lib/account_manager/cubits/active_local_account_cubit.dart index 58a9cb8..8856848 100644 --- a/lib/account_manager/cubits/active_local_account_cubit.dart +++ b/lib/account_manager/cubits/active_local_account_cubit.dart @@ -14,7 +14,6 @@ class ActiveLocalAccountCubit extends Cubit { switch (change) { case AccountRepositoryChange.activeLocalAccount: emit(_accountRepository.getActiveLocalAccount()); - break; // Ignore these case AccountRepositoryChange.localAccounts: case AccountRepositoryChange.userLogins: diff --git a/lib/account_manager/models/per_account_collection_state/per_account_collection_state.dart b/lib/account_manager/models/per_account_collection_state/per_account_collection_state.dart index 7fc8f0d..9e0a6f0 100644 --- a/lib/account_manager/models/per_account_collection_state/per_account_collection_state.dart +++ b/lib/account_manager/models/per_account_collection_state/per_account_collection_state.dart @@ -32,6 +32,7 @@ class PerAccountCollectionState with _$PerAccountCollectionState { } extension PerAccountCollectionStateExt on PerAccountCollectionState { + // Returns if the account is ready and logged in bool get isReady => avAccountRecordState != null && avAccountRecordState!.isData && @@ -45,7 +46,11 @@ extension PerAccountCollectionStateExt on PerAccountCollectionState { activeConversationsBlocMapCubit != null && activeSingleContactChatBlocMapCubit != null; - Widget provide({required Widget child}) => MultiBlocProvider(providers: [ + /// If we have a selected account and it is ready and not locked, + /// this will provide the unlocked account's cubits to the context + Widget provideReady({required Widget child}) { + if (isReady) { + return MultiBlocProvider(providers: [ BlocProvider.value(value: accountInfoCubit!), BlocProvider.value(value: accountRecordCubit!), BlocProvider.value(value: contactInvitationListCubit!), @@ -56,4 +61,9 @@ extension PerAccountCollectionStateExt on PerAccountCollectionState { BlocProvider.value(value: activeConversationsBlocMapCubit!), BlocProvider.value(value: activeSingleContactChatBlocMapCubit!), ], child: child); + } else { + // Otherwise we just provide the child + return child; + } + } } diff --git a/lib/chat/views/chat_component_widget.dart b/lib/chat/views/chat_component_widget.dart index 8e43299..f41ba47 100644 --- a/lib/chat/views/chat_component_widget.dart +++ b/lib/chat/views/chat_component_widget.dart @@ -33,11 +33,6 @@ class ChatComponentWidget extends StatelessWidget { @override Widget build(BuildContext context) { - // final theme = Theme.of(context); - // final scale = theme.extension()!; - // final scaleConfig = theme.extension()!; - // final textTheme = theme.textTheme; - // Get the account info final accountInfo = context.watch().state; @@ -125,7 +120,7 @@ class ChatComponentWidget extends StatelessWidget { return Column( children: [ Container( - height: 48, + height: 40, decoration: BoxDecoration( color: scale.border, ), @@ -141,9 +136,10 @@ class ChatComponentWidget extends StatelessWidget { )), const Spacer(), IconButton( + iconSize: 24, icon: Icon(Icons.close, color: scale.borderText), onPressed: _onClose) - .paddingLTRB(16, 0, 16, 0) + .paddingLTRB(0, 0, 8, 0) ]), ), DecoratedBox( diff --git a/lib/chat/views/new_chat_bottom_sheet.dart b/lib/chat/views/new_chat_bottom_sheet.dart deleted file mode 100644 index 646a3ec..0000000 --- a/lib/chat/views/new_chat_bottom_sheet.dart +++ /dev/null @@ -1,31 +0,0 @@ -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'; - -Widget newChatBottomSheetBuilder( - BuildContext sheetContext, BuildContext context) { - //final theme = Theme.of(sheetContext); - //final scale = theme.extension()!; - - return KeyboardListener( - focusNode: FocusNode(), - onKeyEvent: (ke) { - if (ke.logicalKey == LogicalKeyboardKey.escape) { - Navigator.pop(sheetContext); - } - }, - child: styledBottomSheet( - context: context, - title: translate('add_chat_sheet.new_chat'), - child: SizedBox( - height: 160, - child: const Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Text( - 'Group and custom chat functionality is not available yet') - ]).paddingAll(16)))); -} diff --git a/lib/chat/views/no_conversation_widget.dart b/lib/chat/views/no_conversation_widget.dart index 13c4cc6..3269bea 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'; import 'package:flutter_translate/flutter_translate.dart'; -import '../../theme/models/scale_theme/scale_scheme.dart'; -import '../../theme/views/views.dart'; +import '../../theme/theme.dart'; class NoConversationWidget extends StatelessWidget { const NoConversationWidget({super.key}); diff --git a/lib/chat/views/views.dart b/lib/chat/views/views.dart index 7e8adce..9703643 100644 --- a/lib/chat/views/views.dart +++ b/lib/chat/views/views.dart @@ -1,4 +1,3 @@ export 'chat_component_widget.dart'; export 'empty_chat_widget.dart'; -export 'new_chat_bottom_sheet.dart'; export 'no_conversation_widget.dart'; diff --git a/lib/contact_invitation/views/contact_invitation_item_widget.dart b/lib/contact_invitation/views/contact_invitation_item_widget.dart index 779f962..544e5db 100644 --- a/lib/contact_invitation/views/contact_invitation_item_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_item_widget.dart @@ -48,7 +48,7 @@ class ContactInvitationItemWidget extends StatelessWidget { key: ObjectKey(contactInvitationRecord), disabled: tileDisabled, selected: selected, - tileScale: ScaleKind.primary, + tileScale: ScaleKind.secondary, title: title, leading: const Icon(Icons.person_add), onTap: () async { diff --git a/lib/contacts/views/contacts_browser.dart b/lib/contacts/views/contacts_browser.dart index c5a6b22..74cd0b5 100644 --- a/lib/contacts/views/contacts_browser.dart +++ b/lib/contacts/views/contacts_browser.dart @@ -17,19 +17,25 @@ import 'contact_item_widget.dart'; import 'empty_contact_list_widget.dart'; enum ContactsBrowserElementKind { - invitation, contact, + invitation, } class ContactsBrowserElement { - ContactsBrowserElement.invitation(proto.ContactInvitationRecord i) - : kind = ContactsBrowserElementKind.invitation, - contact = null, - invitation = i; ContactsBrowserElement.contact(proto.Contact c) : kind = ContactsBrowserElementKind.contact, invitation = null, contact = c; + ContactsBrowserElement.invitation(proto.ContactInvitationRecord i) + : kind = ContactsBrowserElementKind.invitation, + contact = null, + invitation = i; + + String get sortKey => switch (kind) { + ContactsBrowserElementKind.contact => contact!.displayName, + ContactsBrowserElementKind.invitation => + invitation!.recipient + invitation!.message + }; final ContactsBrowserElementKind kind; final proto.ContactInvitationRecord? invitation; @@ -66,27 +72,25 @@ class ContactsBrowser extends StatefulWidget { class _ContactsBrowserState extends State with SingleTickerProviderStateMixin { - Widget buildInvitationBar(BuildContext context) { + Widget buildInvitationButton(BuildContext context) { final theme = Theme.of(context); - final textTheme = theme.textTheme; - final scale = theme.extension()!; + final scaleScheme = theme.extension()!; final scaleConfig = theme.extension()!; final menuIconColor = scaleConfig.preferBorders - ? scale.primaryScale.hoverBorder - : scale.primaryScale.hoverBorder; + ? scaleScheme.primaryScale.hoverBorder + : scaleScheme.primaryScale.hoverBorder; final menuBackgroundColor = scaleConfig.preferBorders - ? scale.primaryScale.elementBackground - : scale.primaryScale.elementBackground; + ? scaleScheme.primaryScale.activeElementBackground + : scaleScheme.primaryScale.activeElementBackground; - final menuBorderColor = scale.primaryScale.hoverBorder; + final menuBorderColor = scaleScheme.primaryScale.hoverBorder; final menuParams = StarMenuParameters( - shape: MenuShape.grid, - checkItemsScreenBoundaries: true, + shape: MenuShape.linear, centerOffset: const Offset(0, 64), - backgroundParams: - BackgroundParams(backgroundColor: theme.shadowColor.withAlpha(128)), + // backgroundParams: + // BackgroundParams(backgroundColor: theme.shadowColor.withAlpha(128)), boundaryBackground: BoundaryBackground( color: menuBackgroundColor, decoration: ShapeDecoration( @@ -99,89 +103,64 @@ class _ContactsBrowserState extends State borderRadius: BorderRadius.circular( 8 * scaleConfig.borderRadiusScale))))); - final receiveInviteMenuItems = [ - Column(mainAxisSize: MainAxisSize.min, children: [ - IconButton( - onPressed: () async { - _receiveInviteMenuController.closeMenu!(); - await ScanInvitationDialog.show(context); - }, - iconSize: 32, - icon: Icon( - Icons.qr_code_scanner, - size: 32, - color: menuIconColor, - ), - ), - Text(translate('add_contact_sheet.scan_invite'), - maxLines: 2, - textAlign: TextAlign.center, - style: textTheme.labelSmall!.copyWith(color: menuIconColor)) - ]).paddingAll(4), - Column(mainAxisSize: MainAxisSize.min, children: [ - IconButton( - onPressed: () async { - _receiveInviteMenuController.closeMenu!(); - await PasteInvitationDialog.show(context); - }, - iconSize: 32, - icon: Icon( - Icons.paste, - size: 32, - color: menuIconColor, - ), - ), - Text(translate('add_contact_sheet.paste_invite'), - maxLines: 2, - textAlign: TextAlign.center, - style: textTheme.labelSmall!.copyWith(color: menuIconColor)) - ]).paddingAll(4) - ]; - - return Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Column(mainAxisSize: MainAxisSize.min, children: [ - IconButton( - onPressed: () async { - await CreateInvitationDialog.show(context); - }, - iconSize: 32, - icon: const Icon(Icons.contact_page), - color: menuIconColor, - ), - Text(translate('add_contact_sheet.create_invite'), - maxLines: 2, - textAlign: TextAlign.center, - style: textTheme.labelSmall!.copyWith(color: menuIconColor)) - ]), - StarMenu( - items: receiveInviteMenuItems, - onItemTapped: (_index, controller) { - controller.closeMenu!(); - }, - controller: _receiveInviteMenuController, - params: menuParams, - child: Column(mainAxisSize: MainAxisSize.min, children: [ - IconButton( - onPressed: () {}, - iconSize: 32, - icon: ImageIcon( - const AssetImage('assets/images/handshake.png'), - size: 32, - color: menuIconColor, - )), - Text(translate('add_contact_sheet.receive_invite'), + ElevatedButton makeMenuButton( + {required IconData iconData, + required String text, + required void Function()? onPressed}) => + ElevatedButton.icon( + onPressed: onPressed, + icon: Icon( + iconData, + size: 32, + ).paddingSTEB(0, 8, 0, 8), + label: Text( + text, maxLines: 2, textAlign: TextAlign.center, - style: textTheme.labelSmall!.copyWith(color: menuIconColor)) - ]), - ), - ]).paddingAll(16); + ).paddingSTEB(0, 8, 0, 8)); + + final inviteMenuItems = [ + makeMenuButton( + iconData: Icons.paste, + text: translate('add_contact_sheet.paste_invite'), + onPressed: () async { + _invitationMenuController.closeMenu!(); + await PasteInvitationDialog.show(context); + }), + makeMenuButton( + iconData: Icons.qr_code_scanner, + text: translate('add_contact_sheet.scan_invite'), + onPressed: () async { + _invitationMenuController.closeMenu!(); + await ScanInvitationDialog.show(context); + }).paddingLTRB(0, 0, 0, 8), + makeMenuButton( + iconData: Icons.contact_page, + text: translate('add_contact_sheet.create_invite'), + onPressed: () async { + _invitationMenuController.closeMenu!(); + await CreateInvitationDialog.show(context); + }).paddingLTRB(0, 0, 0, 8), + ]; + + return StarMenu( + items: inviteMenuItems, + onItemTapped: (_index, controller) { + controller.closeMenu!(); + }, + controller: _invitationMenuController, + params: menuParams, + child: IconButton( + onPressed: () {}, + iconSize: 24, + icon: Icon(Icons.person_add, color: menuIconColor), + tooltip: translate('add_contact_sheet.add_contact')), + ); } @override Widget build(BuildContext context) { final theme = Theme.of(context); - final textTheme = theme.textTheme; final scale = theme.extension()!; //final scaleConfig = theme.extension()!; @@ -196,100 +175,89 @@ class _ContactsBrowserState extends State final contactList = ciState.state.asData?.value.map((x) => x.value).toIList(); - final expansionListData = - >{}; - if (contactInvitationRecordList.isNotEmpty) { - expansionListData[ContactsBrowserElementKind.invitation] = - contactInvitationRecordList - .toList() - .map(ContactsBrowserElement.invitation) - .toList(); - } + final initialList = []; if (contactList != null) { - expansionListData[ContactsBrowserElementKind.contact] = - contactList.toList().map(ContactsBrowserElement.contact).toList(); + initialList + .addAll(contactList.toList().map(ContactsBrowserElement.contact)); } + if (contactInvitationRecordList.isNotEmpty) { + initialList.addAll(contactInvitationRecordList + .toList() + .map(ContactsBrowserElement.invitation)); + } + + initialList.sort((a, b) => a.sortKey.compareTo(b.sortKey)); return Column(children: [ - buildInvitationBar(context), - SearchableList.expansion( - expansionListData: expansionListData, - expansionTitleBuilder: (k) { - final kind = k as ContactsBrowserElementKind; - late final String title; - switch (kind) { - case ContactsBrowserElementKind.contact: - title = translate('contacts_dialog.contacts'); - case ContactsBrowserElementKind.invitation: - title = translate('contacts_dialog.invitations'); - } - - return Center( - child: Text(title, style: textTheme.titleSmall), - ); - }, - expansionInitiallyExpanded: (k) => true, - expansionListBuilder: (_index, element) { - switch (element.kind) { - case ContactsBrowserElementKind.contact: - final contact = element.contact!; - return ContactItemWidget( - contact: contact, - selected: widget.selectedContactRecordKey == - contact.localConversationRecordKey.toVeilid(), - disabled: false, - onDoubleTap: _onStartChat, - onTap: _onSelectContact, - onDelete: _onDeleteContact) - .paddingLTRB(0, 4, 0, 0); - case ContactsBrowserElementKind.invitation: - final invitation = element.invitation!; - return ContactInvitationItemWidget( - contactInvitationRecord: invitation, disabled: false) - .paddingLTRB(0, 4, 0, 0); - } - }, - filterExpansionData: (value) { - final lowerValue = value.toLowerCase(); - final filteredMap = { - for (final entry in expansionListData.entries) - entry.key: (expansionListData[entry.key] ?? []).where((element) { + SearchableList( + initialList: initialList, + itemBuilder: (element) { switch (element.kind) { case ContactsBrowserElementKind.contact: final contact = element.contact!; - return contact.nickname - .toLowerCase() - .contains(lowerValue) || - contact.profile.name - .toLowerCase() - .contains(lowerValue) || - contact.profile.pronouns - .toLowerCase() - .contains(lowerValue); + return ContactItemWidget( + contact: contact, + selected: widget.selectedContactRecordKey == + contact.localConversationRecordKey.toVeilid(), + disabled: false, + onDoubleTap: _onStartChat, + onTap: _onSelectContact, + onDelete: _onDeleteContact) + .paddingLTRB(0, 4, 0, 0); case ContactsBrowserElementKind.invitation: final invitation = element.invitation!; - return invitation.message - .toLowerCase() - .contains(lowerValue) || - invitation.recipient.toLowerCase().contains(lowerValue); + return ContactInvitationItemWidget( + contactInvitationRecord: invitation, + disabled: false) + .paddingLTRB(0, 4, 0, 0); } - }).toList() - }; - return filteredMap; - }, - hideEmptyExpansionItems: true, - searchFieldHeight: 40, - listViewPadding: const EdgeInsets.all(4), - spaceBetweenSearchAndList: 4, - emptyWidget: contactList == null - ? waitingPage(text: translate('contact_list.loading_contacts')) - : const EmptyContactListWidget(), - defaultSuffixIconColor: scale.primaryScale.border, - closeKeyboardWhenScrolling: true, - searchFieldEnabled: contactList != null, - inputDecoration: - InputDecoration(labelText: translate('contact_list.search')), - ).expanded() + }, + filter: (value) { + final lowerValue = value.toLowerCase(); + + final filtered = []; + for (final element in initialList) { + switch (element.kind) { + case ContactsBrowserElementKind.contact: + final contact = element.contact!; + if (contact.nickname.toLowerCase().contains(lowerValue) || + contact.profile.name + .toLowerCase() + .contains(lowerValue) || + contact.profile.pronouns + .toLowerCase() + .contains(lowerValue)) { + filtered.add(element); + } + case ContactsBrowserElementKind.invitation: + final invitation = element.invitation!; + if (invitation.message + .toLowerCase() + .contains(lowerValue) || + invitation.recipient + .toLowerCase() + .contains(lowerValue)) { + filtered.add(element); + } + } + } + return filtered; + }, + searchFieldHeight: 40, + listViewPadding: const EdgeInsets.fromLTRB(4, 0, 4, 4), + searchFieldPadding: const EdgeInsets.fromLTRB(4, 8, 4, 4), + emptyWidget: contactList == null + ? waitingPage( + text: translate('contact_list.loading_contacts')) + : const EmptyContactListWidget(), + defaultSuffixIconColor: scale.primaryScale.border, + closeKeyboardWhenScrolling: true, + searchFieldEnabled: contactList != null, + inputDecoration: + InputDecoration(labelText: translate('contact_list.search')), + secondaryWidget: + buildInvitationButton(context).paddingLTRB(4, 0, 0, 0)) + .expanded() ]); } @@ -318,5 +286,5 @@ class _ContactsBrowserState extends State } //////////////////////////////////////////////////////////////////////////// - final _receiveInviteMenuController = StarMenuController(); + final _invitationMenuController = StarMenuController(); } diff --git a/lib/contacts/views/contacts_dialog.dart b/lib/contacts/views/contacts_dialog.dart deleted file mode 100644 index 721043e..0000000 --- a/lib/contacts/views/contacts_dialog.dart +++ /dev/null @@ -1,196 +0,0 @@ -import 'package:async_tools/async_tools.dart'; -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:provider/provider.dart'; - -import '../../chat/chat.dart'; -import '../../chat_list/chat_list.dart'; -import '../../layout/layout.dart'; -import '../../proto/proto.dart' as proto; -import '../../theme/theme.dart'; -import '../contacts.dart'; - -const _kDoBackArrow = 'doBackArrow'; - -class ContactsDialog extends StatefulWidget { - const ContactsDialog._({required this.modalContext}); - - @override - State createState() => _ContactsDialogState(); - - static Future show(BuildContext modalContext) async { - await showDialog( - context: modalContext, - barrierDismissible: false, - useRootNavigator: false, - builder: (context) => ContactsDialog._(modalContext: modalContext)); - } - - final BuildContext modalContext; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - .add(DiagnosticsProperty('modalContext', modalContext)); - } -} - -class _ContactsDialogState extends State { - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final scale = theme.extension()!; - final appBarIconColor = scale.primaryScale.borderText; - - final enableSplit = !isMobileWidth(context); - final enableLeft = enableSplit || _selectedContact == null; - final enableRight = enableSplit || _selectedContact != null; - - return SizedBox( - width: MediaQuery.of(context).size.width, - child: StyledScaffold( - appBar: DefaultAppBar( - title: Text(!enableSplit && enableRight - ? translate('contacts_dialog.edit_contact') - : translate('contacts_dialog.contacts')), - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () { - singleFuture((this, _kDoBackArrow), () async { - final confirmed = await _onContactSelected(null); - if (!enableSplit && enableRight) { - } else { - if (confirmed) { - if (context.mounted) { - Navigator.pop(context); - } - } - } - }); - }, - ), - actions: [ - if (_selectedContact != null) - FittedBox( - fit: BoxFit.scaleDown, - child: - Column(mainAxisSize: MainAxisSize.min, children: [ - IconButton( - icon: const Icon(Icons.chat_bubble), - color: appBarIconColor, - tooltip: translate('contacts_dialog.new_chat'), - onPressed: () async { - await _onChatStarted(_selectedContact!); - }), - Text(translate('contacts_dialog.new_chat'), - style: theme.textTheme.labelSmall! - .copyWith(color: appBarIconColor)), - ])).paddingLTRB(8, 0, 8, 0), - if (enableSplit && _selectedContact != null) - FittedBox( - fit: BoxFit.scaleDown, - child: - Column(mainAxisSize: MainAxisSize.min, children: [ - IconButton( - icon: const Icon(Icons.close), - color: appBarIconColor, - tooltip: - translate('contacts_dialog.close_contact'), - onPressed: () async { - await _onContactSelected(null); - }), - Text(translate('contacts_dialog.close_contact'), - style: theme.textTheme.labelSmall! - .copyWith(color: appBarIconColor)), - ])).paddingLTRB(8, 0, 8, 0), - ]), - body: LayoutBuilder(builder: (context, constraint) { - final maxWidth = constraint.maxWidth; - - return ColoredBox( - color: scale.primaryScale.appBackground, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Offstage( - offstage: !enableLeft, - child: SizedBox( - width: enableLeft && !enableRight - ? maxWidth - : (maxWidth / 3).clamp(200, 500), - child: DecoratedBox( - decoration: BoxDecoration( - color: scale - .primaryScale.subtleBackground), - child: ContactsBrowser( - selectedContactRecordKey: _selectedContact - ?.localConversationRecordKey - .toVeilid(), - onContactSelected: _onContactSelected, - onStartChat: _onChatStarted, - ).paddingLTRB(8, 0, 8, 8)))), - if (enableRight && enableLeft) - Container( - constraints: const BoxConstraints( - minWidth: 1, maxWidth: 1), - color: scale.primaryScale.subtleBorder), - if (enableRight) - if (_selectedContact == null) - const NoContactWidget().expanded() - else - ContactDetailsWidget( - contact: _selectedContact!, - onModifiedState: _onModifiedState) - .paddingLTRB(16, 16, 16, 16) - .expanded(), - ])); - }))); - } - - void _onModifiedState(bool isModified) { - setState(() { - _isModified = isModified; - }); - } - - Future _onContactSelected(proto.Contact? contact) async { - if (contact != _selectedContact && _isModified) { - final ok = await showConfirmModal( - context: context, - title: translate('confirmation.discard_changes'), - text: translate('confirmation.are_you_sure_discard')); - if (!ok) { - return false; - } - } - setState(() { - _selectedContact = contact; - _isModified = false; - }); - return true; - } - - Future _onChatStarted(proto.Contact contact) async { - final chatListCubit = context.read(); - await chatListCubit.getOrCreateChatSingleContact(contact: contact); - - if (mounted) { - context - .read() - .setActiveChat(contact.localConversationRecordKey.toVeilid()); - - Navigator.pop(context); - } - } - - proto.Contact? _selectedContact; - bool _isModified = false; -} diff --git a/lib/contacts/views/contacts_page.dart b/lib/contacts/views/contacts_page.dart new file mode 100644 index 0000000..28217e6 --- /dev/null +++ b/lib/contacts/views/contacts_page.dart @@ -0,0 +1,171 @@ +import 'package:async_tools/async_tools.dart'; +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:provider/provider.dart'; + +import '../../chat/chat.dart'; +import '../../chat_list/chat_list.dart'; +import '../../layout/layout.dart'; +import '../../proto/proto.dart' as proto; +import '../../theme/theme.dart'; +import '../contacts.dart'; + +const _kDoBackArrow = 'doBackArrow'; + +class ContactsPage extends StatefulWidget { + const ContactsPage({super.key}); + + @override + State createState() => _ContactsPageState(); +} + +class _ContactsPageState extends State { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final appBarIconColor = scale.primaryScale.borderText; + + final enableSplit = !isMobileSize(context); + final enableLeft = enableSplit || _selectedContact == null; + final enableRight = enableSplit || _selectedContact != null; + + return StyledScaffold( + appBar: DefaultAppBar( + title: Text(!enableSplit && enableRight + ? translate('contacts_dialog.edit_contact') + : translate('contacts_dialog.contacts')), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + singleFuture((this, _kDoBackArrow), () async { + final confirmed = await _onContactSelected(null); + if (!enableSplit && enableRight) { + } else { + if (confirmed) { + if (context.mounted) { + Navigator.pop(context); + } + } + } + }); + }, + ), + actions: [ + if (_selectedContact != null) + FittedBox( + fit: BoxFit.scaleDown, + child: Column(mainAxisSize: MainAxisSize.min, children: [ + IconButton( + icon: const Icon(Icons.chat_bubble), + color: appBarIconColor, + tooltip: translate('contacts_dialog.new_chat'), + onPressed: () async { + await _onChatStarted(_selectedContact!); + }), + Text(translate('contacts_dialog.new_chat'), + style: theme.textTheme.labelSmall! + .copyWith(color: appBarIconColor)), + ])).paddingLTRB(8, 0, 8, 0), + if (enableSplit && _selectedContact != null) + FittedBox( + fit: BoxFit.scaleDown, + child: Column(mainAxisSize: MainAxisSize.min, children: [ + IconButton( + icon: const Icon(Icons.close), + color: appBarIconColor, + tooltip: translate('contacts_dialog.close_contact'), + onPressed: () async { + await _onContactSelected(null); + }), + Text(translate('contacts_dialog.close_contact'), + style: theme.textTheme.labelSmall! + .copyWith(color: appBarIconColor)), + ])).paddingLTRB(8, 0, 8, 0), + ]), + body: LayoutBuilder(builder: (context, constraint) { + final maxWidth = constraint.maxWidth; + + return ColoredBox( + color: scale.primaryScale.appBackground, + child: + Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Offstage( + offstage: !enableLeft, + child: SizedBox( + width: enableLeft && !enableRight + ? maxWidth + : (maxWidth / 3).clamp(200, 500), + child: DecoratedBox( + decoration: BoxDecoration( + color: scale.primaryScale.subtleBackground), + child: ContactsBrowser( + selectedContactRecordKey: _selectedContact + ?.localConversationRecordKey + .toVeilid(), + onContactSelected: _onContactSelected, + onStartChat: _onChatStarted, + ).paddingLTRB(8, 0, 8, 8)))), + if (enableRight && enableLeft) + Container( + constraints: + const BoxConstraints(minWidth: 1, maxWidth: 1), + color: scale.primaryScale.subtleBorder), + if (enableRight) + if (_selectedContact == null) + const NoContactWidget().expanded() + else + ContactDetailsWidget( + contact: _selectedContact!, + onModifiedState: _onModifiedState) + .paddingLTRB(16, 16, 16, 16) + .expanded(), + ])); + })); + } + + void _onModifiedState(bool isModified) { + setState(() { + _isModified = isModified; + }); + } + + Future _onContactSelected(proto.Contact? contact) async { + if (contact != _selectedContact && _isModified) { + final ok = await showConfirmModal( + context: context, + title: translate('confirmation.discard_changes'), + text: translate('confirmation.are_you_sure_discard')); + if (!ok) { + return false; + } + } + setState(() { + _selectedContact = contact; + _isModified = false; + }); + return true; + } + + Future _onChatStarted(proto.Contact contact) async { + final chatListCubit = context.read(); + await chatListCubit.getOrCreateChatSingleContact(contact: contact); + + if (mounted) { + context + .read() + .setActiveChat(contact.localConversationRecordKey.toVeilid()); + Navigator.pop(context); + } + } + + proto.Contact? _selectedContact; + bool _isModified = false; +} diff --git a/lib/contacts/views/views.dart b/lib/contacts/views/views.dart index 55b96d0..b74aff3 100644 --- a/lib/contacts/views/views.dart +++ b/lib/contacts/views/views.dart @@ -2,7 +2,7 @@ export 'availability_widget.dart'; export 'contact_details_widget.dart'; export 'contact_item_widget.dart'; export 'contacts_browser.dart'; -export 'contacts_dialog.dart'; +export 'contacts_page.dart'; export 'edit_contact_form.dart'; export 'empty_contact_list_widget.dart'; export 'no_contact_widget.dart'; diff --git a/lib/layout/home/drawer_menu/drawer_menu.dart b/lib/layout/home/drawer_menu/drawer_menu.dart index 6aa817f..779609c 100644 --- a/lib/layout/home/drawer_menu/drawer_menu.dart +++ b/lib/layout/home/drawer_menu/drawer_menu.dart @@ -84,8 +84,9 @@ class _DrawerMenuState extends State { hoverBorder = border; activeBorder = border; } else { - background = - selected ? scale.activeElementBackground : scale.elementBackground; + background = selected + ? scale.elementBackground + : scale.elementBackground.withAlpha(128); hoverBackground = scale.hoverElementBackground; activeBackground = scale.activeElementBackground; border = loggedIn ? scale.border : scale.subtleBorder; @@ -132,9 +133,16 @@ class _DrawerMenuState extends State { callback: callback, footerButtonIcon: loggedIn ? Icons.edit_outlined : null, footerCallback: footerCallback, - footerButtonIconColor: border, - footerButtonIconHoverColor: hoverBackground, - footerButtonIconFocusColor: activeBackground, + footerButtonIconColor: + scaleConfig.preferBorders ? scale.border : scale.borderText, + footerButtonIconHoverColor: + (scaleConfig.preferBorders || scaleConfig.useVisualIndicators) + ? null + : hoverBorder, + footerButtonIconFocusColor: + (scaleConfig.preferBorders || scaleConfig.useVisualIndicators) + ? null + : activeBorder, minHeight: 48, )); } diff --git a/lib/layout/home/drawer_menu/menu_item_widget.dart b/lib/layout/home/drawer_menu/menu_item_widget.dart index a786010..1255458 100644 --- a/lib/layout/home/drawer_menu/menu_item_widget.dart +++ b/lib/layout/home/drawer_menu/menu_item_widget.dart @@ -39,6 +39,8 @@ class MenuItemWidget extends StatelessWidget { } return backgroundColor; }), + overlayColor: + WidgetStateProperty.resolveWith((states) => backgroundHoverColor), side: WidgetStateBorderSide.resolveWith((states) { if (states.contains(WidgetState.hovered)) { return borderColor != null diff --git a/lib/layout/home/home_account_ready.dart b/lib/layout/home/home_account_ready.dart index 9109b78..5f90c9f 100644 --- a/lib/layout/home/home_account_ready.dart +++ b/lib/layout/home/home_account_ready.dart @@ -89,7 +89,11 @@ class _HomeAccountReadyState extends State { )), tooltip: translate('menu.contacts_tooltip'), onPressed: () async { - await ContactsDialog.show(context); + await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const ContactsPage(), + ), + ); }); }); @@ -139,10 +143,7 @@ class _HomeAccountReadyState extends State { @override Widget build(BuildContext context) { - final isLarge = responsiveVisibility( - context: context, - phone: false, - ); + final isSmallScreen = isMobileSize(context); final theme = Theme.of(context); final scaleScheme = theme.extension()!; @@ -159,14 +160,7 @@ class _HomeAccountReadyState extends State { late final bool visibleRight; late final double leftWidth; late final double rightWidth; - if (isLarge) { - visibleLeft = true; - visibleRight = true; - leftWidth = leftColumnSize; - rightWidth = constraints.maxWidth - - leftColumnSize - - (scaleConfig.useVisualIndicators ? 2 : 0); - } else { + if (isSmallScreen) { if (hasActiveChat) { visibleLeft = false; visibleRight = true; @@ -178,6 +172,13 @@ class _HomeAccountReadyState extends State { leftWidth = constraints.maxWidth; rightWidth = 400; // whatever } + } else { + visibleLeft = true; + visibleRight = true; + leftWidth = leftColumnSize; + rightWidth = constraints.maxWidth - + leftColumnSize - + (scaleConfig.useVisualIndicators ? 2 : 0); } return Row(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index 63e2f19..a534415 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -136,15 +136,11 @@ class HomeScreenState extends State } // Re-export all ready blocs to the account display subtree - return perAccountCollectionState.provide( - child: Navigator( - onPopPage: (route, result) { - if (!route.didPop(result)) { - return false; - } - return true; - }, - pages: const [MaterialPage(child: HomeAccountReady())])); + final pages = >[ + const MaterialPage(child: HomeAccountReady()) + ]; + return perAccountCollectionState.provideReady( + child: Navigator(onDidRemovePage: pages.remove, pages: pages)); } } diff --git a/lib/router/cubits/router_cubit.dart b/lib/router/cubits/router_cubit.dart index 48bf95f..6684c45 100644 --- a/lib/router/cubits/router_cubit.dart +++ b/lib/router/cubits/router_cubit.dart @@ -65,29 +65,49 @@ class RouterCubit extends Cubit { ), GoRoute( path: '/edit_account', + redirect: (_, state) { + final extra = state.extra; + if (extra == null || + extra is! List || + extra[0] is! TypedKey) { + return '/'; + } + return null; + }, builder: (context, state) { - final extra = state.extra! as List; + final extra = state.extra! as List; return EditAccountPage( - superIdentityRecordKey: extra[0]! as TypedKey, - initialValue: extra[1]! as AccountSpec, - accountRecord: extra[2]! as OwnedDHTRecordPointer, + superIdentityRecordKey: extra[0] as TypedKey, + initialValue: extra[1] as AccountSpec, + accountRecord: extra[2] as OwnedDHTRecordPointer, ); }, ), GoRoute( - path: '/new_account', - builder: (context, state) => const NewAccountPage(), - ), - GoRoute( - path: '/new_account/recovery_key', - builder: (context, state) { - final extra = state.extra! as List; + path: '/new_account', + builder: (context, state) => const NewAccountPage(), + routes: [ + GoRoute( + path: 'recovery_key', + redirect: (_, state) { + final extra = state.extra; + if (extra == null || + extra is! List || + extra[0] is! WritableSuperIdentity || + extra[1] is! String) { + return '/'; + } + return null; + }, + builder: (context, state) { + final extra = state.extra! as List; - return ShowRecoveryKeyPage( - writableSuperIdentity: - extra[0]! as WritableSuperIdentity, - name: extra[1]! as String); - }), + return ShowRecoveryKeyPage( + writableSuperIdentity: + extra[0] as WritableSuperIdentity, + name: extra[1] as String); + }), + ]), GoRoute( path: '/settings', builder: (context, state) => const SettingsPage(), diff --git a/lib/theme/models/chat_theme.dart b/lib/theme/models/chat_theme.dart index 5552979..b650e17 100644 --- a/lib/theme/models/chat_theme.dart +++ b/lib/theme/models/chat_theme.dart @@ -16,7 +16,7 @@ ChatTheme makeChatTheme( : scale.secondaryScale.calloutBackground, backgroundColor: scale.grayScale.appBackground.withAlpha(scaleConfig.wallpaperAlpha), - messageBorderRadius: scaleConfig.borderRadiusScale * 16, + messageBorderRadius: scaleConfig.borderRadiusScale * 12, bubbleBorderSide: scaleConfig.preferBorders ? BorderSide( color: scale.primaryScale.calloutBackground, @@ -37,7 +37,7 @@ ChatTheme makeChatTheme( filled: !scaleConfig.preferBorders, fillColor: scale.primaryScale.subtleBackground, isDense: true, - contentPadding: const EdgeInsets.fromLTRB(8, 12, 8, 12), + contentPadding: const EdgeInsets.fromLTRB(8, 8, 8, 8), disabledBorder: OutlineInputBorder( borderSide: scaleConfig.preferBorders ? BorderSide(color: scale.grayScale.border, width: 2) @@ -65,10 +65,12 @@ ChatTheme makeChatTheme( color: scaleConfig.preferBorders ? scale.primaryScale.elementBackground : scale.primaryScale.border), - inputPadding: const EdgeInsets.all(12), + inputPadding: const EdgeInsets.all(6), inputTextColor: !scaleConfig.preferBorders ? scale.primaryScale.appText : scale.primaryScale.border, + messageInsetsHorizontal: 12, + messageInsetsVertical: 8, attachmentButtonIcon: const Icon(Icons.attach_file), sentMessageBodyTextStyle: textTheme.bodyLarge!.copyWith( color: scaleConfig.preferBorders diff --git a/lib/theme/models/contrast_generator.dart b/lib/theme/models/contrast_generator.dart index 861b052..314e28a 100644 --- a/lib/theme/models/contrast_generator.dart +++ b/lib/theme/models/contrast_generator.dart @@ -263,6 +263,36 @@ ThemeData contrastGenerator({ final baseThemeData = scaleTheme.toThemeData(brightness); + WidgetStateProperty elementBorderWidgetStateProperty() => + WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.disabled)) { + return BorderSide(color: scheme.grayScale.border.withAlpha(0x7F)); + } else if (states.contains(WidgetState.pressed)) { + return BorderSide( + color: scheme.primaryScale.border, + strokeAlign: BorderSide.strokeAlignOutside); + } else if (states.contains(WidgetState.hovered)) { + return BorderSide(color: scheme.primaryScale.hoverBorder, width: 2); + } else if (states.contains(WidgetState.focused)) { + return BorderSide(color: scheme.primaryScale.hoverBorder, width: 2); + } + return BorderSide(color: scheme.primaryScale.border); + }); + + WidgetStateProperty elementBackgroundWidgetStateProperty() => + WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.disabled)) { + return scheme.grayScale.elementBackground; + } else if (states.contains(WidgetState.pressed)) { + return scheme.primaryScale.activeElementBackground; + } else if (states.contains(WidgetState.hovered)) { + return scheme.primaryScale.hoverElementBackground; + } else if (states.contains(WidgetState.focused)) { + return scheme.primaryScale.activeElementBackground; + } + return scheme.primaryScale.elementBackground; + }); + final elevatedButtonTheme = ElevatedButtonThemeData( style: ElevatedButton.styleFrom( backgroundColor: scheme.primaryScale.elementBackground, @@ -274,20 +304,9 @@ ThemeData contrastGenerator({ side: BorderSide(color: scheme.primaryScale.border), borderRadius: BorderRadius.circular(8 * scaleConfig.borderRadiusScale))) - .copyWith(side: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.disabled)) { - return BorderSide(color: scheme.grayScale.border.withAlpha(0x7F)); - } else if (states.contains(WidgetState.pressed)) { - return BorderSide( - color: scheme.primaryScale.border, - strokeAlign: BorderSide.strokeAlignOutside); - } else if (states.contains(WidgetState.hovered)) { - return BorderSide(color: scheme.primaryScale.hoverBorder); - } else if (states.contains(WidgetState.focused)) { - return BorderSide(color: scheme.primaryScale.hoverBorder, width: 2); - } - return BorderSide(color: scheme.primaryScale.border); - }))); + .copyWith( + side: elementBorderWidgetStateProperty(), + backgroundColor: elementBackgroundWidgetStateProperty())); final themeData = baseThemeData.copyWith( // chipTheme: baseThemeData.chipTheme.copyWith( diff --git a/lib/theme/models/scale_theme/scale_scheme.dart b/lib/theme/models/scale_theme/scale_scheme.dart index 6bdae25..dd88b4f 100644 --- a/lib/theme/models/scale_theme/scale_scheme.dart +++ b/lib/theme/models/scale_theme/scale_scheme.dart @@ -97,7 +97,7 @@ class ScaleScheme extends ThemeExtension { onSurfaceVariant: secondaryScale.appText, outline: primaryScale.border, outlineVariant: secondaryScale.border, - shadow: primaryScale.appBackground.darken(60), + shadow: primaryScale.primary.darken(60), //scrim: primaryScale.background, // inverseSurface: primaryScale.subtleText, // onInverseSurface: primaryScale.subtleBackground, diff --git a/lib/theme/models/scale_theme/scale_theme.dart b/lib/theme/models/scale_theme/scale_theme.dart index 4bfc438..e787c0e 100644 --- a/lib/theme/models/scale_theme/scale_theme.dart +++ b/lib/theme/models/scale_theme/scale_theme.dart @@ -43,6 +43,50 @@ class ScaleTheme extends ThemeExtension { config: config.lerp(other.config, t)); } + WidgetStateProperty elementBorderWidgetStateProperty() => + WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.disabled)) { + return BorderSide( + color: scheme.grayScale.border.withAlpha(0x7F), + strokeAlign: BorderSide.strokeAlignOutside); + } else if (states.contains(WidgetState.pressed)) { + return BorderSide( + color: scheme.primaryScale.border, + ); + } else if (states.contains(WidgetState.hovered)) { + return BorderSide( + color: scheme.primaryScale.hoverBorder, + strokeAlign: BorderSide.strokeAlignOutside); + } else if (states.contains(WidgetState.focused)) { + return BorderSide( + color: scheme.primaryScale.hoverBorder, + width: 2, + strokeAlign: BorderSide.strokeAlignOutside); + } + return BorderSide( + color: scheme.primaryScale.border, + strokeAlign: BorderSide.strokeAlignOutside); + }); + + WidgetStateProperty elementColorWidgetStateProperty() => + WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.disabled)) { + return scheme.grayScale.primary.withAlpha(0x7F); + } else if (states.contains(WidgetState.pressed)) { + return scheme.primaryScale.borderText; + } else if (states.contains(WidgetState.hovered)) { + return scheme.primaryScale.borderText; + } else if (states.contains(WidgetState.focused)) { + return scheme.primaryScale.borderText; + } + return Color.lerp( + scheme.primaryScale.borderText, scheme.primaryScale.primary, 0.25); + }); + + // WidgetStateProperty elementBackgroundWidgetStateProperty() { + // return null; + // } + ThemeData toThemeData(Brightness brightness) { final colorScheme = scheme.toColorScheme(brightness); @@ -51,8 +95,9 @@ class ScaleTheme extends ThemeExtension { final elevatedButtonTheme = ElevatedButtonThemeData( style: ElevatedButton.styleFrom( + elevation: 0, + textStyle: textTheme.labelSmall, backgroundColor: scheme.primaryScale.elementBackground, - foregroundColor: scheme.primaryScale.appText, disabledBackgroundColor: scheme.grayScale.elementBackground.withAlpha(0x7F), disabledForegroundColor: @@ -61,20 +106,11 @@ class ScaleTheme extends ThemeExtension { side: BorderSide(color: scheme.primaryScale.border), borderRadius: BorderRadius.circular(8 * config.borderRadiusScale))) - .copyWith(side: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.disabled)) { - return BorderSide(color: scheme.grayScale.border.withAlpha(0x7F)); - } else if (states.contains(WidgetState.pressed)) { - return BorderSide( - color: scheme.primaryScale.border, - strokeAlign: BorderSide.strokeAlignOutside); - } else if (states.contains(WidgetState.hovered)) { - return BorderSide(color: scheme.primaryScale.hoverBorder); - } else if (states.contains(WidgetState.focused)) { - return BorderSide(color: scheme.primaryScale.hoverBorder, width: 2); - } - return BorderSide(color: scheme.primaryScale.border); - }))); + .copyWith( + foregroundColor: elementColorWidgetStateProperty(), + side: elementBorderWidgetStateProperty(), + iconColor: elementColorWidgetStateProperty(), + )); final themeData = baseThemeData.copyWith( scrollbarTheme: baseThemeData.scrollbarTheme.copyWith( diff --git a/lib/theme/views/pop_control.dart b/lib/theme/views/pop_control.dart index 29dc562..deaf785 100644 --- a/lib/theme/views/pop_control.dart +++ b/lib/theme/views/pop_control.dart @@ -29,11 +29,12 @@ class PopControl extends StatelessWidget { return PopScope( canPop: false, - onPopInvoked: (didPop) { + onPopInvokedWithResult: (didPop, _) { if (didPop) { return; } _doDismiss(navigator); + return; }, child: child); } diff --git a/lib/theme/views/styled_scaffold.dart b/lib/theme/views/styled_scaffold.dart index 9d02fab..4fc803f 100644 --- a/lib/theme/views/styled_scaffold.dart +++ b/lib/theme/views/styled_scaffold.dart @@ -1,4 +1,3 @@ -import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/material.dart'; import '../theme.dart'; @@ -13,7 +12,7 @@ class StyledScaffold extends StatelessWidget { final scaleConfig = theme.extension()!; final scale = scaleScheme.scale(ScaleKind.primary); - final enableBorder = !isMobileSize(context); + const enableBorder = false; //!isMobileSize(context); var scaffold = clipBorder( clipEnabled: enableBorder, @@ -28,7 +27,7 @@ class StyledScaffold extends StatelessWidget { return GestureDetector( onTap: () => FocusManager.instance.primaryFocus?.unfocus(), - child: scaffold.paddingAll(enableBorder ? 32 : 0)); + child: scaffold /*.paddingAll(enableBorder ? 32 : 0) */); } //////////////////////////////////////////////////////////////////////////// diff --git a/lib/tools/window_control.dart b/lib/tools/window_control.dart index 2e9a21f..32c05ff 100644 --- a/lib/tools/window_control.dart +++ b/lib/tools/window_control.dart @@ -7,7 +7,6 @@ import 'package:flutter/services.dart'; import 'package:window_manager/window_manager.dart'; import '../theme/views/responsive.dart'; -import 'tools.dart'; export 'package:window_manager/window_manager.dart' show TitleBarStyle; diff --git a/pubspec.lock b/pubspec.lock index 529ae6c..fa60f63 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1310,10 +1310,10 @@ packages: description: path: "." ref: main - resolved-ref: db0f7b6f1baec0250ecba82f3d161bac1cf23d7d + resolved-ref: f367c2f713dcc0c965a4f7af5952d94b2f699998 url: "https://gitlab.com/veilid/Searchable-Listview.git" source: git - version: "2.14.1" + version: "2.16.0" share_plus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 5617c1c..5206f5d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -111,13 +111,13 @@ dependencies: xterm: ^4.0.0 zxing2: ^0.2.3 -#dependency_overrides: +# dependency_overrides: # async_tools: # path: ../dart_async_tools # bloc_advanced_tools: # path: ../bloc_advanced_tools -# searchable_listview: -# path: ../Searchable-Listview +# searchable_listview: +# path: ../Searchable-Listview # flutter_chat_ui: # path: ../flutter_chat_ui From 23867a1784a549227a75365278280ba19cdc55f0 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 20 Mar 2025 17:31:02 -0400 Subject: [PATCH 216/270] ui cleanup --- assets/images/handshake.png | Bin 43924 -> 0 bytes assets/images/toilet.png | Bin 28325 -> 0 bytes assets/images/toilet.svg | 9 +++ lib/account_manager/views/profile_widget.dart | 6 +- lib/chat_list/views/chat_list_widget.dart | 3 +- .../chat_single_contact_item_widget.dart | 4 +- lib/contacts/views/availability_widget.dart | 57 ++++++++------- lib/contacts/views/contacts_page.dart | 69 ++++++++---------- lib/layout/default_app_bar.dart | 9 ++- lib/layout/home/home_account_ready.dart | 8 +- .../scale_theme/scale_app_bar_theme.dart | 31 ++++++++ lib/theme/models/scale_theme/scale_theme.dart | 7 +- .../models/scale_theme/scale_tile_theme.dart | 1 - lib/theme/views/slider_tile.dart | 4 +- lib/theme/views/wallpaper_preferences.dart | 10 ++- lib/theme/views/widget_helpers.dart | 4 +- pubspec.yaml | 3 +- 17 files changed, 135 insertions(+), 90 deletions(-) delete mode 100644 assets/images/handshake.png delete mode 100644 assets/images/toilet.png create mode 100644 assets/images/toilet.svg create mode 100644 lib/theme/models/scale_theme/scale_app_bar_theme.dart diff --git a/assets/images/handshake.png b/assets/images/handshake.png deleted file mode 100644 index d40fad1c889f016d4a61f59f17350a99b9ef0e31..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43924 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 diff --git a/assets/images/toilet.png b/assets/images/toilet.png deleted file mode 100644 index 6c0efa2a51a8399cf5af75529780da71f17a58d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28325 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 diff --git a/assets/images/toilet.svg b/assets/images/toilet.svg new file mode 100644 index 0000000..0984274 --- /dev/null +++ b/assets/images/toilet.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/lib/account_manager/views/profile_widget.dart b/lib/account_manager/views/profile_widget.dart index 672b13a..c026856 100644 --- a/lib/account_manager/views/profile_widget.dart +++ b/lib/account_manager/views/profile_widget.dart @@ -43,7 +43,7 @@ class ProfileWidget extends StatelessWidget { : scale.primaryScale.borderText, width: 2), borderRadius: BorderRadius.all( - Radius.circular(12 * scaleConfig.borderRadiusScale))), + Radius.circular(8 * scaleConfig.borderRadiusScale))), ), child: Row(children: [ const Spacer(), @@ -54,7 +54,7 @@ class ProfileWidget extends StatelessWidget { ? scale.primaryScale.border : scale.primaryScale.borderText), textAlign: TextAlign.left, - ).paddingAll(12), + ).paddingAll(8), if (_profile.pronouns.isNotEmpty && _showPronouns) Text('(${_profile.pronouns})', textAlign: TextAlign.right, @@ -62,7 +62,7 @@ class ProfileWidget extends StatelessWidget { color: scaleConfig.preferBorders ? scale.primaryScale.border : scale.primaryScale.primary)) - .paddingAll(12), + .paddingAll(8), const Spacer() ]), ); diff --git a/lib/chat_list/views/chat_list_widget.dart b/lib/chat_list/views/chat_list_widget.dart index f9a6ad3..73bd7e6 100644 --- a/lib/chat_list/views/chat_list_widget.dart +++ b/lib/chat_list/views/chat_list_widget.dart @@ -83,7 +83,8 @@ class ChatListWidget extends StatelessWidget { }, filter: (value) => _itemFilter(contactMap, chatList, value), - spaceBetweenSearchAndList: 4, + searchFieldPadding: + const EdgeInsets.fromLTRB(0, 0, 0, 4), inputDecoration: InputDecoration( labelText: translate('chat_list.search'), ), 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 3bdf645..5191fdd 100644 --- a/lib/chat_list/views/chat_single_contact_item_widget.dart +++ b/lib/chat_list/views/chat_single_contact_item_widget.dart @@ -50,7 +50,7 @@ class ChatSingleContactItemWidget extends StatelessWidget { final avatar = AvatarWidget( name: name, - size: 34, + size: 32, borderColor: scaleTheme.config.useVisualIndicators ? scaleTheme.scheme.primaryScale.primaryText : scaleTheme.scheme.primaryScale.subtleBorder, @@ -75,7 +75,7 @@ class ChatSingleContactItemWidget extends StatelessWidget { trailing: AvailabilityWidget( availability: availability, color: scaleTileTheme.textColor, - ), + ).fit(fit: BoxFit.scaleDown), onTap: () { singleFuture(activeChatCubit, () async { activeChatCubit.setActiveChat(_localConversationRecordKey); diff --git a/lib/contacts/views/availability_widget.dart b/lib/contacts/views/availability_widget.dart index a79f774..75ef0a8 100644 --- a/lib/contacts/views/availability_widget.dart +++ b/lib/contacts/views/availability_widget.dart @@ -1,35 +1,39 @@ import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_translate/flutter_translate.dart'; import '../../proto/proto.dart' as proto; +import '../../theme/theme.dart'; class AvailabilityWidget extends StatelessWidget { const AvailabilityWidget( {required this.availability, required this.color, this.vertical = true, - this.iconSize = 24, + this.size = 32, super.key}); static Widget availabilityIcon(proto.Availability availability, Color color, {double size = 24}) { - late final Widget iconData; + late final Widget icon; switch (availability) { case proto.Availability.AVAILABILITY_AWAY: - iconData = ImageIcon(const AssetImage('assets/images/toilet.png'), - size: size, color: color); + icon = SvgPicture.asset('assets/images/toilet.svg', + width: size, + height: size, + colorFilter: ColorFilter.mode(color, BlendMode.srcATop)); case proto.Availability.AVAILABILITY_BUSY: - iconData = Icon(Icons.event_busy, size: size); + icon = Icon(Icons.event_busy, size: size); case proto.Availability.AVAILABILITY_FREE: - iconData = Icon(Icons.event_available, size: size); + icon = Icon(Icons.event_available, size: size); case proto.Availability.AVAILABILITY_OFFLINE: - iconData = Icon(Icons.cloud_off, size: size); + icon = Icon(Icons.cloud_off, size: size); case proto.Availability.AVAILABILITY_UNSPECIFIED: - iconData = Icon(Icons.question_mark, size: size); + icon = Icon(Icons.question_mark, size: size); } - return iconData; + return icon; } static String availabilityName(proto.Availability availability) { @@ -53,26 +57,25 @@ class AvailabilityWidget extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); final textTheme = theme.textTheme; - // final scale = theme.extension()!; - // final scaleConfig = theme.extension()!; final name = availabilityName(availability); - final icon = availabilityIcon(availability, color, size: iconSize); + final icon = availabilityIcon(availability, color, size: size * 2 / 3); return vertical - ? Column( - mainAxisSize: MainAxisSize.min, - //mainAxisAlignment: MainAxisAlignment.center, - children: [ - icon, - Text(name, style: textTheme.labelSmall!.copyWith(color: color)) - .paddingLTRB(0, 0, 0, 0) - ]) - : Row(mainAxisSize: MainAxisSize.min, children: [ - icon, - Text(name, style: textTheme.labelLarge!.copyWith(color: color)) - .paddingLTRB(8, 0, 0, 0) - ]); + ? ConstrainedBox( + constraints: BoxConstraints.tightFor(width: size), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + icon, + Text(name, style: textTheme.labelSmall!.copyWith(color: color)) + .fit(fit: BoxFit.scaleDown) + ])) + : ConstrainedBox( + constraints: BoxConstraints.tightFor(height: size), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + icon, + Text(name, style: textTheme.labelLarge!.copyWith(color: color)) + .paddingLTRB(size / 4, 0, 0, 0) + ])); } //////////////////////////////////////////////////////////////////////////// @@ -80,7 +83,7 @@ class AvailabilityWidget extends StatelessWidget { final proto.Availability availability; final Color color; final bool vertical; - final double iconSize; + final double size; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -89,7 +92,7 @@ class AvailabilityWidget extends StatelessWidget { ..add( DiagnosticsProperty('availability', availability)) ..add(DiagnosticsProperty('vertical', vertical)) - ..add(DoubleProperty('iconSize', iconSize)) + ..add(DoubleProperty('size', size)) ..add(ColorProperty('color', color)); } } diff --git a/lib/contacts/views/contacts_page.dart b/lib/contacts/views/contacts_page.dart index 28217e6..0f1731d 100644 --- a/lib/contacts/views/contacts_page.dart +++ b/lib/contacts/views/contacts_page.dart @@ -1,6 +1,5 @@ import 'package:async_tools/async_tools.dart'; 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:provider/provider.dart'; @@ -30,8 +29,10 @@ class _ContactsPageState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final scale = theme.extension()!; - final appBarIconColor = scale.primaryScale.borderText; + final scaleTheme = theme.extension()!; + final appBarTheme = scaleTheme.appBarTheme(); + final scaleScheme = theme.extension()!; + final scale = scaleScheme.scale(ScaleKind.primary); final enableSplit = !isMobileSize(context); final enableLeft = enableSplit || _selectedContact == null; @@ -39,9 +40,11 @@ class _ContactsPageState extends State { return StyledScaffold( appBar: DefaultAppBar( - title: Text(!enableSplit && enableRight - ? translate('contacts_dialog.edit_contact') - : translate('contacts_dialog.contacts')), + title: Text( + !enableSplit && enableRight + ? translate('contacts_dialog.edit_contact') + : translate('contacts_dialog.contacts'), + ), leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () { @@ -60,41 +63,29 @@ class _ContactsPageState extends State { ), actions: [ if (_selectedContact != null) - FittedBox( - fit: BoxFit.scaleDown, - child: Column(mainAxisSize: MainAxisSize.min, children: [ - IconButton( - icon: const Icon(Icons.chat_bubble), - color: appBarIconColor, - tooltip: translate('contacts_dialog.new_chat'), - onPressed: () async { - await _onChatStarted(_selectedContact!); - }), - Text(translate('contacts_dialog.new_chat'), - style: theme.textTheme.labelSmall! - .copyWith(color: appBarIconColor)), - ])).paddingLTRB(8, 0, 8, 0), + IconButton( + icon: const Icon(Icons.chat_bubble), + iconSize: 24, + color: appBarTheme.iconColor, + tooltip: translate('contacts_dialog.new_chat'), + onPressed: () async { + await _onChatStarted(_selectedContact!); + }).paddingLTRB(8, 0, 8, 0), if (enableSplit && _selectedContact != null) - FittedBox( - fit: BoxFit.scaleDown, - child: Column(mainAxisSize: MainAxisSize.min, children: [ - IconButton( - icon: const Icon(Icons.close), - color: appBarIconColor, - tooltip: translate('contacts_dialog.close_contact'), - onPressed: () async { - await _onContactSelected(null); - }), - Text(translate('contacts_dialog.close_contact'), - style: theme.textTheme.labelSmall! - .copyWith(color: appBarIconColor)), - ])).paddingLTRB(8, 0, 8, 0), + IconButton( + icon: const Icon(Icons.close), + iconSize: 24, + color: appBarTheme.iconColor, + tooltip: translate('contacts_dialog.close_contact'), + onPressed: () async { + await _onContactSelected(null); + }).paddingLTRB(8, 0, 8, 0), ]), body: LayoutBuilder(builder: (context, constraint) { final maxWidth = constraint.maxWidth; return ColoredBox( - color: scale.primaryScale.appBackground, + color: scale.appBackground, child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ Offstage( @@ -104,20 +95,20 @@ class _ContactsPageState extends State { ? maxWidth : (maxWidth / 3).clamp(200, 500), child: DecoratedBox( - decoration: BoxDecoration( - color: scale.primaryScale.subtleBackground), + decoration: + BoxDecoration(color: scale.subtleBackground), child: ContactsBrowser( selectedContactRecordKey: _selectedContact ?.localConversationRecordKey .toVeilid(), onContactSelected: _onContactSelected, onStartChat: _onChatStarted, - ).paddingLTRB(8, 0, 8, 8)))), + ).paddingLTRB(4, 0, 4, 8)))), if (enableRight && enableLeft) Container( constraints: const BoxConstraints(minWidth: 1, maxWidth: 1), - color: scale.primaryScale.subtleBorder), + color: scale.subtleBorder), if (enableRight) if (_selectedContact == null) const NoContactWidget().expanded() diff --git a/lib/layout/default_app_bar.dart b/lib/layout/default_app_bar.dart index fbf2360..b9c0b41 100644 --- a/lib/layout/default_app_bar.dart +++ b/lib/layout/default_app_bar.dart @@ -4,9 +4,12 @@ import 'package:flutter_svg/flutter_svg.dart'; class DefaultAppBar extends AppBar { DefaultAppBar( - {required super.title, super.key, Widget? leading, super.actions}) + {super.title, + super.flexibleSpace, + super.key, + Widget? leading, + super.actions}) : super( - titleSpacing: 0, leading: leading ?? Container( margin: const EdgeInsets.all(4), @@ -14,6 +17,6 @@ class DefaultAppBar extends AppBar { color: Colors.black.withAlpha(32), shape: BoxShape.circle), child: - SvgPicture.asset('assets/images/vlogo.svg', height: 32) + SvgPicture.asset('assets/images/vlogo.svg', height: 24) .paddingAll(4))); } diff --git a/lib/layout/home/home_account_ready.dart b/lib/layout/home/home_account_ready.dart index 5f90c9f..3674a08 100644 --- a/lib/layout/home/home_account_ready.dart +++ b/lib/layout/home/home_account_ready.dart @@ -33,7 +33,7 @@ class _HomeAccountReadyState extends State { color: scaleConfig.preferBorders ? scale.primaryScale.border : scale.primaryScale.borderText, - constraints: const BoxConstraints.expand(height: 48, width: 48), + constraints: const BoxConstraints.expand(height: 40, width: 40), style: ButtonStyle( backgroundColor: WidgetStateProperty.all( scaleConfig.preferBorders @@ -50,7 +50,7 @@ class _HomeAccountReadyState extends State { : scale.primaryScale.borderText, width: 2), borderRadius: BorderRadius.all( - Radius.circular(12 * scaleConfig.borderRadiusScale))), + Radius.circular(8 * scaleConfig.borderRadiusScale))), )), tooltip: translate('menu.accounts_menu_tooltip'), onPressed: () async { @@ -68,7 +68,7 @@ class _HomeAccountReadyState extends State { color: scaleConfig.preferBorders ? scale.primaryScale.border : scale.primaryScale.borderText, - constraints: const BoxConstraints.expand(height: 48, width: 48), + constraints: const BoxConstraints.expand(height: 40, width: 40), style: ButtonStyle( backgroundColor: WidgetStateProperty.all( scaleConfig.preferBorders @@ -85,7 +85,7 @@ class _HomeAccountReadyState extends State { : scale.primaryScale.borderText, width: 2), borderRadius: BorderRadius.all( - Radius.circular(12 * scaleConfig.borderRadiusScale))), + Radius.circular(8 * scaleConfig.borderRadiusScale))), )), tooltip: translate('menu.contacts_tooltip'), onPressed: () async { diff --git a/lib/theme/models/scale_theme/scale_app_bar_theme.dart b/lib/theme/models/scale_theme/scale_app_bar_theme.dart new file mode 100644 index 0000000..ea8e83e --- /dev/null +++ b/lib/theme/models/scale_theme/scale_app_bar_theme.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +import 'scale_theme.dart'; + +class ScaleAppBarTheme { + ScaleAppBarTheme({ + required this.textStyle, + required this.iconColor, + required this.backgroundColor, + }); + + final TextStyle textStyle; + final Color iconColor; + final Color backgroundColor; +} + +extension ScaleAppBarThemeExt on ScaleTheme { + ScaleAppBarTheme appBarTheme({ScaleKind scaleKind = ScaleKind.primary}) { + final scale = scheme.scale(scaleKind); + + final textStyle = textTheme.titleLarge!.copyWith(color: scale.borderText); + final iconColor = scale.borderText; + final backgroundColor = scale.border; + + return ScaleAppBarTheme( + textStyle: textStyle, + iconColor: iconColor, + backgroundColor: backgroundColor, + ); + } +} diff --git a/lib/theme/models/scale_theme/scale_theme.dart b/lib/theme/models/scale_theme/scale_theme.dart index e787c0e..43ca641 100644 --- a/lib/theme/models/scale_theme/scale_theme.dart +++ b/lib/theme/models/scale_theme/scale_theme.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'scale_input_decorator_theme.dart'; import 'scale_scheme.dart'; +export 'scale_app_bar_theme.dart'; export 'scale_color.dart'; export 'scale_input_decorator_theme.dart'; export 'scale_scheme.dart'; @@ -137,8 +138,10 @@ class ScaleTheme extends ThemeExtension { return scheme.primaryScale.subtleBorder; })), appBarTheme: baseThemeData.appBarTheme.copyWith( - backgroundColor: scheme.primaryScale.border, - foregroundColor: scheme.primaryScale.borderText), + backgroundColor: scheme.primaryScale.border, + foregroundColor: scheme.primaryScale.borderText, + toolbarHeight: 40, + ), bottomSheetTheme: baseThemeData.bottomSheetTheme.copyWith( elevation: 0, modalElevation: 0, diff --git a/lib/theme/models/scale_theme/scale_tile_theme.dart b/lib/theme/models/scale_theme/scale_tile_theme.dart index da2c3cd..d549157 100644 --- a/lib/theme/models/scale_theme/scale_tile_theme.dart +++ b/lib/theme/models/scale_theme/scale_tile_theme.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'scale_scheme.dart'; import 'scale_theme.dart'; class ScaleTileTheme { diff --git a/lib/theme/views/slider_tile.dart b/lib/theme/views/slider_tile.dart index d293fa6..8e5f178 100644 --- a/lib/theme/views/slider_tile.dart +++ b/lib/theme/views/slider_tile.dart @@ -125,15 +125,13 @@ class SliderTile extends StatelessWidget { child: ListTile( onTap: onTap, dense: true, - visualDensity: - const VisualDensity(horizontal: -4, vertical: -4), title: Text( title, overflow: TextOverflow.fade, softWrap: false, ), subtitle: subtitle.isNotEmpty ? Text(subtitle) : null, - minTileHeight: 48, + minTileHeight: 52, iconColor: scaleTileTheme.textColor, textColor: scaleTileTheme.textColor, leading: diff --git a/lib/theme/views/wallpaper_preferences.dart b/lib/theme/views/wallpaper_preferences.dart index 48f0a6a..e90022c 100644 --- a/lib/theme/views/wallpaper_preferences.dart +++ b/lib/theme/views/wallpaper_preferences.dart @@ -14,10 +14,18 @@ Widget buildSettingsPageWallpaperPreferences( required ThemeSwitcherState switcher}) { final preferencesRepository = PreferencesRepository.instance; final themePreferences = preferencesRepository.value.themePreference; + final theme = Theme.of(context); + final scale = theme.extension()!; + final textTheme = theme.textTheme; + return FormBuilderCheckbox( name: formFieldEnableWallpaper, - title: Text(translate('settings_page.enable_wallpaper')), + title: Text(translate('settings_page.enable_wallpaper'), + style: textTheme.labelMedium), initialValue: themePreferences.enableWallpaper, + side: BorderSide(color: scale.primaryScale.border, width: 2), + checkColor: scale.primaryScale.borderText, + activeColor: scale.primaryScale.border, onChanged: (value) async { if (value != null) { final newThemePrefs = diff --git a/lib/theme/views/widget_helpers.dart b/lib/theme/views/widget_helpers.dart index 1b768dd..910074e 100644 --- a/lib/theme/views/widget_helpers.dart +++ b/lib/theme/views/widget_helpers.dart @@ -442,7 +442,7 @@ Widget styledTitleContainer({ color: borderColor ?? scale.primaryScale.border, shape: RoundedRectangleBorder( borderRadius: - BorderRadius.circular(12 * scaleConfig.borderRadiusScale), + BorderRadius.circular(8 * scaleConfig.borderRadiusScale), )), child: Column(children: [ Text( @@ -456,7 +456,7 @@ Widget styledTitleContainer({ backgroundColor ?? scale.primaryScale.subtleBackground, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( - 12 * scaleConfig.borderRadiusScale), + 8 * scaleConfig.borderRadiusScale), )), child: child) .paddingAll(4) diff --git a/pubspec.yaml b/pubspec.yaml index 5206f5d..a37a8ab 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -176,10 +176,9 @@ flutter: - assets/images/splash.svg - assets/images/title.svg - assets/images/vlogo.svg + - assets/images/toilet.svg # Raster Images - assets/images/ellet.png - - assets/images/handshake.png - - assets/images/toilet.png # Printing - assets/js/pdf/3.2.146/pdf.min.js # Sounds From 2141dbff214b06da8aea303121b0395fa9c06be6 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 21 Mar 2025 11:33:58 -0400 Subject: [PATCH 217/270] deadlock cleanup --- .../cubits/account_info_cubit.dart | 1 - .../cubits/local_accounts_cubit.dart | 1 - .../cubits/per_account_collection_cubit.dart | 55 ++++++----- .../cubits/user_logins_cubit.dart | 1 - .../views/edit_account_page.dart | 24 +++-- .../views/edit_profile_form.dart | 3 - lib/app.dart | 4 +- lib/chat/views/chat_component_widget.dart | 2 +- lib/chat_list/cubits/chat_list_cubit.dart | 2 - .../views/create_invitation_dialog.dart | 1 + .../views/scan_invitation_dialog.dart | 4 +- lib/contacts/cubits/contact_list_cubit.dart | 6 +- lib/contacts/views/edit_contact_form.dart | 3 - .../views/empty_contact_list_widget.dart | 5 +- .../active_conversations_bloc_map_cubit.dart | 3 +- lib/layout/home/drawer_menu/drawer_menu.dart | 19 ++-- lib/layout/home/home_screen.dart | 3 +- .../views/notifications_preferences.dart | 9 -- lib/theme/models/radix_generator.dart | 2 +- lib/theme/models/scale_theme/scale_color.dart | 12 +-- .../scale_custom_dropdown_theme.dart | 2 +- .../models/scale_theme/scale_scheme.dart | 16 ++-- lib/theme/models/scale_theme/scale_theme.dart | 25 ++++- lib/theme/models/theme_preference.dart | 7 +- lib/theme/views/scanner_error_widget.dart | 4 - lib/theme/views/styled_alert.dart | 9 +- lib/theme/views/wallpaper_preferences.dart | 4 - .../repository/processor_repository.dart | 7 -- .../example/test/widget_test.dart | 5 +- .../lib/dht_support/src/dht_log/dht_log.dart | 38 ++++---- .../src/dht_log/dht_log_spine.dart | 18 ++-- .../src/dht_record/dht_record.dart | 44 ++++----- .../src/dht_record/dht_record_pool.dart | 95 ++++++++++--------- .../src/dht_short_array/dht_short_array.dart | 40 ++++---- .../dht_short_array_cubit.dart | 1 - .../dht_short_array/dht_short_array_head.dart | 9 +- .../dht_short_array_write.dart | 4 +- .../src/interfaces/dht_closeable.dart | 6 +- pubspec.lock | 7 +- pubspec.yaml | 6 +- 40 files changed, 254 insertions(+), 253 deletions(-) diff --git a/lib/account_manager/cubits/account_info_cubit.dart b/lib/account_manager/cubits/account_info_cubit.dart index d9d93fc..a5eab11 100644 --- a/lib/account_manager/cubits/account_info_cubit.dart +++ b/lib/account_manager/cubits/account_info_cubit.dart @@ -23,7 +23,6 @@ class AccountInfoCubit extends Cubit { if (acctInfo != null) { emit(acctInfo); } - break; } }); } diff --git a/lib/account_manager/cubits/local_accounts_cubit.dart b/lib/account_manager/cubits/local_accounts_cubit.dart index 704d8c5..3781297 100644 --- a/lib/account_manager/cubits/local_accounts_cubit.dart +++ b/lib/account_manager/cubits/local_accounts_cubit.dart @@ -20,7 +20,6 @@ class LocalAccountsCubit extends Cubit switch (change) { case AccountRepositoryChange.localAccounts: emit(_accountRepository.getLocalAccounts()); - break; // Ignore these case AccountRepositoryChange.userLogins: case AccountRepositoryChange.activeLocalAccount: diff --git a/lib/account_manager/cubits/per_account_collection_cubit.dart b/lib/account_manager/cubits/per_account_collection_cubit.dart index 089443a..fc2d447 100644 --- a/lib/account_manager/cubits/per_account_collection_cubit.dart +++ b/lib/account_manager/cubits/per_account_collection_cubit.dart @@ -15,6 +15,9 @@ import '../../notifications/notifications.dart'; import '../../proto/proto.dart' as proto; import '../account_manager.dart'; +const _kAccountRecordSubscriptionListenKey = + 'accountRecordSubscriptionListenKey'; + class PerAccountCollectionCubit extends Cubit { PerAccountCollectionCubit({ required Locator locator, @@ -32,6 +35,7 @@ class PerAccountCollectionCubit extends Cubit { await _processor.close(); await accountInfoCubit.close(); await _accountRecordSubscription?.cancel(); + await serialFutureClose((this, _kAccountRecordSubscriptionListenKey)); await accountRecordCubit?.close(); await activeSingleContactChatBlocMapCubitUpdater.close(); @@ -83,7 +87,7 @@ class PerAccountCollectionCubit extends Cubit { accountRecordCubit = null; // Update state to 'loading' - nextState = _updateAccountRecordState(nextState, null); + nextState = await _updateAccountRecordState(nextState, null); emit(nextState); } else { ///////////////// Logged in /////////////////// @@ -95,20 +99,22 @@ class PerAccountCollectionCubit extends Cubit { // Update state to value nextState = - _updateAccountRecordState(nextState, accountRecordCubit!.state); + await _updateAccountRecordState(nextState, accountRecordCubit!.state); emit(nextState); // Subscribe AccountRecordCubit _accountRecordSubscription ??= accountRecordCubit!.stream.listen((avAccountRecordState) { - emit(_updateAccountRecordState(state, avAccountRecordState)); + serialFuture((this, _kAccountRecordSubscriptionListenKey), () async { + emit(await _updateAccountRecordState(state, avAccountRecordState)); + }); }); } } - PerAccountCollectionState _updateAccountRecordState( + Future _updateAccountRecordState( PerAccountCollectionState prevState, - AsyncValue? avAccountRecordState) { + AsyncValue? avAccountRecordState) async { // Get next state final nextState = prevState.copyWith(avAccountRecordState: avAccountRecordState); @@ -121,8 +127,8 @@ class PerAccountCollectionCubit extends Cubit { .avAccountRecordState?.asData?.value.contactInvitationRecords .toVeilid(); - final contactInvitationListCubit = contactInvitationListCubitUpdater.update( - accountInfo.userLogin == null || + final contactInvitationListCubit = await contactInvitationListCubitUpdater + .update(accountInfo.userLogin == null || contactInvitationListRecordPointer == null ? null : (accountInfo, contactInvitationListRecordPointer)); @@ -131,34 +137,35 @@ class PerAccountCollectionCubit extends Cubit { final contactListRecordPointer = nextState.avAccountRecordState?.asData?.value.contactList.toVeilid(); - final contactListCubit = contactListCubitUpdater.update( + final contactListCubit = await contactListCubitUpdater.update( accountInfo.userLogin == null || contactListRecordPointer == null ? null : (accountInfo, contactListRecordPointer)); // WaitingInvitationsBlocMapCubit - final waitingInvitationsBlocMapCubit = waitingInvitationsBlocMapCubitUpdater - .update(accountInfo.userLogin == null || - contactInvitationListCubit == null || - contactListCubit == null - ? null - : ( - accountInfo, - accountRecordCubit!, - contactInvitationListCubit, - contactListCubit, - _locator(), - )); + final waitingInvitationsBlocMapCubit = + await waitingInvitationsBlocMapCubitUpdater.update( + accountInfo.userLogin == null || + contactInvitationListCubit == null || + contactListCubit == null + ? null + : ( + accountInfo, + accountRecordCubit!, + contactInvitationListCubit, + contactListCubit, + _locator(), + )); // ActiveChatCubit - final activeChatCubit = activeChatCubitUpdater + final activeChatCubit = await activeChatCubitUpdater .update((accountInfo.userLogin == null) ? null : true); // ChatListCubit final chatListRecordPointer = nextState.avAccountRecordState?.asData?.value.chatList.toVeilid(); - final chatListCubit = chatListCubitUpdater.update( + final chatListCubit = await chatListCubitUpdater.update( accountInfo.userLogin == null || chatListRecordPointer == null || activeChatCubit == null @@ -167,7 +174,7 @@ class PerAccountCollectionCubit extends Cubit { // ActiveConversationsBlocMapCubit final activeConversationsBlocMapCubit = - activeConversationsBlocMapCubitUpdater.update( + await activeConversationsBlocMapCubitUpdater.update( accountRecordCubit == null || chatListCubit == null || contactListCubit == null @@ -181,7 +188,7 @@ class PerAccountCollectionCubit extends Cubit { // ActiveSingleContactChatBlocMapCubit final activeSingleContactChatBlocMapCubit = - activeSingleContactChatBlocMapCubitUpdater.update( + await activeSingleContactChatBlocMapCubitUpdater.update( accountInfo.userLogin == null || activeConversationsBlocMapCubit == null ? null diff --git a/lib/account_manager/cubits/user_logins_cubit.dart b/lib/account_manager/cubits/user_logins_cubit.dart index 734ced3..5623a34 100644 --- a/lib/account_manager/cubits/user_logins_cubit.dart +++ b/lib/account_manager/cubits/user_logins_cubit.dart @@ -17,7 +17,6 @@ class UserLoginsCubit extends Cubit { switch (change) { case AccountRepositoryChange.userLogins: emit(_accountRepository.getUserLogins()); - break; // Ignore these case AccountRepositoryChange.localAccounts: case AccountRepositoryChange.activeLocalAccount: diff --git a/lib/account_manager/views/edit_account_page.dart b/lib/account_manager/views/edit_account_page.dart index 81b67eb..bd18967 100644 --- a/lib/account_manager/views/edit_account_page.dart +++ b/lib/account_manager/views/edit_account_page.dart @@ -61,6 +61,13 @@ class _EditAccountPageState extends WindowSetupState { ); Future _onRemoveAccount() async { + // dismiss the keyboard by unfocusing the textfield + FocusScope.of(context).unfocus(); + await asyncSleep(const Duration(milliseconds: 250)); + if (!mounted) { + return; + } + final confirmed = await StyledDialog.show( context: context, title: translate('edit_account_page.remove_account_confirm'), @@ -87,10 +94,7 @@ class _EditAccountPageState extends WindowSetupState { ])) ]).paddingAll(24) ])); - if (confirmed != null && confirmed && mounted) { - // dismiss the keyboard by unfocusing the textfield - FocusScope.of(context).unfocus(); - + if (confirmed != null && confirmed) { try { setState(() { _isInAsyncCall = true; @@ -125,6 +129,13 @@ class _EditAccountPageState extends WindowSetupState { } Future _onDestroyAccount() async { + // dismiss the keyboard by unfocusing the textfield + FocusScope.of(context).unfocus(); + await asyncSleep(const Duration(milliseconds: 250)); + if (!mounted) { + return; + } + final confirmed = await StyledDialog.show( context: context, title: translate('edit_account_page.destroy_account_confirm'), @@ -154,10 +165,7 @@ class _EditAccountPageState extends WindowSetupState { ])) ]).paddingAll(24) ])); - if (confirmed != null && confirmed && mounted) { - // dismiss the keyboard by unfocusing the textfield - FocusScope.of(context).unfocus(); - + if (confirmed != null && confirmed) { try { setState(() { _isInAsyncCall = true; diff --git a/lib/account_manager/views/edit_profile_form.dart b/lib/account_manager/views/edit_profile_form.dart index d6bb504..80bd2b3 100644 --- a/lib/account_manager/views/edit_profile_form.dart +++ b/lib/account_manager/views/edit_profile_form.dart @@ -282,9 +282,6 @@ class _EditProfileFormState extends State { FormBuilderCheckbox( name: EditProfileForm.formFieldAutoAway, initialValue: _savedValue.autoAway, - side: BorderSide(color: scale.primaryScale.border, width: 2), - checkColor: scale.primaryScale.borderText, - activeColor: scale.primaryScale.border, title: Text(translate('account.form_auto_away'), style: textTheme.labelMedium), onChanged: (v) { diff --git a/lib/app.dart b/lib/app.dart index 519f2bb..f8241da 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -47,7 +47,7 @@ class VeilidChatApp extends StatelessWidget { final ThemeData initialThemeData; - void _reloadTheme(BuildContext context) { + void reloadTheme(BuildContext context) { singleFuture(this, () async { log.info('Reloading theme'); @@ -95,7 +95,7 @@ class VeilidChatApp extends StatelessWidget { }, child: Actions(actions: >{ ReloadThemeIntent: CallbackAction( - onInvoke: (intent) => _reloadTheme(context)), + onInvoke: (intent) => reloadTheme(context)), AttachDetachIntent: CallbackAction( onInvoke: (intent) => _attachDetach(context)), }, child: Focus(autofocus: true, child: builder(context))))); diff --git a/lib/chat/views/chat_component_widget.dart b/lib/chat/views/chat_component_widget.dart index f41ba47..1646772 100644 --- a/lib/chat/views/chat_component_widget.dart +++ b/lib/chat/views/chat_component_widget.dart @@ -120,7 +120,7 @@ class ChatComponentWidget extends StatelessWidget { return Column( children: [ Container( - height: 40, + height: 48, decoration: BoxDecoration( color: scale.border, ), diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index 6bb88c1..3ae3db1 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -86,14 +86,12 @@ class ChatListCubit extends DHTShortArrayCubit // Nothing to do here return; } - break; case proto.Chat_Kind.group: if (c.group.localConversationRecordKey == contact.localConversationRecordKey) { throw StateError('direct conversation record key should' ' not be used for group chats!'); } - break; case proto.Chat_Kind.notSet: throw StateError('unknown chat kind'); } diff --git a/lib/contact_invitation/views/create_invitation_dialog.dart b/lib/contact_invitation/views/create_invitation_dialog.dart index d835de8..581e8d6 100644 --- a/lib/contact_invitation/views/create_invitation_dialog.dart +++ b/lib/contact_invitation/views/create_invitation_dialog.dart @@ -191,6 +191,7 @@ class _CreateInvitationDialogState extends State { mainAxisSize: MainAxisSize.min, children: [ TextField( + autofocus: true, controller: _recipientTextController, onChanged: (value) { setState(() {}); diff --git a/lib/contact_invitation/views/scan_invitation_dialog.dart b/lib/contact_invitation/views/scan_invitation_dialog.dart index 645640e..fa8bba9 100644 --- a/lib/contact_invitation/views/scan_invitation_dialog.dart +++ b/lib/contact_invitation/views/scan_invitation_dialog.dart @@ -86,7 +86,7 @@ class ScannerOverlay extends CustomPainter { final cutoutPath = Path()..addRect(scanWindow); final backgroundPaint = Paint() - ..color = Colors.black.withOpacity(0.5) + ..color = Colors.black.withAlpha(127) ..style = PaintingStyle.fill ..blendMode = BlendMode.dstOut; @@ -188,7 +188,7 @@ class ScanInvitationDialogState extends State { child: Container( alignment: Alignment.bottomCenter, height: 100, - color: Colors.black.withOpacity(0.4), + color: Colors.black.withAlpha(127), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ diff --git a/lib/contacts/cubits/contact_list_cubit.dart b/lib/contacts/cubits/contact_list_cubit.dart index df70cc0..a0591ad 100644 --- a/lib/contacts/cubits/contact_list_cubit.dart +++ b/lib/contacts/cubits/contact_list_cubit.dart @@ -149,10 +149,14 @@ class ContactListCubit extends DHTShortArrayCubit { // Mark the conversation records for deletion await DHTRecordPool.instance .deleteRecord(deletedItem.localConversationRecordKey.toVeilid()); + } on Exception catch (e) { + log.debug('error deleting local conversation record: $e', e); + } + try { await DHTRecordPool.instance .deleteRecord(deletedItem.remoteConversationRecordKey.toVeilid()); } on Exception catch (e) { - log.debug('error deleting conversation records: $e', e); + log.debug('error deleting remote conversation record: $e', e); } } } diff --git a/lib/contacts/views/edit_contact_form.dart b/lib/contacts/views/edit_contact_form.dart index 5477c60..514f019 100644 --- a/lib/contacts/views/edit_contact_form.dart +++ b/lib/contacts/views/edit_contact_form.dart @@ -195,9 +195,6 @@ class _EditContactFormState extends State { FormBuilderCheckbox( name: EditContactForm.formFieldShowAvailability, initialValue: _savedValue.showAvailability, - side: BorderSide(color: scale.primaryScale.border, width: 2), - checkColor: scale.primaryScale.borderText, - activeColor: scale.primaryScale.border, title: Text(translate('contact_form.form_show_availability'), style: textTheme.labelMedium), ), diff --git a/lib/contacts/views/empty_contact_list_widget.dart b/lib/contacts/views/empty_contact_list_widget.dart index 2563a1d..e6912fd 100644 --- a/lib/contacts/views/empty_contact_list_widget.dart +++ b/lib/contacts/views/empty_contact_list_widget.dart @@ -15,8 +15,7 @@ class EmptyContactListWidget extends StatelessWidget { final textTheme = theme.textTheme; final scale = theme.extension()!; - return Expanded( - child: Column( + return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.center, @@ -35,6 +34,6 @@ class EmptyContactListWidget extends StatelessWidget { ), ), ], - )); + ); } } diff --git a/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart b/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart index fbf0e80..08a249f 100644 --- a/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart +++ b/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart @@ -50,7 +50,7 @@ typedef ActiveConversationsBlocMapState // We currently only build the cubits for the chats that are active, not // archived chats or contacts that are not actively in a chat. // -// TODO: Polling contacts for new inactive chats is yet to be done +// TODO(crioux): Polling contacts for new inactive chats is yet to be done // class ActiveConversationsBlocMapCubit extends BlocMapCubit, ActiveConversationCubit> @@ -166,7 +166,6 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit { // ? grayColorFilter // : null) // .paddingLTRB(0, 0, 16, 0), - SvgPicture.asset( - height: 48, - 'assets/images/title.svg', - colorFilter: scaleConfig.useVisualIndicators - ? grayColorFilter - : src96StencilFilter), + GestureDetector( + onLongPress: () async { + context + .findAncestorWidgetOfExactType()! + .reloadTheme(context); + }, + child: SvgPicture.asset( + height: 48, + 'assets/images/title.svg', + colorFilter: scaleConfig.useVisualIndicators + ? grayColorFilter + : src96StencilFilter)), ]))), Text(translate('menu.accounts'), style: theme.textTheme.titleMedium!.copyWith( diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index a534415..3d1b14f 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -66,10 +66,11 @@ class HomeScreenState extends State final scale = theme.extension()!; final scaleConfig = theme.extension()!; - await showWarningWidgetModal( + await showAlertWidgetModal( context: context, title: translate('splash.beta_title'), child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + const Icon(Icons.warning, size: 64), RichText( textAlign: TextAlign.center, text: TextSpan( diff --git a/lib/notifications/views/notifications_preferences.dart b/lib/notifications/views/notifications_preferences.dart index 95d4a1e..82f3555 100644 --- a/lib/notifications/views/notifications_preferences.dart +++ b/lib/notifications/views/notifications_preferences.dart @@ -129,9 +129,6 @@ Widget buildSettingsPageNotificationPreferences( // Display Beta Warning FormBuilderCheckbox( name: formFieldDisplayBetaWarning, - side: BorderSide(color: scale.primaryScale.border, width: 2), - checkColor: scale.primaryScale.borderText, - activeColor: scale.primaryScale.border, title: Text(translate('settings_page.display_beta_warning'), style: textTheme.labelMedium), initialValue: notificationsPreference.displayBetaWarning, @@ -147,9 +144,6 @@ Widget buildSettingsPageNotificationPreferences( // Enable Badge FormBuilderCheckbox( name: formFieldEnableBadge, - side: BorderSide(color: scale.primaryScale.border, width: 2), - checkColor: scale.primaryScale.borderText, - activeColor: scale.primaryScale.border, title: Text(translate('settings_page.enable_badge'), style: textTheme.labelMedium), initialValue: notificationsPreference.enableBadge, @@ -164,9 +158,6 @@ Widget buildSettingsPageNotificationPreferences( // Enable Notifications FormBuilderCheckbox( name: formFieldEnableNotifications, - side: BorderSide(color: scale.primaryScale.border, width: 2), - checkColor: scale.primaryScale.borderText, - activeColor: scale.primaryScale.border, title: Text(translate('settings_page.enable_notifications'), style: textTheme.labelMedium), initialValue: notificationsPreference.enableNotifications, diff --git a/lib/theme/models/radix_generator.dart b/lib/theme/models/radix_generator.dart index 3e3b0e6..ce05769 100644 --- a/lib/theme/models/radix_generator.dart +++ b/lib/theme/models/radix_generator.dart @@ -638,7 +638,7 @@ ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) { useVisualIndicators: false, preferBorders: false, borderRadiusScale: 1, - wallpaperAlpha: wallpaperAlpha(brightness, themeColor), + wallpaperOpacity: wallpaperAlpha(brightness, themeColor), ); final scaleTheme = ScaleTheme( diff --git a/lib/theme/models/scale_theme/scale_color.dart b/lib/theme/models/scale_theme/scale_color.dart index e50a01b..d79d878 100644 --- a/lib/theme/models/scale_theme/scale_color.dart +++ b/lib/theme/models/scale_theme/scale_color.dart @@ -50,11 +50,11 @@ class ScaleColor { Color? subtleBorder, Color? border, Color? hoverBorder, - Color? background, - Color? hoverBackground, + Color? primary, + Color? hoverPrimary, Color? subtleText, Color? appText, - Color? foregroundText, + Color? primaryText, Color? borderText, Color? dialogBorder, Color? dialogBorderText, @@ -72,11 +72,11 @@ class ScaleColor { subtleBorder: subtleBorder ?? this.subtleBorder, border: border ?? this.border, hoverBorder: hoverBorder ?? this.hoverBorder, - primary: background ?? this.primary, - hoverPrimary: hoverBackground ?? this.hoverPrimary, + primary: primary ?? this.primary, + hoverPrimary: hoverPrimary ?? this.hoverPrimary, subtleText: subtleText ?? this.subtleText, appText: appText ?? this.appText, - primaryText: foregroundText ?? this.primaryText, + primaryText: primaryText ?? this.primaryText, borderText: borderText ?? this.borderText, dialogBorder: dialogBorder ?? this.dialogBorder, dialogBorderText: dialogBorderText ?? this.dialogBorderText, diff --git a/lib/theme/models/scale_theme/scale_custom_dropdown_theme.dart b/lib/theme/models/scale_theme/scale_custom_dropdown_theme.dart index 692ec85..2c5eb1c 100644 --- a/lib/theme/models/scale_theme/scale_custom_dropdown_theme.dart +++ b/lib/theme/models/scale_theme/scale_custom_dropdown_theme.dart @@ -68,7 +68,7 @@ extension ScaleCustomDropdownThemeExt on ScaleTheme { listItemDecoration: null, ); - final disabledDecoration = CustomDropdownDisabledDecoration( + const disabledDecoration = CustomDropdownDisabledDecoration( fillColor: null, shadow: null, suffixIcon: null, diff --git a/lib/theme/models/scale_theme/scale_scheme.dart b/lib/theme/models/scale_theme/scale_scheme.dart index dd88b4f..8363476 100644 --- a/lib/theme/models/scale_theme/scale_scheme.dart +++ b/lib/theme/models/scale_theme/scale_scheme.dart @@ -111,27 +111,27 @@ class ScaleConfig extends ThemeExtension { required this.useVisualIndicators, required this.preferBorders, required this.borderRadiusScale, - required double wallpaperAlpha, - }) : _wallpaperAlpha = wallpaperAlpha; + required this.wallpaperOpacity, + }); final bool useVisualIndicators; final bool preferBorders; final double borderRadiusScale; - final double _wallpaperAlpha; + final double wallpaperOpacity; - int get wallpaperAlpha => _wallpaperAlpha.toInt(); + int get wallpaperAlpha => wallpaperOpacity.toInt(); @override ScaleConfig copyWith( {bool? useVisualIndicators, bool? preferBorders, double? borderRadiusScale, - double? wallpaperAlpha}) => + double? wallpaperOpacity}) => ScaleConfig( useVisualIndicators: useVisualIndicators ?? this.useVisualIndicators, preferBorders: preferBorders ?? this.preferBorders, borderRadiusScale: borderRadiusScale ?? this.borderRadiusScale, - wallpaperAlpha: wallpaperAlpha ?? this._wallpaperAlpha, + wallpaperOpacity: wallpaperOpacity ?? this.wallpaperOpacity, ); @override @@ -145,7 +145,7 @@ class ScaleConfig extends ThemeExtension { preferBorders: t < .5 ? preferBorders : other.preferBorders, borderRadiusScale: lerpDouble(borderRadiusScale, other.borderRadiusScale, t) ?? 1, - wallpaperAlpha: - lerpDouble(_wallpaperAlpha, other._wallpaperAlpha, t) ?? 1); + wallpaperOpacity: + lerpDouble(wallpaperOpacity, other.wallpaperOpacity, t) ?? 1); } } diff --git a/lib/theme/models/scale_theme/scale_theme.dart b/lib/theme/models/scale_theme/scale_theme.dart index 43ca641..ef430d2 100644 --- a/lib/theme/models/scale_theme/scale_theme.dart +++ b/lib/theme/models/scale_theme/scale_theme.dart @@ -84,6 +84,24 @@ class ScaleTheme extends ThemeExtension { scheme.primaryScale.borderText, scheme.primaryScale.primary, 0.25); }); + WidgetStateProperty checkboxFillColorWidgetStateProperty() => + WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + if (states.contains(WidgetState.disabled)) { + return scheme.grayScale.primary.withAlpha(0x7F); + } else if (states.contains(WidgetState.pressed)) { + return scheme.primaryScale.hoverBorder; + } else if (states.contains(WidgetState.hovered)) { + return scheme.primaryScale.hoverBorder; + } else if (states.contains(WidgetState.focused)) { + return scheme.primaryScale.border; + } + return scheme.primaryScale.border; + } else { + return Colors.transparent; + } + }); + // WidgetStateProperty elementBackgroundWidgetStateProperty() { // return null; // } @@ -140,7 +158,7 @@ class ScaleTheme extends ThemeExtension { appBarTheme: baseThemeData.appBarTheme.copyWith( backgroundColor: scheme.primaryScale.border, foregroundColor: scheme.primaryScale.borderText, - toolbarHeight: 40, + toolbarHeight: 48, ), bottomSheetTheme: baseThemeData.bottomSheetTheme.copyWith( elevation: 0, @@ -150,6 +168,11 @@ class ScaleTheme extends ThemeExtension { topLeft: Radius.circular(16 * config.borderRadiusScale), topRight: Radius.circular(16 * config.borderRadiusScale)))), canvasColor: scheme.primaryScale.subtleBackground, + checkboxTheme: baseThemeData.checkboxTheme.copyWith( + side: BorderSide(color: scheme.primaryScale.border, width: 2), + checkColor: elementColorWidgetStateProperty(), + fillColor: checkboxFillColorWidgetStateProperty(), + ), chipTheme: baseThemeData.chipTheme.copyWith( backgroundColor: scheme.primaryScale.elementBackground, selectedColor: scheme.primaryScale.activeElementBackground, diff --git a/lib/theme/models/theme_preference.dart b/lib/theme/models/theme_preference.dart index dc2e082..4be6b4e 100644 --- a/lib/theme/models/theme_preference.dart +++ b/lib/theme/models/theme_preference.dart @@ -1,6 +1,5 @@ import 'package:change_case/change_case.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_svg/svg.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -103,7 +102,7 @@ extension ThemePreferencesExt on ThemePreferences { useVisualIndicators: true, preferBorders: false, borderRadiusScale: 1, - wallpaperAlpha: 255), + wallpaperOpacity: 255), primaryFront: Colors.black, primaryBack: Colors.white, secondaryFront: Colors.black, @@ -123,7 +122,7 @@ extension ThemePreferencesExt on ThemePreferences { useVisualIndicators: true, preferBorders: true, borderRadiusScale: 0.2, - wallpaperAlpha: 208), + wallpaperOpacity: 208), primaryFront: const Color(0xFF000000), primaryBack: const Color(0xFF00FF00), secondaryFront: const Color(0xFF000000), @@ -141,7 +140,7 @@ extension ThemePreferencesExt on ThemePreferences { useVisualIndicators: true, preferBorders: true, borderRadiusScale: 0.2, - wallpaperAlpha: 192), + wallpaperOpacity: 192), primaryFront: const Color(0xFF000000), primaryBack: const Color(0xFF00FF00), secondaryFront: const Color(0xFF000000), diff --git a/lib/theme/views/scanner_error_widget.dart b/lib/theme/views/scanner_error_widget.dart index 0926128..d5463f4 100644 --- a/lib/theme/views/scanner_error_widget.dart +++ b/lib/theme/views/scanner_error_widget.dart @@ -14,16 +14,12 @@ class ScannerErrorWidget extends StatelessWidget { switch (error.errorCode) { case MobileScannerErrorCode.controllerUninitialized: errorMessage = 'Controller not ready.'; - break; case MobileScannerErrorCode.permissionDenied: errorMessage = 'Permission denied'; - break; case MobileScannerErrorCode.unsupported: errorMessage = 'Scanning is unsupported on this device'; - break; default: errorMessage = 'Generic Error'; - break; } return ColoredBox( diff --git a/lib/theme/views/styled_alert.dart b/lib/theme/views/styled_alert.dart index 82218e8..8602c22 100644 --- a/lib/theme/views/styled_alert.dart +++ b/lib/theme/views/styled_alert.dart @@ -11,6 +11,7 @@ AlertStyle _alertStyle(BuildContext context) { return AlertStyle( animationType: AnimationType.grow, + isCloseButton: false, //animationDuration: const Duration(milliseconds: 200), alertBorder: RoundedRectangleBorder( side: !scaleConfig.useVisualIndicators @@ -131,7 +132,7 @@ Future showErrorStacktraceModal( ); } -Future showWarningModal( +Future showAlertModal( {required BuildContext context, required String title, required String text}) async { @@ -139,7 +140,7 @@ Future showWarningModal( context: context, style: _alertStyle(context), useRootNavigator: false, - type: AlertType.warning, + type: AlertType.none, title: title, desc: text, buttons: [ @@ -161,7 +162,7 @@ Future showWarningModal( ).show(); } -Future showWarningWidgetModal( +Future showAlertWidgetModal( {required BuildContext context, required String title, required Widget child}) async { @@ -169,7 +170,7 @@ Future showWarningWidgetModal( context: context, style: _alertStyle(context), useRootNavigator: false, - type: AlertType.warning, + type: AlertType.none, title: title, content: child, buttons: [ diff --git a/lib/theme/views/wallpaper_preferences.dart b/lib/theme/views/wallpaper_preferences.dart index e90022c..f9ae94c 100644 --- a/lib/theme/views/wallpaper_preferences.dart +++ b/lib/theme/views/wallpaper_preferences.dart @@ -15,7 +15,6 @@ Widget buildSettingsPageWallpaperPreferences( final preferencesRepository = PreferencesRepository.instance; final themePreferences = preferencesRepository.value.themePreference; final theme = Theme.of(context); - final scale = theme.extension()!; final textTheme = theme.textTheme; return FormBuilderCheckbox( @@ -23,9 +22,6 @@ Widget buildSettingsPageWallpaperPreferences( title: Text(translate('settings_page.enable_wallpaper'), style: textTheme.labelMedium), initialValue: themePreferences.enableWallpaper, - side: BorderSide(color: scale.primaryScale.border, width: 2), - checkColor: scale.primaryScale.borderText, - activeColor: scale.primaryScale.border, onChanged: (value) async { if (value != null) { final newThemePrefs = diff --git a/lib/veilid_processor/repository/processor_repository.dart b/lib/veilid_processor/repository/processor_repository.dart index 3622219..7ff2482 100644 --- a/lib/veilid_processor/repository/processor_repository.dart +++ b/lib/veilid_processor/repository/processor_repository.dart @@ -44,13 +44,6 @@ class ProcessorRepository { 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) { diff --git a/packages/veilid_support/example/test/widget_test.dart b/packages/veilid_support/example/test/widget_test.dart index 092d222..d5cfa06 100644 --- a/packages/veilid_support/example/test/widget_test.dart +++ b/packages/veilid_support/example/test/widget_test.dart @@ -5,13 +5,12 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. +import 'package:example/main.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:example/main.dart'; - void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { + testWidgets('Counter increments smoke test', (tester) async { // Build our app and trigger a frame. await tester.pumpWidget(const MyApp()); diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart index bba3306..8f88ce1 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart @@ -171,32 +171,31 @@ class DHTLog implements DHTDeleteable { /// Add a reference to this log @override - Future ref() async => _mutex.protect(() async { - _openCount++; - }); + void ref() { + _openCount++; + } /// Free all resources for the DHTLog @override - Future close() async => _mutex.protect(() async { - if (_openCount == 0) { - throw StateError('already closed'); - } - _openCount--; - if (_openCount != 0) { - return false; - } - await _watchController?.close(); - _watchController = null; - await _spine.close(); - return true; - }); + Future close() async { + if (_openCount == 0) { + throw StateError('already closed'); + } + _openCount--; + if (_openCount != 0) { + return false; + } + // + await _watchController?.close(); + _watchController = null; + await _spine.close(); + return true; + } /// Free all resources for the DHTLog and delete it from the DHT /// Will wait until the short array is closed to delete it @override - Future delete() async { - await _spine.delete(); - } + Future delete() => _spine.delete(); //////////////////////////////////////////////////////////////////////////// // Public API @@ -306,7 +305,6 @@ class DHTLog implements DHTDeleteable { // Openable int _openCount; - final _mutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); // Watch mutex to ensure we keep the representation valid final Mutex _listenMutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index 1ea48be..8eff1b6 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -24,13 +24,11 @@ class _DHTLogPosition extends DHTCloseable { /// Add a reference to this log @override - Future ref() async { - await shortArray.ref(); - } + void ref() => shortArray.ref(); /// Free all resources for the DHTLogPosition @override - Future close() async => _dhtLogSpine._segmentClosed(_segmentNumber); + Future close() => _dhtLogSpine._segmentClosed(_segmentNumber); } class _DHTLogSegmentLookup extends Equatable { @@ -124,12 +122,8 @@ class _DHTLogSpine { }); } - Future delete() async { - await _spineMutex.protect(() async { - // Will deep delete all segment records as they are children - await _spineRecord.delete(); - }); - } + // Will deep delete all segment records as they are children + Future delete() async => _spineMutex.protect(_spineRecord.delete); Future operate(Future Function(_DHTLogSpine) closure) async => // ignore: prefer_expression_function_bodies @@ -431,7 +425,7 @@ class _DHTLogSpine { late DHTShortArray shortArray; if (openedSegment != null) { // If so, return a ref - await openedSegment.ref(); + openedSegment.ref(); shortArray = openedSegment; } else { // Otherwise open a segment @@ -453,7 +447,7 @@ class _DHTLogSpine { // LRU cache the segment number if (!_openCache.remove(segmentNumber)) { // If this is new to the cache ref it when it goes in - await shortArray.ref(); + shortArray.ref(); } _openCache.add(segmentNumber); if (_openCache.length > _openCacheSize) { 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 bddb4e7..0a51ba1 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 @@ -64,34 +64,35 @@ class DHTRecord implements DHTDeleteable { /// Add a reference to this DHTRecord @override - Future ref() async => _mutex.protect(() async { - _openCount++; - }); + void ref() { + _openCount++; + } /// Free all resources for the DHTRecord @override - Future close() async => _mutex.protect(() async { - if (_openCount == 0) { - throw StateError('already closed'); - } - _openCount--; - if (_openCount != 0) { - return false; - } + Future close() async { + if (_openCount == 0) { + throw StateError('already closed'); + } + _openCount--; + if (_openCount != 0) { + return false; + } - await serialFutureClose((this, _sfListen)); - await _watchController?.close(); - _watchController = null; - await DHTRecordPool.instance._recordClosed(this); - return true; - }); + await _watchController?.close(); + _watchController = null; + await serialFutureClose((this, _sfListen)); + + await DHTRecordPool.instance._recordClosed(this); + + return true; + } /// Free all resources for the DHTRecord and delete it from the DHT - /// Will wait until the record is closed to delete it + /// Returns true if the deletion was processed immediately + /// Returns false if the deletion was marked for later @override - Future delete() async => _mutex.protect(() async { - await DHTRecordPool.instance.deleteRecord(key); - }); + Future delete() async => DHTRecordPool.instance.deleteRecord(key); //////////////////////////////////////////////////////////////////////////// // Public API @@ -562,7 +563,6 @@ class DHTRecord implements DHTDeleteable { final KeyPair? _writer; final VeilidCrypto _crypto; final String debugName; - final _mutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); int _openCount; StreamController? _watchController; _WatchState? _watchState; 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 3f55687..15c955d 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 @@ -479,25 +479,29 @@ class DHTRecordPool with TableDBBackedJson { // Called when a DHTRecord is closed // Cleans up the opened record housekeeping and processes any late deletions Future _recordClosed(DHTRecord record) async { - await _recordTagLock.protect(record.key, - closure: () => _mutex.protect(() async { - final key = record.key; + final key = record.key; + await _recordTagLock.protect(key, closure: () async { + await _mutex.protect(() async { + log('closeDHTRecord: debugName=${record.debugName} key=$key'); - log('closeDHTRecord: debugName=${record.debugName} key=$key'); + final openedRecordInfo = _opened[key]; + if (openedRecordInfo == null || + !openedRecordInfo.records.remove(record)) { + throw StateError('record already closed'); + } + if (openedRecordInfo.records.isNotEmpty) { + return; + } + _opened.remove(key); + await _routingContext.closeDHTRecord(key); + await _checkForLateDeletesInner(key); + }); - final openedRecordInfo = _opened[key]; - if (openedRecordInfo == null || - !openedRecordInfo.records.remove(record)) { - throw StateError('record already closed'); - } - if (openedRecordInfo.records.isEmpty) { - await _watchStateProcessors.remove(key); - await _routingContext.closeDHTRecord(key); - _opened.remove(key); - - await _checkForLateDeletesInner(key); - } - })); + // This happens after the mutex is released + // because the record has already been removed from _opened + // which means that updates to the state processor won't happen + await _watchStateProcessors.remove(key); + }); } // Check to see if this key can finally be deleted @@ -929,40 +933,37 @@ class DHTRecordPool with TableDBBackedJson { } /// Ticker to check watch state change requests - Future tick() async { - final now = veilid.now(); + Future tick() async => _mutex.protect(() async { + // See if any opened records need watch state changes + final now = veilid.now(); + for (final kv in _opened.entries) { + final openedRecordKey = kv.key; + final openedRecordInfo = kv.value; - await _mutex.protect(() async { - // See if any opened records need watch state changes - for (final kv in _opened.entries) { - final openedRecordKey = kv.key; - final openedRecordInfo = kv.value; + var wantsWatchStateUpdate = + openedRecordInfo.shared.needsWatchStateUpdate; - var wantsWatchStateUpdate = - openedRecordInfo.shared.needsWatchStateUpdate; + // Check if we have reached renewal time for the watch + if (openedRecordInfo.shared.unionWatchState != null && + openedRecordInfo.shared.unionWatchState!.renewalTime != null && + now.value > + openedRecordInfo.shared.unionWatchState!.renewalTime!.value) { + wantsWatchStateUpdate = true; + } - // Check if we have reached renewal time for the watch - if (openedRecordInfo.shared.unionWatchState != null && - openedRecordInfo.shared.unionWatchState!.renewalTime != null && - now.value > - openedRecordInfo.shared.unionWatchState!.renewalTime!.value) { - wantsWatchStateUpdate = true; + if (wantsWatchStateUpdate) { + // Update union watch state + final unionWatchState = + _collectUnionWatchState(openedRecordInfo.records); + + _watchStateProcessors.updateState( + openedRecordKey, + unionWatchState, + (newState) => + _watchStateChange(openedRecordKey, unionWatchState)); + } } - - if (wantsWatchStateUpdate) { - // Update union watch state - final unionWatchState = - _collectUnionWatchState(openedRecordInfo.records); - - _watchStateProcessors.updateState( - openedRecordKey, - unionWatchState, - (newState) => - _watchStateChange(openedRecordKey, unionWatchState)); - } - } - }); - } + }); ////////////////////////////////////////////////////////////// // AsyncTableDBBacked 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 c0ec901..8101a7a 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 @@ -148,33 +148,32 @@ class DHTShortArray implements DHTDeleteable { /// Add a reference to this shortarray @override - Future ref() async => _mutex.protect(() async { - _openCount++; - }); + void ref() { + _openCount++; + } /// Free all resources for the DHTShortArray @override - Future close() async => _mutex.protect(() async { - if (_openCount == 0) { - throw StateError('already closed'); - } - _openCount--; - if (_openCount != 0) { - return false; - } + Future close() async { + if (_openCount == 0) { + throw StateError('already closed'); + } + _openCount--; + if (_openCount != 0) { + return false; + } - await _watchController?.close(); - _watchController = null; - await _head.close(); - return true; - }); + await _watchController?.close(); + _watchController = null; + await _head.close(); + return true; + } /// Free all resources for the DHTShortArray and delete it from the DHT - /// Will wait until the short array is closed to delete it + /// Returns true if the deletion was processed immediately + /// Returns false if the deletion was marked for later @override - Future delete() async { - await _head.delete(); - } + Future delete() async => _head.delete(); //////////////////////////////////////////////////////////////////////////// // Public API @@ -289,7 +288,6 @@ class DHTShortArray implements DHTDeleteable { // Openable int _openCount; - final _mutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); // Watch mutex to ensure we keep the representation valid final Mutex _listenMutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); 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 8bdda7c..ab56c77 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 @@ -8,7 +8,6 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:meta/meta.dart'; import '../../../veilid_support.dart'; -import '../interfaces/refreshable_cubit.dart'; @immutable class DHTShortArrayElementState extends Equatable { 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 4a2c79a..0aaed19 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 @@ -65,12 +65,9 @@ class _DHTShortArrayHead { }); } - Future delete() async { - await _headMutex.protect(() async { - // Will deep delete all linked records as they are children - await _headRecord.delete(); - }); - } + /// Returns true if the deletion was processed immediately + /// Returns false if the deletion was marked for later + Future delete() => _headMutex.protect(_headRecord.delete); Future operate(Future Function(_DHTShortArrayHead) closure) async => // ignore: prefer_expression_function_bodies 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 index c52a7b2..f3e1ac3 100644 --- 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 @@ -40,7 +40,7 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead } } if (!success) { - throw DHTExceptionOutdated(); + throw const DHTExceptionOutdated(); } } @@ -97,7 +97,7 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead } } if (!success) { - throw DHTExceptionOutdated(); + throw const DHTExceptionOutdated(); } } diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_closeable.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_closeable.dart index c913340..0fb10ab 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_closeable.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_closeable.dart @@ -4,7 +4,7 @@ import 'package:meta/meta.dart'; abstract class DHTCloseable { // Public interface - Future ref(); + void ref(); Future close(); // Internal implementation @@ -15,7 +15,9 @@ abstract class DHTCloseable { } abstract class DHTDeleteable extends DHTCloseable { - Future delete(); + /// Returns true if the deletion was processed immediately + /// Returns false if the deletion was marked for later + Future delete(); } extension DHTCloseableExt on DHTCloseable { diff --git a/pubspec.lock b/pubspec.lock index fa60f63..b4df49f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -156,10 +156,9 @@ packages: bloc_advanced_tools: dependency: "direct main" description: - name: bloc_advanced_tools - sha256: "977f3c7e3f9a19aec2f2c734ae99c8f0799c1b78f9fd7e4dce91a2dbf773e11b" - url: "https://pub.dev" - source: hosted + path: "../bloc_advanced_tools" + relative: true + source: path version: "0.1.9" blurry_modal_progress_hud: dependency: "direct main" diff --git a/pubspec.yaml b/pubspec.yaml index a37a8ab..b909a16 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -111,11 +111,11 @@ dependencies: xterm: ^4.0.0 zxing2: ^0.2.3 -# dependency_overrides: +dependency_overrides: # async_tools: # path: ../dart_async_tools -# bloc_advanced_tools: -# path: ../bloc_advanced_tools + bloc_advanced_tools: + path: ../bloc_advanced_tools # searchable_listview: # path: ../Searchable-Listview # flutter_chat_ui: From 739df7c427de01cf7131278597ae9d3bf7248dda Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 21 Mar 2025 14:50:17 -0400 Subject: [PATCH 218/270] dependency upgrades --- packages/veilid_support/example/.gitignore | 2 + .../example/integration_test/app_test.dart | 90 +++++++++---------- .../veilid_support/example/macos/Podfile.lock | 6 +- .../xcshareddata/xcschemes/Runner.xcscheme | 1 + .../example/macos/Runner/AppDelegate.swift | 4 + packages/veilid_support/example/pubspec.lock | 81 +++++++---------- packages/veilid_support/example/pubspec.yaml | 6 +- packages/veilid_support/pubspec.lock | 69 +++++++------- packages/veilid_support/pubspec.yaml | 46 +++++----- pubspec.lock | 83 ++++++++--------- pubspec.yaml | 37 ++++---- 11 files changed, 206 insertions(+), 219 deletions(-) diff --git a/packages/veilid_support/example/.gitignore b/packages/veilid_support/example/.gitignore index 29a3a50..79c113f 100644 --- a/packages/veilid_support/example/.gitignore +++ b/packages/veilid_support/example/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related diff --git a/packages/veilid_support/example/integration_test/app_test.dart b/packages/veilid_support/example/integration_test/app_test.dart index 6912fd3..5dc7acd 100644 --- a/packages/veilid_support/example/integration_test/app_test.dart +++ b/packages/veilid_support/example/integration_test/app_test.dart @@ -36,6 +36,51 @@ void main() { setUpAll(veilidFixture.attach); tearDownAll(veilidFixture.detach); + group('DHT Support Tests', () { + setUpAll(updateProcessorFixture.setUp); + setUpAll(tickerFixture.setUp); + tearDownAll(tickerFixture.tearDown); + tearDownAll(updateProcessorFixture.tearDown); + + test('create pool', testDHTRecordPoolCreate); + + group('DHTRecordPool Tests', () { + setUpAll(dhtRecordPoolFixture.setUp); + tearDownAll(dhtRecordPoolFixture.tearDown); + + test('create/delete record', testDHTRecordCreateDelete); + test('record scopes', testDHTRecordScopes); + test('create/delete deep record', testDHTRecordDeepCreateDelete); + }); + + group('DHTShortArray Tests', () { + setUpAll(dhtRecordPoolFixture.setUp); + tearDownAll(dhtRecordPoolFixture.tearDown); + + for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { + test('create shortarray stride=$stride', + makeTestDHTShortArrayCreateDelete(stride: stride)); + test('add shortarray stride=$stride', + makeTestDHTShortArrayAdd(stride: stride)); + } + }); + + group('DHTLog Tests', () { + setUpAll(dhtRecordPoolFixture.setUp); + tearDownAll(dhtRecordPoolFixture.tearDown); + + for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { + test('create log stride=$stride', + makeTestDHTLogCreateDelete(stride: stride)); + test( + timeout: const Timeout(Duration(seconds: 480)), + 'add/truncate log stride=$stride', + makeTestDHTLogAddTruncate(stride: stride), + ); + } + }); + }); + group('TableDB Tests', () { group('TableDBArray Tests', () { // test('create/delete TableDBArray', testTableDBArrayCreateDelete); @@ -146,51 +191,6 @@ void main() { }); }); }); - - group('DHT Support Tests', () { - setUpAll(updateProcessorFixture.setUp); - setUpAll(tickerFixture.setUp); - tearDownAll(tickerFixture.tearDown); - tearDownAll(updateProcessorFixture.tearDown); - - test('create pool', testDHTRecordPoolCreate); - - group('DHTRecordPool Tests', () { - setUpAll(dhtRecordPoolFixture.setUp); - tearDownAll(dhtRecordPoolFixture.tearDown); - - test('create/delete record', testDHTRecordCreateDelete); - test('record scopes', testDHTRecordScopes); - test('create/delete deep record', testDHTRecordDeepCreateDelete); - }); - - group('DHTShortArray Tests', () { - setUpAll(dhtRecordPoolFixture.setUp); - tearDownAll(dhtRecordPoolFixture.tearDown); - - for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { - test('create shortarray stride=$stride', - makeTestDHTShortArrayCreateDelete(stride: stride)); - test('add shortarray stride=$stride', - makeTestDHTShortArrayAdd(stride: stride)); - } - }); - - group('DHTLog Tests', () { - setUpAll(dhtRecordPoolFixture.setUp); - tearDownAll(dhtRecordPoolFixture.tearDown); - - for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { - test('create log stride=$stride', - makeTestDHTLogCreateDelete(stride: stride)); - test( - timeout: const Timeout(Duration(seconds: 480)), - 'add/truncate log stride=$stride', - makeTestDHTLogAddTruncate(stride: stride), - ); - } - }); - }); }); }); } diff --git a/packages/veilid_support/example/macos/Podfile.lock b/packages/veilid_support/example/macos/Podfile.lock index 6a58494..a2618bd 100644 --- a/packages/veilid_support/example/macos/Podfile.lock +++ b/packages/veilid_support/example/macos/Podfile.lock @@ -21,9 +21,9 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - veilid: a54f57b7bcf0e4e072fe99272d76ca126b2026d0 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + veilid: 319e2e78836d7b3d08203596d0b4a0e244b68d29 PODFILE CHECKSUM: 16208599a12443d53889ba2270a4985981cfb204 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/packages/veilid_support/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/veilid_support/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 15368ec..ac78810 100644 --- a/packages/veilid_support/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/veilid_support/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -59,6 +59,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/packages/veilid_support/example/macos/Runner/AppDelegate.swift b/packages/veilid_support/example/macos/Runner/AppDelegate.swift index 8e02df2..b3c1761 100644 --- a/packages/veilid_support/example/macos/Runner/AppDelegate.swift +++ b/packages/veilid_support/example/macos/Runner/AppDelegate.swift @@ -6,4 +6,8 @@ class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } } diff --git a/packages/veilid_support/example/pubspec.lock b/packages/veilid_support/example/pubspec.lock index 3844db3..6ef291b 100644 --- a/packages/veilid_support/example/pubspec.lock +++ b/packages/veilid_support/example/pubspec.lock @@ -5,31 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 url: "https://pub.dev" source: hosted - version: "76.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.3" + version: "80.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" url: "https://pub.dev" source: hosted - version: "6.11.0" + version: "7.3.0" args: dependency: transitive description: name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" async: dependency: transitive description: @@ -42,26 +37,26 @@ packages: dependency: "direct dev" description: name: async_tools - sha256: bbded696bfcb1437d0ca510ac047f261f9c7494fea2c488dd32ba2800e7f49e8 + sha256: afd5426e76631172f8ce6a6359b264b092fa9d2a52cd2528100115be9525e067 url: "https://pub.dev" source: hosted - version: "0.1.7" + version: "0.1.9" bloc: dependency: transitive description: name: bloc - sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + sha256: "52c10575f4445c61dd9e0cafcc6356fdd827c4c64dd7945ef3c4105f6b6ac189" url: "https://pub.dev" source: hosted - version: "8.1.4" + version: "9.0.0" bloc_advanced_tools: dependency: transitive description: name: bloc_advanced_tools - sha256: d8a680d8a0469456399fb26bae9f7a1d2a1420b5bdf75e204e0fadab9edb0811 + sha256: "7c7f294b425552c2d4831b01ad0d3e1f33f2bdf9acfb7b639caa072781d228cf" url: "https://pub.dev" source: hosted - version: "0.1.8" + version: "0.1.10" boolean_selector: dependency: transitive description: @@ -162,18 +157,18 @@ packages: dependency: transitive description: name: fast_immutable_collections - sha256: c3c73f4f989d3302066e4ec94e6ec73b5dc872592d02194f49f1352d64126b8c + sha256: "95a69b9380483dff49ae2c12c9eb92e2b4e1aeff481a33c2a20883471771598a" url: "https://pub.dev" source: hosted - version: "10.2.4" + version: "11.0.3" ffi: dependency: transitive description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" file: dependency: transitive description: @@ -214,10 +209,10 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "3.0.0" frontend_server_client: dependency: transitive description: @@ -280,10 +275,10 @@ packages: dependency: transitive description: name: js - sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "0.7.2" json_annotation: dependency: transitive description: @@ -320,10 +315,10 @@ packages: dependency: "direct dev" description: name: lint_hard - sha256: "638d2cce6d3d5499826be71311d18cded797a51351eaa1aee7a35a2f0f9bc46e" + sha256: ffe7058cb49e021d244d67e650a63380445b56643c2849c6929e938246b99058 url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.0.0" logging: dependency: transitive description: @@ -340,14 +335,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" - macros: - dependency: transitive - description: - name: macros - sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" - url: "https://pub.dev" - source: hosted - version: "0.1.3-main.0" matcher: dependency: transitive description: @@ -392,10 +379,10 @@ packages: dependency: transitive description: name: package_config - sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.0" path: dependency: transitive description: @@ -416,10 +403,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12" url: "https://pub.dev" source: hosted - version: "2.2.15" + version: "2.2.16" path_provider_foundation: dependency: transitive description: @@ -496,10 +483,10 @@ packages: dependency: transitive description: name: pub_semver - sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" shelf: dependency: transitive description: @@ -528,10 +515,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.0" sky_engine: dependency: transitive description: flutter @@ -698,10 +685,10 @@ packages: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web_socket: dependency: transitive description: @@ -751,5 +738,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.7.0-0 <4.0.0" - flutter: ">=3.24.0" + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/packages/veilid_support/example/pubspec.yaml b/packages/veilid_support/example/pubspec.yaml index c043a1b..b8333e6 100644 --- a/packages/veilid_support/example/pubspec.yaml +++ b/packages/veilid_support/example/pubspec.yaml @@ -14,11 +14,11 @@ dependencies: path: ../ dev_dependencies: - async_tools: ^0.1.6 + async_tools: ^0.1.9 integration_test: sdk: flutter - lint_hard: ^5.0.0 - test: ^1.25.2 + lint_hard: ^6.0.0 + test: ^1.25.15 veilid_test: path: ../../../../veilid/veilid-flutter/packages/veilid_test diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index 447a27c..e3dfcdd 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" async: dependency: transitive description: @@ -36,26 +36,27 @@ packages: async_tools: dependency: "direct main" description: - path: "../../../dart_async_tools" - relative: true - source: path - version: "0.1.7" + name: async_tools + sha256: afd5426e76631172f8ce6a6359b264b092fa9d2a52cd2528100115be9525e067 + url: "https://pub.dev" + source: hosted + version: "0.1.9" bloc: dependency: "direct main" description: name: bloc - sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + sha256: "52c10575f4445c61dd9e0cafcc6356fdd827c4c64dd7945ef3c4105f6b6ac189" url: "https://pub.dev" source: hosted - version: "8.1.4" + version: "9.0.0" bloc_advanced_tools: dependency: "direct main" description: name: bloc_advanced_tools - sha256: d8a680d8a0469456399fb26bae9f7a1d2a1420b5bdf75e204e0fadab9edb0811 + sha256: "7c7f294b425552c2d4831b01ad0d3e1f33f2bdf9acfb7b639caa072781d228cf" url: "https://pub.dev" source: hosted - version: "0.1.8" + version: "0.1.10" boolean_selector: dependency: transitive description: @@ -124,10 +125,10 @@ packages: dependency: transitive description: name: built_value - sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2" + sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 url: "https://pub.dev" source: hosted - version: "8.9.3" + version: "8.9.5" change_case: dependency: transitive description: @@ -220,18 +221,18 @@ packages: dependency: "direct main" description: name: fast_immutable_collections - sha256: c3c73f4f989d3302066e4ec94e6ec73b5dc872592d02194f49f1352d64126b8c + sha256: "95a69b9380483dff49ae2c12c9eb92e2b4e1aeff481a33c2a20883471771598a" url: "https://pub.dev" source: hosted - version: "10.2.4" + version: "11.0.3" ffi: dependency: transitive description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" file: dependency: transitive description: @@ -262,18 +263,18 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c" + sha256: "7ed2ddaa47524976d5f2aa91432a79da36a76969edd84170777ac5bea82d797c" url: "https://pub.dev" source: hosted - version: "2.5.8" + version: "3.0.4" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "3.0.0" frontend_server_client: dependency: transitive description: @@ -342,10 +343,10 @@ packages: dependency: transitive description: name: js - sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "0.7.2" json_annotation: dependency: "direct main" description: @@ -366,10 +367,10 @@ packages: dependency: "direct dev" description: name: lint_hard - sha256: "638d2cce6d3d5499826be71311d18cded797a51351eaa1aee7a35a2f0f9bc46e" + sha256: ffe7058cb49e021d244d67e650a63380445b56643c2849c6929e938246b99058 url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.0.0" logging: dependency: transitive description: @@ -430,10 +431,10 @@ packages: dependency: transitive description: name: package_config - sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.0" path: dependency: "direct main" description: @@ -454,10 +455,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12" url: "https://pub.dev" source: hosted - version: "2.2.15" + version: "2.2.16" path_provider_foundation: dependency: transitive description: @@ -526,10 +527,10 @@ packages: dependency: transitive description: name: pub_semver - sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" pubspec_parse: dependency: transitive description: @@ -746,10 +747,10 @@ packages: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web_socket: dependency: transitive description: @@ -791,5 +792,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.7.0-0 <4.0.0" - flutter: ">=3.24.0" + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index bcd965d..5aff89e 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -1,40 +1,40 @@ name: veilid_support description: Veilid Support Library -publish_to: 'none' +publish_to: "none" version: 1.0.2+0 environment: - sdk: '>=3.2.0 <4.0.0' + sdk: ">=3.2.0 <4.0.0" dependencies: - async_tools: ^0.1.7 - bloc: ^8.1.4 - bloc_advanced_tools: ^0.1.8 - charcode: ^1.3.1 - collection: ^1.18.0 - equatable: ^2.0.5 - fast_immutable_collections: ^10.2.3 - freezed_annotation: ^2.4.1 + async_tools: ^0.1.9 + bloc: ^9.0.0 + bloc_advanced_tools: ^0.1.10 + charcode: ^1.4.0 + collection: ^1.19.1 + equatable: ^2.0.7 + fast_immutable_collections: ^11.0.3 + freezed_annotation: ^3.0.0 json_annotation: ^4.9.0 loggy: ^2.0.3 - meta: ^1.12.0 + meta: ^1.16.0 - path: ^1.9.0 - path_provider: ^2.1.3 + path: ^1.9.1 + path_provider: ^2.1.5 protobuf: ^3.1.0 veilid: # veilid: ^0.0.1 path: ../../../veilid/veilid-flutter -dependency_overrides: - async_tools: - path: ../../../dart_async_tools -# bloc_advanced_tools: -# path: ../../../bloc_advanced_tools +# dependency_overrides: +# async_tools: +# path: ../../../dart_async_tools +# bloc_advanced_tools: +# path: ../../../bloc_advanced_tools dev_dependencies: - build_runner: ^2.4.10 - freezed: ^2.5.2 - json_serializable: ^6.8.0 - lint_hard: ^5.0.0 - test: ^1.25.2 + build_runner: ^2.4.15 + freezed: ^3.0.4 + json_serializable: ^6.9.4 + lint_hard: ^6.0.0 + test: ^1.25.15 diff --git a/pubspec.lock b/pubspec.lock index b4df49f..ea4a216 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -93,10 +93,10 @@ packages: dependency: "direct main" description: name: async_tools - sha256: a258558160d6adc18612d0c635ce0d18ceabc022f7933ce78ca4806075d79578 + sha256: afd5426e76631172f8ce6a6359b264b092fa9d2a52cd2528100115be9525e067 url: "https://pub.dev" source: hosted - version: "0.1.8" + version: "0.1.9" auto_size_text: dependency: "direct main" description: @@ -149,17 +149,18 @@ packages: dependency: "direct main" description: name: bloc - sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + sha256: "52c10575f4445c61dd9e0cafcc6356fdd827c4c64dd7945ef3c4105f6b6ac189" url: "https://pub.dev" source: hosted - version: "8.1.4" + version: "9.0.0" bloc_advanced_tools: dependency: "direct main" description: - path: "../bloc_advanced_tools" - relative: true - source: path - version: "0.1.9" + name: bloc_advanced_tools + sha256: "7c7f294b425552c2d4831b01ad0d3e1f33f2bdf9acfb7b639caa072781d228cf" + url: "https://pub.dev" + source: hosted + version: "0.1.10" blurry_modal_progress_hud: dependency: "direct main" description: @@ -284,10 +285,10 @@ packages: dependency: transitive description: name: camera_avfoundation - sha256: "3057ada0b30402e3a9b6dffec365c9736a36edbf04abaecc67c4309eadc86b49" + sha256: ba48b65a3a97004276ede882e6b838d9667642ff462c95a8bb57ca8a82b6bd25 url: "https://pub.dev" source: hosted - version: "0.9.18+9" + version: "0.9.18+11" camera_platform_interface: dependency: transitive description: @@ -476,10 +477,10 @@ packages: dependency: "direct main" description: name: fast_immutable_collections - sha256: c3c73f4f989d3302066e4ec94e6ec73b5dc872592d02194f49f1352d64126b8c + sha256: "95a69b9380483dff49ae2c12c9eb92e2b4e1aeff481a33c2a20883471771598a" url: "https://pub.dev" source: hosted - version: "10.2.4" + version: "11.0.3" ffi: dependency: transitive description: @@ -529,10 +530,10 @@ packages: dependency: "direct main" description: name: flutter_bloc - sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a + sha256: "1046d719fbdf230330d3443187cc33cc11963d15c9089f6cc56faa42a4c5f0cc" url: "https://pub.dev" source: hosted - version: "8.1.6" + version: "9.1.0" flutter_cache_manager: dependency: transitive description: @@ -562,18 +563,18 @@ packages: dependency: "direct main" description: name: flutter_form_builder - sha256: "375da52998c72f80dec9187bd93afa7ab202b89d5d066699368ff96d39fd4876" + sha256: aa3901466c70b69ae6c7f3d03fcbccaec5fde179d3fded0b10203144b546ad28 url: "https://pub.dev" source: hosted - version: "9.7.0" + version: "10.0.1" flutter_hooks: dependency: "direct main" description: name: flutter_hooks - sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70 + sha256: b772e710d16d7a20c0740c4f855095026b31c7eb5ba3ab67d2bd52021cd9461d url: "https://pub.dev" source: hosted - version: "0.20.5" + version: "0.21.2" flutter_link_previewer: dependency: transitive description: @@ -692,18 +693,18 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c" + sha256: "7ed2ddaa47524976d5f2aa91432a79da36a76969edd84170777ac5bea82d797c" url: "https://pub.dev" source: hosted - version: "2.5.8" + version: "3.0.4" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "3.0.0" frontend_server_client: dependency: transitive description: @@ -752,14 +753,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" - hive: - dependency: transitive - description: - name: hive - sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" - url: "https://pub.dev" - source: hosted - version: "2.2.3" html: dependency: transitive description: @@ -792,14 +785,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" - hydrated_bloc: - dependency: "direct main" - description: - name: hydrated_bloc - sha256: af35b357739fe41728df10bec03aad422cdc725a1e702e03af9d2a41ea05160c - url: "https://pub.dev" - source: hosted - version: "9.1.5" icons_launcher: dependency: "direct dev" description: @@ -876,10 +861,10 @@ packages: dependency: "direct dev" description: name: lint_hard - sha256: "638d2cce6d3d5499826be71311d18cded797a51351eaa1aee7a35a2f0f9bc46e" + sha256: ffe7058cb49e021d244d67e650a63380445b56643c2849c6929e938246b99058 url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.0.0" logging: dependency: transitive description: @@ -1192,14 +1177,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + qr_code_dart_decoder: + dependency: transitive + description: + name: qr_code_dart_decoder + sha256: "6da7eda27726d504bed3c30eabf78ddca3eb9265e1c8dc49b30ef5974b9c267f" + url: "https://pub.dev" + source: hosted + version: "0.0.5" qr_code_dart_scan: dependency: "direct main" description: name: qr_code_dart_scan - sha256: a21340c4a2ca14e2e114915940fcad166f15c1a065fed8b4fede4a4aba5bc4ff + sha256: bc4fc6f400b4350c6946d123c7871e156459703a61f8fa57d7144df9bbb46610 url: "https://pub.dev" source: hosted - version: "0.9.11" + version: "0.10.0" qr_flutter: dependency: "direct main" description: @@ -1418,10 +1411,10 @@ packages: dependency: "direct main" description: name: sliver_expandable - sha256: ae20eb848bd0ba9dd704732ad654438ac5a5bea2b023fa3cf80a086166d96d97 + sha256: "046d8912ebd072bf9d8e8161e50a4669c520f691fce8bfcbae4ada6982b18ba3" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" sliver_fill_remaining_box_adapter: dependency: "direct main" description: @@ -1895,4 +1888,4 @@ packages: version: "1.1.2" sdks: dart: ">=3.7.0 <4.0.0" - flutter: ">=3.27.0" + flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml index b909a16..379d2f2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,13 +15,13 @@ dependencies: animated_theme_switcher: ^2.0.10 ansicolor: ^2.0.3 archive: ^4.0.4 - async_tools: ^0.1.8 + async_tools: ^0.1.9 auto_size_text: ^3.0.0 awesome_extensions: ^2.0.21 badges: ^3.1.2 basic_utils: ^5.8.2 - bloc: ^8.1.4 - bloc_advanced_tools: ^0.1.9 + bloc: ^9.0.0 + bloc_advanced_tools: ^0.1.10 blurry_modal_progress_hud: ^1.1.1 change_case: ^2.2.0 charcode: ^1.4.0 @@ -30,20 +30,20 @@ dependencies: cupertino_icons: ^1.0.8 equatable: ^2.0.7 expansion_tile_group: ^2.2.0 - fast_immutable_collections: ^10.2.4 + fast_immutable_collections: ^11.0.3 file_saver: ^0.2.14 fixnum: ^1.1.1 flutter: sdk: flutter flutter_animate: ^4.5.2 - flutter_bloc: ^8.1.6 + flutter_bloc: ^9.1.0 flutter_chat_types: ^3.6.2 flutter_chat_ui: git: url: https://gitlab.com/veilid/flutter-chat-ui.git ref: main - flutter_form_builder: ^9.7.0 - flutter_hooks: ^0.20.5 + flutter_form_builder: ^10.0.1 + flutter_hooks: ^0.21.2 flutter_localizations: sdk: flutter flutter_native_splash: ^2.4.5 @@ -54,9 +54,8 @@ dependencies: flutter_translate: ^4.1.0 flutter_zoom_drawer: ^3.2.0 form_builder_validators: ^11.1.2 - freezed_annotation: ^2.4.4 + freezed_annotation: ^3.0.0 go_router: ^14.8.1 - hydrated_bloc: ^9.1.5 image: ^4.5.3 intl: ^0.19.0 json_annotation: ^4.9.0 @@ -73,7 +72,7 @@ dependencies: printing: ^5.14.2 protobuf: ^3.1.0 provider: ^6.1.2 - qr_code_dart_scan: ^0.9.11 + qr_code_dart_scan: ^0.10.0 qr_flutter: ^4.1.0 radix_colors: ^1.0.4 reorderable_grid: ^1.0.10 @@ -87,7 +86,7 @@ dependencies: share_plus: ^10.1.4 shared_preferences: ^2.5.2 signal_strength_indicator: ^0.4.1 - sliver_expandable: ^1.1.1 + sliver_expandable: ^1.1.2 sliver_fill_remaining_box_adapter: ^1.0.0 sliver_tools: ^0.2.12 sorted_list: @@ -111,22 +110,22 @@ dependencies: xterm: ^4.0.0 zxing2: ^0.2.3 -dependency_overrides: +# dependency_overrides: # async_tools: # path: ../dart_async_tools - bloc_advanced_tools: - path: ../bloc_advanced_tools -# searchable_listview: -# path: ../Searchable-Listview +# bloc_advanced_tools: +# path: ../bloc_advanced_tools +# searchable_listview: +# path: ../Searchable-Listview # flutter_chat_ui: -# path: ../flutter_chat_ui +# path: ../flutter_chat_ui dev_dependencies: build_runner: ^2.4.15 - freezed: ^2.5.8 + freezed: ^3.0.4 icons_launcher: ^3.0.1 json_serializable: ^6.9.4 - lint_hard: ^5.0.0 + lint_hard: ^6.0.0 flutter_native_splash: color: "#8588D0" From d6b1c2090609545297adf3fb1bec18f7e09b0444 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 22 Mar 2025 21:43:37 -0400 Subject: [PATCH 219/270] debugging work --- lib/account_manager/models/account_info.dart | 9 +- .../models/local_account/local_account.dart | 2 +- .../local_account/local_account.freezed.dart | 488 ++++++----- .../models/local_account/local_account.g.dart | 6 +- .../per_account_collection_state.dart | 22 +- .../per_account_collection_state.freezed.dart | 665 +++++++-------- .../models/user_login/user_login.dart | 2 +- .../models/user_login/user_login.freezed.dart | 398 +++++---- .../models/user_login/user_login.g.dart | 5 +- lib/chat/cubits/chat_component_cubit.dart | 10 +- lib/chat/models/chat_component_state.dart | 2 +- .../models/chat_component_state.freezed.dart | 361 ++++----- lib/chat/models/message_state.dart | 2 +- lib/chat/models/message_state.freezed.dart | 257 +++--- lib/chat/models/message_state.g.dart | 6 +- lib/chat/models/window_state.dart | 2 +- lib/chat/models/window_state.freezed.dart | 264 +++--- lib/chat_list/cubits/chat_list_cubit.dart | 2 +- lib/chat_list/views/chat_list_widget.dart | 4 +- .../cubits/contact_invitation_list_cubit.dart | 2 +- .../waiting_invitations_bloc_map_cubit.dart | 2 +- .../models/notifications_preference.dart | 2 +- .../notifications_preference.freezed.dart | 565 +++++++------ .../models/notifications_preference.g.dart | 8 +- .../models/notifications_state.dart | 4 +- .../models/notifications_state.freezed.dart | 452 ++++++----- lib/proto/proto.dart | 292 +++++++ lib/proto/veilidchat.pb.dart | 20 +- lib/proto/veilidchat.pbjson.dart | 35 +- lib/proto/veilidchat.proto | 1 + lib/router/cubits/router_cubit.dart | 2 +- lib/router/cubits/router_cubit.freezed.dart | 241 +++--- lib/router/cubits/router_cubit.g.dart | 5 +- lib/settings/models/preferences.dart | 4 +- lib/settings/models/preferences.freezed.dart | 761 +++++++++--------- lib/settings/models/preferences.g.dart | 12 +- lib/theme/models/theme_preference.dart | 2 +- .../models/theme_preference.freezed.dart | 356 ++++---- lib/theme/models/theme_preference.g.dart | 8 +- lib/tools/loggy.dart | 6 + lib/tools/state_logger.dart | 10 +- .../models/processor_connection_state.dart | 2 +- .../processor_connection_state.freezed.dart | 317 ++++---- .../lib/dht_support/proto/proto.dart | 42 + .../src/dht_log/dht_log_cubit.dart | 8 + .../src/dht_log/dht_log_spine.dart | 5 +- .../src/dht_record/dht_record.dart | 4 +- .../src/dht_record/dht_record_pool.dart | 45 +- .../dht_record/dht_record_pool.freezed.dart | 623 +++++++------- .../src/dht_record/dht_record_pool.g.dart | 16 +- .../src/dht_record/extensions.dart | 57 ++ .../dht_short_array_cubit.dart | 23 +- .../dht_short_array/dht_short_array_head.dart | 10 +- .../dht_short_array_write.dart | 4 +- .../identity_support/account_record_info.dart | 2 +- .../account_record_info.freezed.dart | 277 +++---- .../account_record_info.g.dart | 8 +- .../lib/identity_support/identity.dart | 2 +- .../identity_support/identity.freezed.dart | 231 +++--- .../lib/identity_support/identity.g.dart | 6 +- .../identity_support/identity_instance.dart | 2 +- .../identity_instance.freezed.dart | 284 +++---- .../identity_support/identity_instance.g.dart | 8 +- .../lib/identity_support/super_identity.dart | 2 +- .../super_identity.freezed.dart | 330 ++++---- .../identity_support/super_identity.g.dart | 6 +- packages/veilid_support/lib/proto/proto.dart | 24 + .../veilid_support/lib/src/dynamic_debug.dart | 130 +++ .../veilid_support/lib/veilid_support.dart | 3 +- packages/veilid_support/pubspec.lock | 2 +- packages/veilid_support/pubspec.yaml | 1 + 71 files changed, 4155 insertions(+), 3616 deletions(-) create mode 100644 packages/veilid_support/lib/dht_support/src/dht_record/extensions.dart create mode 100644 packages/veilid_support/lib/src/dynamic_debug.dart diff --git a/lib/account_manager/models/account_info.dart b/lib/account_manager/models/account_info.dart index 12ed5e1..8f57add 100644 --- a/lib/account_manager/models/account_info.dart +++ b/lib/account_manager/models/account_info.dart @@ -13,7 +13,7 @@ enum AccountInfoStatus { } @immutable -class AccountInfo extends Equatable { +class AccountInfo extends Equatable implements ToDebugMap { const AccountInfo({ required this.status, required this.localAccount, @@ -30,6 +30,13 @@ class AccountInfo extends Equatable { localAccount, userLogin, ]; + + @override + Map toDebugMap() => { + 'status': status, + 'localAccount': localAccount, + 'userLogin': userLogin, + }; } extension AccountInfoExt on AccountInfo { diff --git a/lib/account_manager/models/local_account/local_account.dart b/lib/account_manager/models/local_account/local_account.dart index 76070ae..1ec6d22 100644 --- a/lib/account_manager/models/local_account/local_account.dart +++ b/lib/account_manager/models/local_account/local_account.dart @@ -16,7 +16,7 @@ part 'local_account.freezed.dart'; // This is the root of the account information tree for VeilidChat // @freezed -class LocalAccount with _$LocalAccount { +sealed class LocalAccount with _$LocalAccount { const factory LocalAccount({ // The super identity key record for the account, // containing the publicKey in the currentIdentity 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 effc69a..e2c3c55 100644 --- a/lib/account_manager/models/local_account/local_account.freezed.dart +++ b/lib/account_manager/models/local_account/local_account.freezed.dart @@ -1,3 +1,4 @@ +// dart format width=80 // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint @@ -9,238 +10,43 @@ part of 'local_account.dart'; // FreezedGenerator // ************************************************************************** +// dart format off 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'); - -LocalAccount _$LocalAccountFromJson(Map json) { - return _LocalAccount.fromJson(json); -} - /// @nodoc mixin _$LocalAccount { // The super identity key record for the account, // containing the publicKey in the currentIdentity - SuperIdentity get superIdentity => - throw _privateConstructorUsedError; // The encrypted currentIdentity secret that goes with + SuperIdentity + get superIdentity; // The encrypted currentIdentity secret that goes with // the identityPublicKey with appended salt @Uint8ListJsonConverter() - Uint8List get identitySecretBytes => - throw _privateConstructorUsedError; // The kind of encryption input used on the account - EncryptionKeyType get encryptionKeyType => - throw _privateConstructorUsedError; // If account is not hidden, password can be retrieved via - bool get biometricsEnabled => - throw _privateConstructorUsedError; // Keep account hidden unless account password is entered + Uint8List + get identitySecretBytes; // The kind of encryption input used on the account + EncryptionKeyType + get encryptionKeyType; // If account is not hidden, password can be retrieved via + bool + get biometricsEnabled; // Keep account hidden unless account password is entered // (tries all hidden accounts with auth method (no biometrics)) - bool get hiddenAccount => - throw _privateConstructorUsedError; // Display name for account until it is unlocked - String get name => throw _privateConstructorUsedError; - - /// Serializes this LocalAccount to a JSON map. - Map toJson() => throw _privateConstructorUsedError; + bool get hiddenAccount; // Display name for account until it is unlocked + String get name; /// Create a copy of LocalAccount /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') $LocalAccountCopyWith get copyWith => - throw _privateConstructorUsedError; -} + _$LocalAccountCopyWithImpl( + this as LocalAccount, _$identity); -/// @nodoc -abstract class $LocalAccountCopyWith<$Res> { - factory $LocalAccountCopyWith( - LocalAccount value, $Res Function(LocalAccount) then) = - _$LocalAccountCopyWithImpl<$Res, LocalAccount>; - @useResult - $Res call( - {SuperIdentity superIdentity, - @Uint8ListJsonConverter() Uint8List identitySecretBytes, - EncryptionKeyType encryptionKeyType, - bool biometricsEnabled, - bool hiddenAccount, - String name}); - - $SuperIdentityCopyWith<$Res> get superIdentity; -} - -/// @nodoc -class _$LocalAccountCopyWithImpl<$Res, $Val extends LocalAccount> - implements $LocalAccountCopyWith<$Res> { - _$LocalAccountCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of LocalAccount - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? superIdentity = null, - Object? identitySecretBytes = null, - Object? encryptionKeyType = null, - Object? biometricsEnabled = null, - Object? hiddenAccount = null, - Object? name = null, - }) { - return _then(_value.copyWith( - superIdentity: null == superIdentity - ? _value.superIdentity - : superIdentity // ignore: cast_nullable_to_non_nullable - as SuperIdentity, - identitySecretBytes: null == identitySecretBytes - ? _value.identitySecretBytes - : identitySecretBytes // ignore: cast_nullable_to_non_nullable - as Uint8List, - encryptionKeyType: null == encryptionKeyType - ? _value.encryptionKeyType - : encryptionKeyType // ignore: cast_nullable_to_non_nullable - as EncryptionKeyType, - biometricsEnabled: null == biometricsEnabled - ? _value.biometricsEnabled - : biometricsEnabled // ignore: cast_nullable_to_non_nullable - as bool, - hiddenAccount: null == hiddenAccount - ? _value.hiddenAccount - : hiddenAccount // ignore: cast_nullable_to_non_nullable - as bool, - name: null == name - ? _value.name - : name // ignore: cast_nullable_to_non_nullable - as String, - ) as $Val); - } - - /// Create a copy of LocalAccount - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $SuperIdentityCopyWith<$Res> get superIdentity { - return $SuperIdentityCopyWith<$Res>(_value.superIdentity, (value) { - return _then(_value.copyWith(superIdentity: value) as $Val); - }); - } -} - -/// @nodoc -abstract class _$$LocalAccountImplCopyWith<$Res> - implements $LocalAccountCopyWith<$Res> { - factory _$$LocalAccountImplCopyWith( - _$LocalAccountImpl value, $Res Function(_$LocalAccountImpl) then) = - __$$LocalAccountImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {SuperIdentity superIdentity, - @Uint8ListJsonConverter() Uint8List identitySecretBytes, - EncryptionKeyType encryptionKeyType, - bool biometricsEnabled, - bool hiddenAccount, - String name}); - - @override - $SuperIdentityCopyWith<$Res> get superIdentity; -} - -/// @nodoc -class __$$LocalAccountImplCopyWithImpl<$Res> - extends _$LocalAccountCopyWithImpl<$Res, _$LocalAccountImpl> - implements _$$LocalAccountImplCopyWith<$Res> { - __$$LocalAccountImplCopyWithImpl( - _$LocalAccountImpl _value, $Res Function(_$LocalAccountImpl) _then) - : super(_value, _then); - - /// Create a copy of LocalAccount - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? superIdentity = null, - Object? identitySecretBytes = null, - Object? encryptionKeyType = null, - Object? biometricsEnabled = null, - Object? hiddenAccount = null, - Object? name = null, - }) { - return _then(_$LocalAccountImpl( - superIdentity: null == superIdentity - ? _value.superIdentity - : superIdentity // ignore: cast_nullable_to_non_nullable - as SuperIdentity, - identitySecretBytes: null == identitySecretBytes - ? _value.identitySecretBytes - : identitySecretBytes // ignore: cast_nullable_to_non_nullable - as Uint8List, - encryptionKeyType: null == encryptionKeyType - ? _value.encryptionKeyType - : encryptionKeyType // ignore: cast_nullable_to_non_nullable - as EncryptionKeyType, - biometricsEnabled: null == biometricsEnabled - ? _value.biometricsEnabled - : biometricsEnabled // ignore: cast_nullable_to_non_nullable - as bool, - hiddenAccount: null == hiddenAccount - ? _value.hiddenAccount - : hiddenAccount // ignore: cast_nullable_to_non_nullable - as bool, - name: null == name - ? _value.name - : name // ignore: cast_nullable_to_non_nullable - as String, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$LocalAccountImpl implements _LocalAccount { - const _$LocalAccountImpl( - {required this.superIdentity, - @Uint8ListJsonConverter() required this.identitySecretBytes, - required this.encryptionKeyType, - required this.biometricsEnabled, - required this.hiddenAccount, - required this.name}); - - factory _$LocalAccountImpl.fromJson(Map json) => - _$$LocalAccountImplFromJson(json); - -// The super identity key record for the account, -// containing the publicKey in the currentIdentity - @override - final SuperIdentity superIdentity; -// The encrypted currentIdentity secret that goes with -// the identityPublicKey with appended salt - @override - @Uint8ListJsonConverter() - final Uint8List identitySecretBytes; -// The kind of encryption input used on the account - @override - final EncryptionKeyType encryptionKeyType; -// If account is not hidden, password can be retrieved via - @override - final bool biometricsEnabled; -// Keep account hidden unless account password is entered -// (tries all hidden accounts with auth method (no biometrics)) - @override - final bool hiddenAccount; -// Display name for account until it is unlocked - @override - final String name; - - @override - String toString() { - return 'LocalAccount(superIdentity: $superIdentity, identitySecretBytes: $identitySecretBytes, encryptionKeyType: $encryptionKeyType, biometricsEnabled: $biometricsEnabled, hiddenAccount: $hiddenAccount, name: $name)'; - } + /// Serializes this LocalAccount to a JSON map. + Map toJson(); @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$LocalAccountImpl && + other is LocalAccount && (identical(other.superIdentity, superIdentity) || other.superIdentity == superIdentity) && const DeepCollectionEquality() @@ -265,60 +71,250 @@ class _$LocalAccountImpl implements _LocalAccount { hiddenAccount, name); - /// Create a copy of LocalAccount - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) @override - @pragma('vm:prefer-inline') - _$$LocalAccountImplCopyWith<_$LocalAccountImpl> get copyWith => - __$$LocalAccountImplCopyWithImpl<_$LocalAccountImpl>(this, _$identity); - - @override - Map toJson() { - return _$$LocalAccountImplToJson( - this, - ); + String toString() { + return 'LocalAccount(superIdentity: $superIdentity, identitySecretBytes: $identitySecretBytes, encryptionKeyType: $encryptionKeyType, biometricsEnabled: $biometricsEnabled, hiddenAccount: $hiddenAccount, name: $name)'; } } -abstract class _LocalAccount implements LocalAccount { - const factory _LocalAccount( - {required final SuperIdentity superIdentity, - @Uint8ListJsonConverter() required final Uint8List identitySecretBytes, - required final EncryptionKeyType encryptionKeyType, - required final bool biometricsEnabled, - required final bool hiddenAccount, - required final String name}) = _$LocalAccountImpl; +/// @nodoc +abstract mixin class $LocalAccountCopyWith<$Res> { + factory $LocalAccountCopyWith( + LocalAccount value, $Res Function(LocalAccount) _then) = + _$LocalAccountCopyWithImpl; + @useResult + $Res call( + {SuperIdentity superIdentity, + @Uint8ListJsonConverter() Uint8List identitySecretBytes, + EncryptionKeyType encryptionKeyType, + bool biometricsEnabled, + bool hiddenAccount, + String name}); - factory _LocalAccount.fromJson(Map json) = - _$LocalAccountImpl.fromJson; + $SuperIdentityCopyWith<$Res> get superIdentity; +} + +/// @nodoc +class _$LocalAccountCopyWithImpl<$Res> implements $LocalAccountCopyWith<$Res> { + _$LocalAccountCopyWithImpl(this._self, this._then); + + final LocalAccount _self; + final $Res Function(LocalAccount) _then; + + /// Create a copy of LocalAccount + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? superIdentity = null, + Object? identitySecretBytes = null, + Object? encryptionKeyType = null, + Object? biometricsEnabled = null, + Object? hiddenAccount = null, + Object? name = null, + }) { + return _then(_self.copyWith( + superIdentity: null == superIdentity + ? _self.superIdentity + : superIdentity // ignore: cast_nullable_to_non_nullable + as SuperIdentity, + identitySecretBytes: null == identitySecretBytes + ? _self.identitySecretBytes + : identitySecretBytes // ignore: cast_nullable_to_non_nullable + as Uint8List, + encryptionKeyType: null == encryptionKeyType + ? _self.encryptionKeyType + : encryptionKeyType // ignore: cast_nullable_to_non_nullable + as EncryptionKeyType, + biometricsEnabled: null == biometricsEnabled + ? _self.biometricsEnabled + : biometricsEnabled // ignore: cast_nullable_to_non_nullable + as bool, + hiddenAccount: null == hiddenAccount + ? _self.hiddenAccount + : hiddenAccount // ignore: cast_nullable_to_non_nullable + as bool, + name: null == name + ? _self.name + : name // ignore: cast_nullable_to_non_nullable + as String, + )); + } + + /// Create a copy of LocalAccount + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $SuperIdentityCopyWith<$Res> get superIdentity { + return $SuperIdentityCopyWith<$Res>(_self.superIdentity, (value) { + return _then(_self.copyWith(superIdentity: value)); + }); + } +} + +/// @nodoc +@JsonSerializable() +class _LocalAccount implements LocalAccount { + const _LocalAccount( + {required this.superIdentity, + @Uint8ListJsonConverter() required this.identitySecretBytes, + required this.encryptionKeyType, + required this.biometricsEnabled, + required this.hiddenAccount, + required this.name}); + factory _LocalAccount.fromJson(Map json) => + _$LocalAccountFromJson(json); // The super identity key record for the account, // containing the publicKey in the currentIdentity @override - SuperIdentity - get superIdentity; // The encrypted currentIdentity secret that goes with + final SuperIdentity superIdentity; +// The encrypted currentIdentity secret that goes with // the identityPublicKey with appended salt @override @Uint8ListJsonConverter() - Uint8List - get identitySecretBytes; // The kind of encryption input used on the account + final Uint8List identitySecretBytes; +// The kind of encryption input used on the account @override - EncryptionKeyType - get encryptionKeyType; // If account is not hidden, password can be retrieved via + final EncryptionKeyType encryptionKeyType; +// If account is not hidden, password can be retrieved via @override - bool - get biometricsEnabled; // Keep account hidden unless account password is entered + final bool biometricsEnabled; +// Keep account hidden unless account password is entered // (tries all hidden accounts with auth method (no biometrics)) @override - bool get hiddenAccount; // Display name for account until it is unlocked + final bool hiddenAccount; +// Display name for account until it is unlocked @override - String get name; + final String name; /// Create a copy of LocalAccount /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) - _$$LocalAccountImplCopyWith<_$LocalAccountImpl> get copyWith => - throw _privateConstructorUsedError; + @pragma('vm:prefer-inline') + _$LocalAccountCopyWith<_LocalAccount> get copyWith => + __$LocalAccountCopyWithImpl<_LocalAccount>(this, _$identity); + + @override + Map toJson() { + return _$LocalAccountToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _LocalAccount && + (identical(other.superIdentity, superIdentity) || + other.superIdentity == superIdentity) && + const DeepCollectionEquality() + .equals(other.identitySecretBytes, identitySecretBytes) && + (identical(other.encryptionKeyType, encryptionKeyType) || + other.encryptionKeyType == encryptionKeyType) && + (identical(other.biometricsEnabled, biometricsEnabled) || + other.biometricsEnabled == biometricsEnabled) && + (identical(other.hiddenAccount, hiddenAccount) || + other.hiddenAccount == hiddenAccount) && + (identical(other.name, name) || other.name == name)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + superIdentity, + const DeepCollectionEquality().hash(identitySecretBytes), + encryptionKeyType, + biometricsEnabled, + hiddenAccount, + name); + + @override + String toString() { + return 'LocalAccount(superIdentity: $superIdentity, identitySecretBytes: $identitySecretBytes, encryptionKeyType: $encryptionKeyType, biometricsEnabled: $biometricsEnabled, hiddenAccount: $hiddenAccount, name: $name)'; + } } + +/// @nodoc +abstract mixin class _$LocalAccountCopyWith<$Res> + implements $LocalAccountCopyWith<$Res> { + factory _$LocalAccountCopyWith( + _LocalAccount value, $Res Function(_LocalAccount) _then) = + __$LocalAccountCopyWithImpl; + @override + @useResult + $Res call( + {SuperIdentity superIdentity, + @Uint8ListJsonConverter() Uint8List identitySecretBytes, + EncryptionKeyType encryptionKeyType, + bool biometricsEnabled, + bool hiddenAccount, + String name}); + + @override + $SuperIdentityCopyWith<$Res> get superIdentity; +} + +/// @nodoc +class __$LocalAccountCopyWithImpl<$Res> + implements _$LocalAccountCopyWith<$Res> { + __$LocalAccountCopyWithImpl(this._self, this._then); + + final _LocalAccount _self; + final $Res Function(_LocalAccount) _then; + + /// Create a copy of LocalAccount + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? superIdentity = null, + Object? identitySecretBytes = null, + Object? encryptionKeyType = null, + Object? biometricsEnabled = null, + Object? hiddenAccount = null, + Object? name = null, + }) { + return _then(_LocalAccount( + superIdentity: null == superIdentity + ? _self.superIdentity + : superIdentity // ignore: cast_nullable_to_non_nullable + as SuperIdentity, + identitySecretBytes: null == identitySecretBytes + ? _self.identitySecretBytes + : identitySecretBytes // ignore: cast_nullable_to_non_nullable + as Uint8List, + encryptionKeyType: null == encryptionKeyType + ? _self.encryptionKeyType + : encryptionKeyType // ignore: cast_nullable_to_non_nullable + as EncryptionKeyType, + biometricsEnabled: null == biometricsEnabled + ? _self.biometricsEnabled + : biometricsEnabled // ignore: cast_nullable_to_non_nullable + as bool, + hiddenAccount: null == hiddenAccount + ? _self.hiddenAccount + : hiddenAccount // ignore: cast_nullable_to_non_nullable + as bool, + name: null == name + ? _self.name + : name // ignore: cast_nullable_to_non_nullable + as String, + )); + } + + /// Create a copy of LocalAccount + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $SuperIdentityCopyWith<$Res> get superIdentity { + return $SuperIdentityCopyWith<$Res>(_self.superIdentity, (value) { + return _then(_self.copyWith(superIdentity: value)); + }); + } +} + +// dart format on diff --git a/lib/account_manager/models/local_account/local_account.g.dart b/lib/account_manager/models/local_account/local_account.g.dart index b60c226..40d55e5 100644 --- a/lib/account_manager/models/local_account/local_account.g.dart +++ b/lib/account_manager/models/local_account/local_account.g.dart @@ -6,8 +6,8 @@ part of 'local_account.dart'; // JsonSerializableGenerator // ************************************************************************** -_$LocalAccountImpl _$$LocalAccountImplFromJson(Map json) => - _$LocalAccountImpl( +_LocalAccount _$LocalAccountFromJson(Map json) => + _LocalAccount( superIdentity: SuperIdentity.fromJson(json['super_identity']), identitySecretBytes: const Uint8ListJsonConverter() .fromJson(json['identity_secret_bytes']), @@ -18,7 +18,7 @@ _$LocalAccountImpl _$$LocalAccountImplFromJson(Map json) => name: json['name'] as String, ); -Map _$$LocalAccountImplToJson(_$LocalAccountImpl instance) => +Map _$LocalAccountToJson(_LocalAccount instance) => { 'super_identity': instance.superIdentity.toJson(), 'identity_secret_bytes': diff --git a/lib/account_manager/models/per_account_collection_state/per_account_collection_state.dart b/lib/account_manager/models/per_account_collection_state/per_account_collection_state.dart index 9e0a6f0..24a394c 100644 --- a/lib/account_manager/models/per_account_collection_state/per_account_collection_state.dart +++ b/lib/account_manager/models/per_account_collection_state/per_account_collection_state.dart @@ -2,6 +2,7 @@ import 'package:async_tools/async_tools.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:veilid_support/veilid_support.dart'; import '../../../chat/chat.dart'; import '../../../chat_list/chat_list.dart'; @@ -14,7 +15,9 @@ import '../../account_manager.dart'; part 'per_account_collection_state.freezed.dart'; @freezed -class PerAccountCollectionState with _$PerAccountCollectionState { +sealed class PerAccountCollectionState + with _$PerAccountCollectionState + implements ToDebugMap { const factory PerAccountCollectionState({ required AccountInfo accountInfo, required AsyncValue? avAccountRecordState, @@ -29,6 +32,23 @@ class PerAccountCollectionState with _$PerAccountCollectionState { required ActiveSingleContactChatBlocMapCubit? activeSingleContactChatBlocMapCubit, }) = _PerAccountCollectionState; + const PerAccountCollectionState._(); + + @override + Map toDebugMap() => { + 'accountInfo': accountInfo, + 'avAccountRecordState': avAccountRecordState, + 'accountInfoCubit': accountInfoCubit, + 'accountRecordCubit': accountRecordCubit, + 'contactInvitationListCubit': contactInvitationListCubit, + 'contactListCubit': contactListCubit, + 'waitingInvitationsBlocMapCubit': waitingInvitationsBlocMapCubit, + 'activeChatCubit': activeChatCubit, + 'chatListCubit': chatListCubit, + 'activeConversationsBlocMapCubit': activeConversationsBlocMapCubit, + 'activeSingleContactChatBlocMapCubit': + activeSingleContactChatBlocMapCubit, + }; } extension PerAccountCollectionStateExt on PerAccountCollectionState { diff --git a/lib/account_manager/models/per_account_collection_state/per_account_collection_state.freezed.dart b/lib/account_manager/models/per_account_collection_state/per_account_collection_state.freezed.dart index 1aa0c7e..4c2e219 100644 --- a/lib/account_manager/models/per_account_collection_state/per_account_collection_state.freezed.dart +++ b/lib/account_manager/models/per_account_collection_state/per_account_collection_state.freezed.dart @@ -1,3 +1,4 @@ +// dart format width=80 // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint @@ -9,311 +10,36 @@ part of 'per_account_collection_state.dart'; // FreezedGenerator // ************************************************************************** +// dart format off 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'); - /// @nodoc mixin _$PerAccountCollectionState { - AccountInfo get accountInfo => throw _privateConstructorUsedError; - AsyncValue? get avAccountRecordState => - throw _privateConstructorUsedError; - AccountInfoCubit? get accountInfoCubit => throw _privateConstructorUsedError; - AccountRecordCubit? get accountRecordCubit => - throw _privateConstructorUsedError; - ContactInvitationListCubit? get contactInvitationListCubit => - throw _privateConstructorUsedError; - ContactListCubit? get contactListCubit => throw _privateConstructorUsedError; - WaitingInvitationsBlocMapCubit? get waitingInvitationsBlocMapCubit => - throw _privateConstructorUsedError; - ActiveChatCubit? get activeChatCubit => throw _privateConstructorUsedError; - ChatListCubit? get chatListCubit => throw _privateConstructorUsedError; - ActiveConversationsBlocMapCubit? get activeConversationsBlocMapCubit => - throw _privateConstructorUsedError; - ActiveSingleContactChatBlocMapCubit? - get activeSingleContactChatBlocMapCubit => - throw _privateConstructorUsedError; + AccountInfo get accountInfo; + AsyncValue? get avAccountRecordState; + AccountInfoCubit? get accountInfoCubit; + AccountRecordCubit? get accountRecordCubit; + ContactInvitationListCubit? get contactInvitationListCubit; + ContactListCubit? get contactListCubit; + WaitingInvitationsBlocMapCubit? get waitingInvitationsBlocMapCubit; + ActiveChatCubit? get activeChatCubit; + ChatListCubit? get chatListCubit; + ActiveConversationsBlocMapCubit? get activeConversationsBlocMapCubit; + ActiveSingleContactChatBlocMapCubit? get activeSingleContactChatBlocMapCubit; /// Create a copy of PerAccountCollectionState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') $PerAccountCollectionStateCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $PerAccountCollectionStateCopyWith<$Res> { - factory $PerAccountCollectionStateCopyWith(PerAccountCollectionState value, - $Res Function(PerAccountCollectionState) then) = - _$PerAccountCollectionStateCopyWithImpl<$Res, PerAccountCollectionState>; - @useResult - $Res call( - {AccountInfo accountInfo, - AsyncValue? avAccountRecordState, - AccountInfoCubit? accountInfoCubit, - AccountRecordCubit? accountRecordCubit, - ContactInvitationListCubit? contactInvitationListCubit, - ContactListCubit? contactListCubit, - WaitingInvitationsBlocMapCubit? waitingInvitationsBlocMapCubit, - ActiveChatCubit? activeChatCubit, - ChatListCubit? chatListCubit, - ActiveConversationsBlocMapCubit? activeConversationsBlocMapCubit, - ActiveSingleContactChatBlocMapCubit? - activeSingleContactChatBlocMapCubit}); - - $AsyncValueCopyWith? get avAccountRecordState; -} - -/// @nodoc -class _$PerAccountCollectionStateCopyWithImpl<$Res, - $Val extends PerAccountCollectionState> - implements $PerAccountCollectionStateCopyWith<$Res> { - _$PerAccountCollectionStateCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of PerAccountCollectionState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? accountInfo = null, - Object? avAccountRecordState = freezed, - Object? accountInfoCubit = freezed, - Object? accountRecordCubit = freezed, - Object? contactInvitationListCubit = freezed, - Object? contactListCubit = freezed, - Object? waitingInvitationsBlocMapCubit = freezed, - Object? activeChatCubit = freezed, - Object? chatListCubit = freezed, - Object? activeConversationsBlocMapCubit = freezed, - Object? activeSingleContactChatBlocMapCubit = freezed, - }) { - return _then(_value.copyWith( - accountInfo: null == accountInfo - ? _value.accountInfo - : accountInfo // ignore: cast_nullable_to_non_nullable - as AccountInfo, - avAccountRecordState: freezed == avAccountRecordState - ? _value.avAccountRecordState - : avAccountRecordState // ignore: cast_nullable_to_non_nullable - as AsyncValue?, - accountInfoCubit: freezed == accountInfoCubit - ? _value.accountInfoCubit - : accountInfoCubit // ignore: cast_nullable_to_non_nullable - as AccountInfoCubit?, - accountRecordCubit: freezed == accountRecordCubit - ? _value.accountRecordCubit - : accountRecordCubit // ignore: cast_nullable_to_non_nullable - as AccountRecordCubit?, - contactInvitationListCubit: freezed == contactInvitationListCubit - ? _value.contactInvitationListCubit - : contactInvitationListCubit // ignore: cast_nullable_to_non_nullable - as ContactInvitationListCubit?, - contactListCubit: freezed == contactListCubit - ? _value.contactListCubit - : contactListCubit // ignore: cast_nullable_to_non_nullable - as ContactListCubit?, - waitingInvitationsBlocMapCubit: freezed == waitingInvitationsBlocMapCubit - ? _value.waitingInvitationsBlocMapCubit - : waitingInvitationsBlocMapCubit // ignore: cast_nullable_to_non_nullable - as WaitingInvitationsBlocMapCubit?, - activeChatCubit: freezed == activeChatCubit - ? _value.activeChatCubit - : activeChatCubit // ignore: cast_nullable_to_non_nullable - as ActiveChatCubit?, - chatListCubit: freezed == chatListCubit - ? _value.chatListCubit - : chatListCubit // ignore: cast_nullable_to_non_nullable - as ChatListCubit?, - activeConversationsBlocMapCubit: freezed == - activeConversationsBlocMapCubit - ? _value.activeConversationsBlocMapCubit - : activeConversationsBlocMapCubit // ignore: cast_nullable_to_non_nullable - as ActiveConversationsBlocMapCubit?, - activeSingleContactChatBlocMapCubit: freezed == - activeSingleContactChatBlocMapCubit - ? _value.activeSingleContactChatBlocMapCubit - : activeSingleContactChatBlocMapCubit // ignore: cast_nullable_to_non_nullable - as ActiveSingleContactChatBlocMapCubit?, - ) as $Val); - } - - /// Create a copy of PerAccountCollectionState - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $AsyncValueCopyWith? get avAccountRecordState { - if (_value.avAccountRecordState == null) { - return null; - } - - return $AsyncValueCopyWith(_value.avAccountRecordState!, - (value) { - return _then(_value.copyWith(avAccountRecordState: value) as $Val); - }); - } -} - -/// @nodoc -abstract class _$$PerAccountCollectionStateImplCopyWith<$Res> - implements $PerAccountCollectionStateCopyWith<$Res> { - factory _$$PerAccountCollectionStateImplCopyWith( - _$PerAccountCollectionStateImpl value, - $Res Function(_$PerAccountCollectionStateImpl) then) = - __$$PerAccountCollectionStateImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {AccountInfo accountInfo, - AsyncValue? avAccountRecordState, - AccountInfoCubit? accountInfoCubit, - AccountRecordCubit? accountRecordCubit, - ContactInvitationListCubit? contactInvitationListCubit, - ContactListCubit? contactListCubit, - WaitingInvitationsBlocMapCubit? waitingInvitationsBlocMapCubit, - ActiveChatCubit? activeChatCubit, - ChatListCubit? chatListCubit, - ActiveConversationsBlocMapCubit? activeConversationsBlocMapCubit, - ActiveSingleContactChatBlocMapCubit? - activeSingleContactChatBlocMapCubit}); - - @override - $AsyncValueCopyWith? get avAccountRecordState; -} - -/// @nodoc -class __$$PerAccountCollectionStateImplCopyWithImpl<$Res> - extends _$PerAccountCollectionStateCopyWithImpl<$Res, - _$PerAccountCollectionStateImpl> - implements _$$PerAccountCollectionStateImplCopyWith<$Res> { - __$$PerAccountCollectionStateImplCopyWithImpl( - _$PerAccountCollectionStateImpl _value, - $Res Function(_$PerAccountCollectionStateImpl) _then) - : super(_value, _then); - - /// Create a copy of PerAccountCollectionState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? accountInfo = null, - Object? avAccountRecordState = freezed, - Object? accountInfoCubit = freezed, - Object? accountRecordCubit = freezed, - Object? contactInvitationListCubit = freezed, - Object? contactListCubit = freezed, - Object? waitingInvitationsBlocMapCubit = freezed, - Object? activeChatCubit = freezed, - Object? chatListCubit = freezed, - Object? activeConversationsBlocMapCubit = freezed, - Object? activeSingleContactChatBlocMapCubit = freezed, - }) { - return _then(_$PerAccountCollectionStateImpl( - accountInfo: null == accountInfo - ? _value.accountInfo - : accountInfo // ignore: cast_nullable_to_non_nullable - as AccountInfo, - avAccountRecordState: freezed == avAccountRecordState - ? _value.avAccountRecordState - : avAccountRecordState // ignore: cast_nullable_to_non_nullable - as AsyncValue?, - accountInfoCubit: freezed == accountInfoCubit - ? _value.accountInfoCubit - : accountInfoCubit // ignore: cast_nullable_to_non_nullable - as AccountInfoCubit?, - accountRecordCubit: freezed == accountRecordCubit - ? _value.accountRecordCubit - : accountRecordCubit // ignore: cast_nullable_to_non_nullable - as AccountRecordCubit?, - contactInvitationListCubit: freezed == contactInvitationListCubit - ? _value.contactInvitationListCubit - : contactInvitationListCubit // ignore: cast_nullable_to_non_nullable - as ContactInvitationListCubit?, - contactListCubit: freezed == contactListCubit - ? _value.contactListCubit - : contactListCubit // ignore: cast_nullable_to_non_nullable - as ContactListCubit?, - waitingInvitationsBlocMapCubit: freezed == waitingInvitationsBlocMapCubit - ? _value.waitingInvitationsBlocMapCubit - : waitingInvitationsBlocMapCubit // ignore: cast_nullable_to_non_nullable - as WaitingInvitationsBlocMapCubit?, - activeChatCubit: freezed == activeChatCubit - ? _value.activeChatCubit - : activeChatCubit // ignore: cast_nullable_to_non_nullable - as ActiveChatCubit?, - chatListCubit: freezed == chatListCubit - ? _value.chatListCubit - : chatListCubit // ignore: cast_nullable_to_non_nullable - as ChatListCubit?, - activeConversationsBlocMapCubit: freezed == - activeConversationsBlocMapCubit - ? _value.activeConversationsBlocMapCubit - : activeConversationsBlocMapCubit // ignore: cast_nullable_to_non_nullable - as ActiveConversationsBlocMapCubit?, - activeSingleContactChatBlocMapCubit: freezed == - activeSingleContactChatBlocMapCubit - ? _value.activeSingleContactChatBlocMapCubit - : activeSingleContactChatBlocMapCubit // ignore: cast_nullable_to_non_nullable - as ActiveSingleContactChatBlocMapCubit?, - )); - } -} - -/// @nodoc - -class _$PerAccountCollectionStateImpl implements _PerAccountCollectionState { - const _$PerAccountCollectionStateImpl( - {required this.accountInfo, - required this.avAccountRecordState, - required this.accountInfoCubit, - required this.accountRecordCubit, - required this.contactInvitationListCubit, - required this.contactListCubit, - required this.waitingInvitationsBlocMapCubit, - required this.activeChatCubit, - required this.chatListCubit, - required this.activeConversationsBlocMapCubit, - required this.activeSingleContactChatBlocMapCubit}); - - @override - final AccountInfo accountInfo; - @override - final AsyncValue? avAccountRecordState; - @override - final AccountInfoCubit? accountInfoCubit; - @override - final AccountRecordCubit? accountRecordCubit; - @override - final ContactInvitationListCubit? contactInvitationListCubit; - @override - final ContactListCubit? contactListCubit; - @override - final WaitingInvitationsBlocMapCubit? waitingInvitationsBlocMapCubit; - @override - final ActiveChatCubit? activeChatCubit; - @override - final ChatListCubit? chatListCubit; - @override - final ActiveConversationsBlocMapCubit? activeConversationsBlocMapCubit; - @override - final ActiveSingleContactChatBlocMapCubit? - activeSingleContactChatBlocMapCubit; - - @override - String toString() { - return 'PerAccountCollectionState(accountInfo: $accountInfo, avAccountRecordState: $avAccountRecordState, accountInfoCubit: $accountInfoCubit, accountRecordCubit: $accountRecordCubit, contactInvitationListCubit: $contactInvitationListCubit, contactListCubit: $contactListCubit, waitingInvitationsBlocMapCubit: $waitingInvitationsBlocMapCubit, activeChatCubit: $activeChatCubit, chatListCubit: $chatListCubit, activeConversationsBlocMapCubit: $activeConversationsBlocMapCubit, activeSingleContactChatBlocMapCubit: $activeSingleContactChatBlocMapCubit)'; - } + _$PerAccountCollectionStateCopyWithImpl( + this as PerAccountCollectionState, _$identity); @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$PerAccountCollectionStateImpl && + other is PerAccountCollectionState && (identical(other.accountInfo, accountInfo) || other.accountInfo == accountInfo) && (identical(other.avAccountRecordState, avAccountRecordState) || @@ -361,61 +87,350 @@ class _$PerAccountCollectionStateImpl implements _PerAccountCollectionState { activeConversationsBlocMapCubit, activeSingleContactChatBlocMapCubit); + @override + String toString() { + return 'PerAccountCollectionState(accountInfo: $accountInfo, avAccountRecordState: $avAccountRecordState, accountInfoCubit: $accountInfoCubit, accountRecordCubit: $accountRecordCubit, contactInvitationListCubit: $contactInvitationListCubit, contactListCubit: $contactListCubit, waitingInvitationsBlocMapCubit: $waitingInvitationsBlocMapCubit, activeChatCubit: $activeChatCubit, chatListCubit: $chatListCubit, activeConversationsBlocMapCubit: $activeConversationsBlocMapCubit, activeSingleContactChatBlocMapCubit: $activeSingleContactChatBlocMapCubit)'; + } +} + +/// @nodoc +abstract mixin class $PerAccountCollectionStateCopyWith<$Res> { + factory $PerAccountCollectionStateCopyWith(PerAccountCollectionState value, + $Res Function(PerAccountCollectionState) _then) = + _$PerAccountCollectionStateCopyWithImpl; + @useResult + $Res call( + {AccountInfo accountInfo, + AsyncValue? avAccountRecordState, + AccountInfoCubit? accountInfoCubit, + AccountRecordCubit? accountRecordCubit, + ContactInvitationListCubit? contactInvitationListCubit, + ContactListCubit? contactListCubit, + WaitingInvitationsBlocMapCubit? waitingInvitationsBlocMapCubit, + ActiveChatCubit? activeChatCubit, + ChatListCubit? chatListCubit, + ActiveConversationsBlocMapCubit? activeConversationsBlocMapCubit, + ActiveSingleContactChatBlocMapCubit? + activeSingleContactChatBlocMapCubit}); + + $AsyncValueCopyWith? get avAccountRecordState; +} + +/// @nodoc +class _$PerAccountCollectionStateCopyWithImpl<$Res> + implements $PerAccountCollectionStateCopyWith<$Res> { + _$PerAccountCollectionStateCopyWithImpl(this._self, this._then); + + final PerAccountCollectionState _self; + final $Res Function(PerAccountCollectionState) _then; + + /// Create a copy of PerAccountCollectionState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? accountInfo = null, + Object? avAccountRecordState = freezed, + Object? accountInfoCubit = freezed, + Object? accountRecordCubit = freezed, + Object? contactInvitationListCubit = freezed, + Object? contactListCubit = freezed, + Object? waitingInvitationsBlocMapCubit = freezed, + Object? activeChatCubit = freezed, + Object? chatListCubit = freezed, + Object? activeConversationsBlocMapCubit = freezed, + Object? activeSingleContactChatBlocMapCubit = freezed, + }) { + return _then(_self.copyWith( + accountInfo: null == accountInfo + ? _self.accountInfo + : accountInfo // ignore: cast_nullable_to_non_nullable + as AccountInfo, + avAccountRecordState: freezed == avAccountRecordState + ? _self.avAccountRecordState! + : avAccountRecordState // ignore: cast_nullable_to_non_nullable + as AsyncValue?, + accountInfoCubit: freezed == accountInfoCubit + ? _self.accountInfoCubit + : accountInfoCubit // ignore: cast_nullable_to_non_nullable + as AccountInfoCubit?, + accountRecordCubit: freezed == accountRecordCubit + ? _self.accountRecordCubit + : accountRecordCubit // ignore: cast_nullable_to_non_nullable + as AccountRecordCubit?, + contactInvitationListCubit: freezed == contactInvitationListCubit + ? _self.contactInvitationListCubit + : contactInvitationListCubit // ignore: cast_nullable_to_non_nullable + as ContactInvitationListCubit?, + contactListCubit: freezed == contactListCubit + ? _self.contactListCubit + : contactListCubit // ignore: cast_nullable_to_non_nullable + as ContactListCubit?, + waitingInvitationsBlocMapCubit: freezed == waitingInvitationsBlocMapCubit + ? _self.waitingInvitationsBlocMapCubit + : waitingInvitationsBlocMapCubit // ignore: cast_nullable_to_non_nullable + as WaitingInvitationsBlocMapCubit?, + activeChatCubit: freezed == activeChatCubit + ? _self.activeChatCubit + : activeChatCubit // ignore: cast_nullable_to_non_nullable + as ActiveChatCubit?, + chatListCubit: freezed == chatListCubit + ? _self.chatListCubit + : chatListCubit // ignore: cast_nullable_to_non_nullable + as ChatListCubit?, + activeConversationsBlocMapCubit: freezed == + activeConversationsBlocMapCubit + ? _self.activeConversationsBlocMapCubit + : activeConversationsBlocMapCubit // ignore: cast_nullable_to_non_nullable + as ActiveConversationsBlocMapCubit?, + activeSingleContactChatBlocMapCubit: freezed == + activeSingleContactChatBlocMapCubit + ? _self.activeSingleContactChatBlocMapCubit + : activeSingleContactChatBlocMapCubit // ignore: cast_nullable_to_non_nullable + as ActiveSingleContactChatBlocMapCubit?, + )); + } + /// Create a copy of PerAccountCollectionState /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') - _$$PerAccountCollectionStateImplCopyWith<_$PerAccountCollectionStateImpl> - get copyWith => __$$PerAccountCollectionStateImplCopyWithImpl< - _$PerAccountCollectionStateImpl>(this, _$identity); + $AsyncValueCopyWith? get avAccountRecordState { + if (_self.avAccountRecordState == null) { + return null; + } + + return $AsyncValueCopyWith(_self.avAccountRecordState!, + (value) { + return _then(_self.copyWith(avAccountRecordState: value)); + }); + } } -abstract class _PerAccountCollectionState implements PerAccountCollectionState { - const factory _PerAccountCollectionState( - {required final AccountInfo accountInfo, - required final AsyncValue? avAccountRecordState, - required final AccountInfoCubit? accountInfoCubit, - required final AccountRecordCubit? accountRecordCubit, - required final ContactInvitationListCubit? contactInvitationListCubit, - required final ContactListCubit? contactListCubit, - required final WaitingInvitationsBlocMapCubit? - waitingInvitationsBlocMapCubit, - required final ActiveChatCubit? activeChatCubit, - required final ChatListCubit? chatListCubit, - required final ActiveConversationsBlocMapCubit? - activeConversationsBlocMapCubit, - required final ActiveSingleContactChatBlocMapCubit? - activeSingleContactChatBlocMapCubit}) = - _$PerAccountCollectionStateImpl; +/// @nodoc + +class _PerAccountCollectionState extends PerAccountCollectionState { + const _PerAccountCollectionState( + {required this.accountInfo, + required this.avAccountRecordState, + required this.accountInfoCubit, + required this.accountRecordCubit, + required this.contactInvitationListCubit, + required this.contactListCubit, + required this.waitingInvitationsBlocMapCubit, + required this.activeChatCubit, + required this.chatListCubit, + required this.activeConversationsBlocMapCubit, + required this.activeSingleContactChatBlocMapCubit}) + : super._(); @override - AccountInfo get accountInfo; + final AccountInfo accountInfo; @override - AsyncValue? get avAccountRecordState; + final AsyncValue? avAccountRecordState; @override - AccountInfoCubit? get accountInfoCubit; + final AccountInfoCubit? accountInfoCubit; @override - AccountRecordCubit? get accountRecordCubit; + final AccountRecordCubit? accountRecordCubit; @override - ContactInvitationListCubit? get contactInvitationListCubit; + final ContactInvitationListCubit? contactInvitationListCubit; @override - ContactListCubit? get contactListCubit; + final ContactListCubit? contactListCubit; @override - WaitingInvitationsBlocMapCubit? get waitingInvitationsBlocMapCubit; + final WaitingInvitationsBlocMapCubit? waitingInvitationsBlocMapCubit; @override - ActiveChatCubit? get activeChatCubit; + final ActiveChatCubit? activeChatCubit; @override - ChatListCubit? get chatListCubit; + final ChatListCubit? chatListCubit; @override - ActiveConversationsBlocMapCubit? get activeConversationsBlocMapCubit; + final ActiveConversationsBlocMapCubit? activeConversationsBlocMapCubit; @override - ActiveSingleContactChatBlocMapCubit? get activeSingleContactChatBlocMapCubit; + final ActiveSingleContactChatBlocMapCubit? + activeSingleContactChatBlocMapCubit; /// Create a copy of PerAccountCollectionState /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) - _$$PerAccountCollectionStateImplCopyWith<_$PerAccountCollectionStateImpl> - get copyWith => throw _privateConstructorUsedError; + @pragma('vm:prefer-inline') + _$PerAccountCollectionStateCopyWith<_PerAccountCollectionState> + get copyWith => + __$PerAccountCollectionStateCopyWithImpl<_PerAccountCollectionState>( + this, _$identity); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _PerAccountCollectionState && + (identical(other.accountInfo, accountInfo) || + other.accountInfo == accountInfo) && + (identical(other.avAccountRecordState, avAccountRecordState) || + other.avAccountRecordState == avAccountRecordState) && + (identical(other.accountInfoCubit, accountInfoCubit) || + other.accountInfoCubit == accountInfoCubit) && + (identical(other.accountRecordCubit, accountRecordCubit) || + other.accountRecordCubit == accountRecordCubit) && + (identical(other.contactInvitationListCubit, + contactInvitationListCubit) || + other.contactInvitationListCubit == + contactInvitationListCubit) && + (identical(other.contactListCubit, contactListCubit) || + other.contactListCubit == contactListCubit) && + (identical(other.waitingInvitationsBlocMapCubit, + waitingInvitationsBlocMapCubit) || + other.waitingInvitationsBlocMapCubit == + waitingInvitationsBlocMapCubit) && + (identical(other.activeChatCubit, activeChatCubit) || + other.activeChatCubit == activeChatCubit) && + (identical(other.chatListCubit, chatListCubit) || + other.chatListCubit == chatListCubit) && + (identical(other.activeConversationsBlocMapCubit, + activeConversationsBlocMapCubit) || + other.activeConversationsBlocMapCubit == + activeConversationsBlocMapCubit) && + (identical(other.activeSingleContactChatBlocMapCubit, + activeSingleContactChatBlocMapCubit) || + other.activeSingleContactChatBlocMapCubit == + activeSingleContactChatBlocMapCubit)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + accountInfo, + avAccountRecordState, + accountInfoCubit, + accountRecordCubit, + contactInvitationListCubit, + contactListCubit, + waitingInvitationsBlocMapCubit, + activeChatCubit, + chatListCubit, + activeConversationsBlocMapCubit, + activeSingleContactChatBlocMapCubit); + + @override + String toString() { + return 'PerAccountCollectionState(accountInfo: $accountInfo, avAccountRecordState: $avAccountRecordState, accountInfoCubit: $accountInfoCubit, accountRecordCubit: $accountRecordCubit, contactInvitationListCubit: $contactInvitationListCubit, contactListCubit: $contactListCubit, waitingInvitationsBlocMapCubit: $waitingInvitationsBlocMapCubit, activeChatCubit: $activeChatCubit, chatListCubit: $chatListCubit, activeConversationsBlocMapCubit: $activeConversationsBlocMapCubit, activeSingleContactChatBlocMapCubit: $activeSingleContactChatBlocMapCubit)'; + } } + +/// @nodoc +abstract mixin class _$PerAccountCollectionStateCopyWith<$Res> + implements $PerAccountCollectionStateCopyWith<$Res> { + factory _$PerAccountCollectionStateCopyWith(_PerAccountCollectionState value, + $Res Function(_PerAccountCollectionState) _then) = + __$PerAccountCollectionStateCopyWithImpl; + @override + @useResult + $Res call( + {AccountInfo accountInfo, + AsyncValue? avAccountRecordState, + AccountInfoCubit? accountInfoCubit, + AccountRecordCubit? accountRecordCubit, + ContactInvitationListCubit? contactInvitationListCubit, + ContactListCubit? contactListCubit, + WaitingInvitationsBlocMapCubit? waitingInvitationsBlocMapCubit, + ActiveChatCubit? activeChatCubit, + ChatListCubit? chatListCubit, + ActiveConversationsBlocMapCubit? activeConversationsBlocMapCubit, + ActiveSingleContactChatBlocMapCubit? + activeSingleContactChatBlocMapCubit}); + + @override + $AsyncValueCopyWith? get avAccountRecordState; +} + +/// @nodoc +class __$PerAccountCollectionStateCopyWithImpl<$Res> + implements _$PerAccountCollectionStateCopyWith<$Res> { + __$PerAccountCollectionStateCopyWithImpl(this._self, this._then); + + final _PerAccountCollectionState _self; + final $Res Function(_PerAccountCollectionState) _then; + + /// Create a copy of PerAccountCollectionState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? accountInfo = null, + Object? avAccountRecordState = freezed, + Object? accountInfoCubit = freezed, + Object? accountRecordCubit = freezed, + Object? contactInvitationListCubit = freezed, + Object? contactListCubit = freezed, + Object? waitingInvitationsBlocMapCubit = freezed, + Object? activeChatCubit = freezed, + Object? chatListCubit = freezed, + Object? activeConversationsBlocMapCubit = freezed, + Object? activeSingleContactChatBlocMapCubit = freezed, + }) { + return _then(_PerAccountCollectionState( + accountInfo: null == accountInfo + ? _self.accountInfo + : accountInfo // ignore: cast_nullable_to_non_nullable + as AccountInfo, + avAccountRecordState: freezed == avAccountRecordState + ? _self.avAccountRecordState + : avAccountRecordState // ignore: cast_nullable_to_non_nullable + as AsyncValue?, + accountInfoCubit: freezed == accountInfoCubit + ? _self.accountInfoCubit + : accountInfoCubit // ignore: cast_nullable_to_non_nullable + as AccountInfoCubit?, + accountRecordCubit: freezed == accountRecordCubit + ? _self.accountRecordCubit + : accountRecordCubit // ignore: cast_nullable_to_non_nullable + as AccountRecordCubit?, + contactInvitationListCubit: freezed == contactInvitationListCubit + ? _self.contactInvitationListCubit + : contactInvitationListCubit // ignore: cast_nullable_to_non_nullable + as ContactInvitationListCubit?, + contactListCubit: freezed == contactListCubit + ? _self.contactListCubit + : contactListCubit // ignore: cast_nullable_to_non_nullable + as ContactListCubit?, + waitingInvitationsBlocMapCubit: freezed == waitingInvitationsBlocMapCubit + ? _self.waitingInvitationsBlocMapCubit + : waitingInvitationsBlocMapCubit // ignore: cast_nullable_to_non_nullable + as WaitingInvitationsBlocMapCubit?, + activeChatCubit: freezed == activeChatCubit + ? _self.activeChatCubit + : activeChatCubit // ignore: cast_nullable_to_non_nullable + as ActiveChatCubit?, + chatListCubit: freezed == chatListCubit + ? _self.chatListCubit + : chatListCubit // ignore: cast_nullable_to_non_nullable + as ChatListCubit?, + activeConversationsBlocMapCubit: freezed == + activeConversationsBlocMapCubit + ? _self.activeConversationsBlocMapCubit + : activeConversationsBlocMapCubit // ignore: cast_nullable_to_non_nullable + as ActiveConversationsBlocMapCubit?, + activeSingleContactChatBlocMapCubit: freezed == + activeSingleContactChatBlocMapCubit + ? _self.activeSingleContactChatBlocMapCubit + : activeSingleContactChatBlocMapCubit // ignore: cast_nullable_to_non_nullable + as ActiveSingleContactChatBlocMapCubit?, + )); + } + + /// Create a copy of PerAccountCollectionState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AsyncValueCopyWith? get avAccountRecordState { + if (_self.avAccountRecordState == null) { + return null; + } + + return $AsyncValueCopyWith(_self.avAccountRecordState!, + (value) { + return _then(_self.copyWith(avAccountRecordState: value)); + }); + } +} + +// dart format on diff --git a/lib/account_manager/models/user_login/user_login.dart b/lib/account_manager/models/user_login/user_login.dart index 7c024cf..d43fdfd 100644 --- a/lib/account_manager/models/user_login/user_login.dart +++ b/lib/account_manager/models/user_login/user_login.dart @@ -9,7 +9,7 @@ part 'user_login.g.dart'; // User logins are stored in the user_logins tablestore table // indexed by the accountSuperIdentityRecordKey @freezed -class UserLogin with _$UserLogin { +sealed class UserLogin with _$UserLogin { const factory UserLogin({ // SuperIdentity record key for the user // used to index the local accounts table 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 2804a77..b0c6070 100644 --- a/lib/account_manager/models/user_login/user_login.freezed.dart +++ b/lib/account_manager/models/user_login/user_login.freezed.dart @@ -1,3 +1,4 @@ +// dart format width=80 // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint @@ -9,195 +10,36 @@ part of 'user_login.dart'; // FreezedGenerator // ************************************************************************** +// dart format off 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'); - -UserLogin _$UserLoginFromJson(Map json) { - return _UserLogin.fromJson(json); -} - /// @nodoc mixin _$UserLogin { // SuperIdentity record key for the user // used to index the local accounts table - Typed get superIdentityRecordKey => - throw _privateConstructorUsedError; // The identity secret as unlocked from the local accounts table - Typed get identitySecret => - throw _privateConstructorUsedError; // The account record key, owner key and secret pulled from the identity - AccountRecordInfo get accountRecordInfo => - throw _privateConstructorUsedError; // The time this login was most recently used - Timestamp get lastActive => throw _privateConstructorUsedError; - - /// Serializes this UserLogin to a JSON map. - Map toJson() => throw _privateConstructorUsedError; + TypedKey + get superIdentityRecordKey; // The identity secret as unlocked from the local accounts table + TypedSecret + get identitySecret; // The account record key, owner key and secret pulled from the identity + AccountRecordInfo + get accountRecordInfo; // The time this login was most recently used + Timestamp get lastActive; /// Create a copy of UserLogin /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') $UserLoginCopyWith get copyWith => - throw _privateConstructorUsedError; -} + _$UserLoginCopyWithImpl(this as UserLogin, _$identity); -/// @nodoc -abstract class $UserLoginCopyWith<$Res> { - factory $UserLoginCopyWith(UserLogin value, $Res Function(UserLogin) then) = - _$UserLoginCopyWithImpl<$Res, UserLogin>; - @useResult - $Res call( - {Typed superIdentityRecordKey, - Typed identitySecret, - AccountRecordInfo accountRecordInfo, - Timestamp lastActive}); - - $AccountRecordInfoCopyWith<$Res> get accountRecordInfo; -} - -/// @nodoc -class _$UserLoginCopyWithImpl<$Res, $Val extends UserLogin> - implements $UserLoginCopyWith<$Res> { - _$UserLoginCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of UserLogin - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? superIdentityRecordKey = null, - Object? identitySecret = null, - Object? accountRecordInfo = null, - Object? lastActive = null, - }) { - return _then(_value.copyWith( - superIdentityRecordKey: null == superIdentityRecordKey - ? _value.superIdentityRecordKey - : superIdentityRecordKey // ignore: cast_nullable_to_non_nullable - as Typed, - identitySecret: null == identitySecret - ? _value.identitySecret - : identitySecret // ignore: cast_nullable_to_non_nullable - as Typed, - accountRecordInfo: null == accountRecordInfo - ? _value.accountRecordInfo - : accountRecordInfo // ignore: cast_nullable_to_non_nullable - as AccountRecordInfo, - lastActive: null == lastActive - ? _value.lastActive - : lastActive // ignore: cast_nullable_to_non_nullable - as Timestamp, - ) as $Val); - } - - /// Create a copy of UserLogin - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $AccountRecordInfoCopyWith<$Res> get accountRecordInfo { - return $AccountRecordInfoCopyWith<$Res>(_value.accountRecordInfo, (value) { - return _then(_value.copyWith(accountRecordInfo: value) as $Val); - }); - } -} - -/// @nodoc -abstract class _$$UserLoginImplCopyWith<$Res> - implements $UserLoginCopyWith<$Res> { - factory _$$UserLoginImplCopyWith( - _$UserLoginImpl value, $Res Function(_$UserLoginImpl) then) = - __$$UserLoginImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {Typed superIdentityRecordKey, - Typed identitySecret, - AccountRecordInfo accountRecordInfo, - Timestamp lastActive}); - - @override - $AccountRecordInfoCopyWith<$Res> get accountRecordInfo; -} - -/// @nodoc -class __$$UserLoginImplCopyWithImpl<$Res> - extends _$UserLoginCopyWithImpl<$Res, _$UserLoginImpl> - implements _$$UserLoginImplCopyWith<$Res> { - __$$UserLoginImplCopyWithImpl( - _$UserLoginImpl _value, $Res Function(_$UserLoginImpl) _then) - : super(_value, _then); - - /// Create a copy of UserLogin - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? superIdentityRecordKey = null, - Object? identitySecret = null, - Object? accountRecordInfo = null, - Object? lastActive = null, - }) { - return _then(_$UserLoginImpl( - superIdentityRecordKey: null == superIdentityRecordKey - ? _value.superIdentityRecordKey - : superIdentityRecordKey // ignore: cast_nullable_to_non_nullable - as Typed, - identitySecret: null == identitySecret - ? _value.identitySecret - : identitySecret // ignore: cast_nullable_to_non_nullable - as Typed, - accountRecordInfo: null == accountRecordInfo - ? _value.accountRecordInfo - : accountRecordInfo // ignore: cast_nullable_to_non_nullable - as AccountRecordInfo, - lastActive: null == lastActive - ? _value.lastActive - : lastActive // ignore: cast_nullable_to_non_nullable - as Timestamp, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$UserLoginImpl implements _UserLogin { - const _$UserLoginImpl( - {required this.superIdentityRecordKey, - required this.identitySecret, - required this.accountRecordInfo, - required this.lastActive}); - - factory _$UserLoginImpl.fromJson(Map json) => - _$$UserLoginImplFromJson(json); - -// SuperIdentity record key for the user -// used to index the local accounts table - @override - final Typed superIdentityRecordKey; -// The identity secret as unlocked from the local accounts table - @override - final Typed identitySecret; -// The account record key, owner key and secret pulled from the identity - @override - final AccountRecordInfo accountRecordInfo; -// The time this login was most recently used - @override - final Timestamp lastActive; - - @override - String toString() { - return 'UserLogin(superIdentityRecordKey: $superIdentityRecordKey, identitySecret: $identitySecret, accountRecordInfo: $accountRecordInfo, lastActive: $lastActive)'; - } + /// Serializes this UserLogin to a JSON map. + Map toJson(); @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$UserLoginImpl && + other is UserLogin && (identical(other.superIdentityRecordKey, superIdentityRecordKey) || other.superIdentityRecordKey == superIdentityRecordKey) && (identical(other.identitySecret, identitySecret) || @@ -213,50 +55,204 @@ class _$UserLoginImpl implements _UserLogin { int get hashCode => Object.hash(runtimeType, superIdentityRecordKey, identitySecret, accountRecordInfo, lastActive); - /// Create a copy of UserLogin - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) @override - @pragma('vm:prefer-inline') - _$$UserLoginImplCopyWith<_$UserLoginImpl> get copyWith => - __$$UserLoginImplCopyWithImpl<_$UserLoginImpl>(this, _$identity); - - @override - Map toJson() { - return _$$UserLoginImplToJson( - this, - ); + String toString() { + return 'UserLogin(superIdentityRecordKey: $superIdentityRecordKey, identitySecret: $identitySecret, accountRecordInfo: $accountRecordInfo, lastActive: $lastActive)'; } } -abstract class _UserLogin implements UserLogin { - const factory _UserLogin( - {required final Typed superIdentityRecordKey, - required final Typed identitySecret, - required final AccountRecordInfo accountRecordInfo, - required final Timestamp lastActive}) = _$UserLoginImpl; +/// @nodoc +abstract mixin class $UserLoginCopyWith<$Res> { + factory $UserLoginCopyWith(UserLogin value, $Res Function(UserLogin) _then) = + _$UserLoginCopyWithImpl; + @useResult + $Res call( + {Typed superIdentityRecordKey, + Typed identitySecret, + AccountRecordInfo accountRecordInfo, + Timestamp lastActive}); - factory _UserLogin.fromJson(Map json) = - _$UserLoginImpl.fromJson; + $AccountRecordInfoCopyWith<$Res> get accountRecordInfo; +} + +/// @nodoc +class _$UserLoginCopyWithImpl<$Res> implements $UserLoginCopyWith<$Res> { + _$UserLoginCopyWithImpl(this._self, this._then); + + final UserLogin _self; + final $Res Function(UserLogin) _then; + + /// Create a copy of UserLogin + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? superIdentityRecordKey = null, + Object? identitySecret = null, + Object? accountRecordInfo = null, + Object? lastActive = null, + }) { + return _then(_self.copyWith( + superIdentityRecordKey: null == superIdentityRecordKey + ? _self.superIdentityRecordKey! + : superIdentityRecordKey // ignore: cast_nullable_to_non_nullable + as Typed, + identitySecret: null == identitySecret + ? _self.identitySecret! + : identitySecret // ignore: cast_nullable_to_non_nullable + as Typed, + accountRecordInfo: null == accountRecordInfo + ? _self.accountRecordInfo + : accountRecordInfo // ignore: cast_nullable_to_non_nullable + as AccountRecordInfo, + lastActive: null == lastActive + ? _self.lastActive + : lastActive // ignore: cast_nullable_to_non_nullable + as Timestamp, + )); + } + + /// Create a copy of UserLogin + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AccountRecordInfoCopyWith<$Res> get accountRecordInfo { + return $AccountRecordInfoCopyWith<$Res>(_self.accountRecordInfo, (value) { + return _then(_self.copyWith(accountRecordInfo: value)); + }); + } +} + +/// @nodoc +@JsonSerializable() +class _UserLogin implements UserLogin { + const _UserLogin( + {required this.superIdentityRecordKey, + required this.identitySecret, + required this.accountRecordInfo, + required this.lastActive}); + factory _UserLogin.fromJson(Map json) => + _$UserLoginFromJson(json); // SuperIdentity record key for the user // used to index the local accounts table @override - Typed - get superIdentityRecordKey; // The identity secret as unlocked from the local accounts table + final Typed superIdentityRecordKey; +// The identity secret as unlocked from the local accounts table @override - Typed - get identitySecret; // The account record key, owner key and secret pulled from the identity + final Typed identitySecret; +// The account record key, owner key and secret pulled from the identity @override - AccountRecordInfo - get accountRecordInfo; // The time this login was most recently used + final AccountRecordInfo accountRecordInfo; +// The time this login was most recently used @override - Timestamp get lastActive; + final Timestamp lastActive; /// Create a copy of UserLogin /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) - _$$UserLoginImplCopyWith<_$UserLoginImpl> get copyWith => - throw _privateConstructorUsedError; + @pragma('vm:prefer-inline') + _$UserLoginCopyWith<_UserLogin> get copyWith => + __$UserLoginCopyWithImpl<_UserLogin>(this, _$identity); + + @override + Map toJson() { + return _$UserLoginToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _UserLogin && + (identical(other.superIdentityRecordKey, superIdentityRecordKey) || + other.superIdentityRecordKey == superIdentityRecordKey) && + (identical(other.identitySecret, identitySecret) || + other.identitySecret == identitySecret) && + (identical(other.accountRecordInfo, accountRecordInfo) || + other.accountRecordInfo == accountRecordInfo) && + (identical(other.lastActive, lastActive) || + other.lastActive == lastActive)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, superIdentityRecordKey, + identitySecret, accountRecordInfo, lastActive); + + @override + String toString() { + return 'UserLogin(superIdentityRecordKey: $superIdentityRecordKey, identitySecret: $identitySecret, accountRecordInfo: $accountRecordInfo, lastActive: $lastActive)'; + } } + +/// @nodoc +abstract mixin class _$UserLoginCopyWith<$Res> + implements $UserLoginCopyWith<$Res> { + factory _$UserLoginCopyWith( + _UserLogin value, $Res Function(_UserLogin) _then) = + __$UserLoginCopyWithImpl; + @override + @useResult + $Res call( + {Typed superIdentityRecordKey, + Typed identitySecret, + AccountRecordInfo accountRecordInfo, + Timestamp lastActive}); + + @override + $AccountRecordInfoCopyWith<$Res> get accountRecordInfo; +} + +/// @nodoc +class __$UserLoginCopyWithImpl<$Res> implements _$UserLoginCopyWith<$Res> { + __$UserLoginCopyWithImpl(this._self, this._then); + + final _UserLogin _self; + final $Res Function(_UserLogin) _then; + + /// Create a copy of UserLogin + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? superIdentityRecordKey = null, + Object? identitySecret = null, + Object? accountRecordInfo = null, + Object? lastActive = null, + }) { + return _then(_UserLogin( + superIdentityRecordKey: null == superIdentityRecordKey + ? _self.superIdentityRecordKey + : superIdentityRecordKey // ignore: cast_nullable_to_non_nullable + as Typed, + identitySecret: null == identitySecret + ? _self.identitySecret + : identitySecret // ignore: cast_nullable_to_non_nullable + as Typed, + accountRecordInfo: null == accountRecordInfo + ? _self.accountRecordInfo + : accountRecordInfo // ignore: cast_nullable_to_non_nullable + as AccountRecordInfo, + lastActive: null == lastActive + ? _self.lastActive + : lastActive // ignore: cast_nullable_to_non_nullable + as Timestamp, + )); + } + + /// Create a copy of UserLogin + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AccountRecordInfoCopyWith<$Res> get accountRecordInfo { + return $AccountRecordInfoCopyWith<$Res>(_self.accountRecordInfo, (value) { + return _then(_self.copyWith(accountRecordInfo: value)); + }); + } +} + +// dart format on diff --git a/lib/account_manager/models/user_login/user_login.g.dart b/lib/account_manager/models/user_login/user_login.g.dart index 173d853..fa5314b 100644 --- a/lib/account_manager/models/user_login/user_login.g.dart +++ b/lib/account_manager/models/user_login/user_login.g.dart @@ -6,8 +6,7 @@ part of 'user_login.dart'; // JsonSerializableGenerator // ************************************************************************** -_$UserLoginImpl _$$UserLoginImplFromJson(Map json) => - _$UserLoginImpl( +_UserLogin _$UserLoginFromJson(Map json) => _UserLogin( superIdentityRecordKey: Typed.fromJson( json['super_identity_record_key']), identitySecret: @@ -17,7 +16,7 @@ _$UserLoginImpl _$$UserLoginImplFromJson(Map json) => lastActive: Timestamp.fromJson(json['last_active']), ); -Map _$$UserLoginImplToJson(_$UserLoginImpl instance) => +Map _$UserLoginToJson(_UserLogin instance) => { 'super_identity_record_key': instance.superIdentityRecordKey.toJson(), 'identity_secret': instance.identitySecret.toJson(), diff --git a/lib/chat/cubits/chat_component_cubit.dart b/lib/chat/cubits/chat_component_cubit.dart index 9e50e02..7ea9e95 100644 --- a/lib/chat/cubits/chat_component_cubit.dart +++ b/lib/chat/cubits/chat_component_cubit.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:async_tools/async_tools.dart'; -import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/widgets.dart'; @@ -184,9 +183,7 @@ class ChatComponentCubit extends Cubit { emit(_convertMessages(state, avMessagesState)); } - void _onChangedContacts( - BlocBusyState>>> - bavContacts) { + void _onChangedContacts(DHTShortArrayCubitState bavContacts) { // Rewrite users when contacts change singleFuture((this, _sfChangedContacts), _updateConversationSubscriptions); } @@ -353,6 +350,7 @@ class ChatComponentCubit extends Cubit { case proto.Message_Kind.membership: case proto.Message_Kind.moderation: case proto.Message_Kind.notSet: + case proto.Message_Kind.readReceipt: return (currentState, null); } } @@ -440,9 +438,7 @@ class ChatComponentCubit extends Cubit { final Map>> _conversationSubscriptions = {}; late StreamSubscription _messagesSubscription; - late StreamSubscription< - BlocBusyState< - AsyncValue>>>> + late StreamSubscription> _contactListSubscription; double scrollOffset = 0; } diff --git a/lib/chat/models/chat_component_state.dart b/lib/chat/models/chat_component_state.dart index b06b413..82e492d 100644 --- a/lib/chat/models/chat_component_state.dart +++ b/lib/chat/models/chat_component_state.dart @@ -13,7 +13,7 @@ import 'window_state.dart'; part 'chat_component_state.freezed.dart'; @freezed -class ChatComponentState with _$ChatComponentState { +sealed class ChatComponentState with _$ChatComponentState { const factory ChatComponentState( { // GlobalKey for the chat diff --git a/lib/chat/models/chat_component_state.freezed.dart b/lib/chat/models/chat_component_state.freezed.dart index f967997..ae3acee 100644 --- a/lib/chat/models/chat_component_state.freezed.dart +++ b/lib/chat/models/chat_component_state.freezed.dart @@ -1,3 +1,4 @@ +// dart format width=80 // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint @@ -9,44 +10,78 @@ part of 'chat_component_state.dart'; // FreezedGenerator // ************************************************************************** +// dart format off 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'); - /// @nodoc mixin _$ChatComponentState { // GlobalKey for the chat - GlobalKey get chatKey => - throw _privateConstructorUsedError; // ScrollController for the chat - AutoScrollController get scrollController => - throw _privateConstructorUsedError; // TextEditingController for the chat - InputTextFieldController get textEditingController => - throw _privateConstructorUsedError; // Local user - User? get localUser => - throw _privateConstructorUsedError; // Active remote users - IMap, User> get remoteUsers => - throw _privateConstructorUsedError; // Historical remote users - IMap, User> get historicalRemoteUsers => - throw _privateConstructorUsedError; // Unknown users - IMap, User> get unknownUsers => - throw _privateConstructorUsedError; // Messages state - AsyncValue> get messageWindow => - throw _privateConstructorUsedError; // Title of the chat - String get title => throw _privateConstructorUsedError; + GlobalKey get chatKey; // ScrollController for the chat + AutoScrollController + get scrollController; // TextEditingController for the chat + InputTextFieldController get textEditingController; // Local user + User? get localUser; // Active remote users + IMap get remoteUsers; // Historical remote users + IMap get historicalRemoteUsers; // Unknown users + IMap get unknownUsers; // Messages state + AsyncValue> get messageWindow; // Title of the chat + String get title; /// Create a copy of ChatComponentState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') $ChatComponentStateCopyWith get copyWith => - throw _privateConstructorUsedError; + _$ChatComponentStateCopyWithImpl( + this as ChatComponentState, _$identity); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is ChatComponentState && + (identical(other.chatKey, chatKey) || other.chatKey == chatKey) && + (identical(other.scrollController, scrollController) || + other.scrollController == scrollController) && + (identical(other.textEditingController, textEditingController) || + other.textEditingController == textEditingController) && + (identical(other.localUser, localUser) || + other.localUser == localUser) && + (identical(other.remoteUsers, remoteUsers) || + other.remoteUsers == remoteUsers) && + (identical(other.historicalRemoteUsers, historicalRemoteUsers) || + other.historicalRemoteUsers == historicalRemoteUsers) && + (identical(other.unknownUsers, unknownUsers) || + other.unknownUsers == unknownUsers) && + (identical(other.messageWindow, messageWindow) || + other.messageWindow == messageWindow) && + (identical(other.title, title) || other.title == title)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + chatKey, + scrollController, + textEditingController, + localUser, + remoteUsers, + historicalRemoteUsers, + unknownUsers, + messageWindow, + title); + + @override + String toString() { + return 'ChatComponentState(chatKey: $chatKey, scrollController: $scrollController, textEditingController: $textEditingController, localUser: $localUser, remoteUsers: $remoteUsers, historicalRemoteUsers: $historicalRemoteUsers, unknownUsers: $unknownUsers, messageWindow: $messageWindow, title: $title)'; + } } /// @nodoc -abstract class $ChatComponentStateCopyWith<$Res> { +abstract mixin class $ChatComponentStateCopyWith<$Res> { factory $ChatComponentStateCopyWith( - ChatComponentState value, $Res Function(ChatComponentState) then) = - _$ChatComponentStateCopyWithImpl<$Res, ChatComponentState>; + ChatComponentState value, $Res Function(ChatComponentState) _then) = + _$ChatComponentStateCopyWithImpl; @useResult $Res call( {GlobalKey chatKey, @@ -63,14 +98,12 @@ abstract class $ChatComponentStateCopyWith<$Res> { } /// @nodoc -class _$ChatComponentStateCopyWithImpl<$Res, $Val extends ChatComponentState> +class _$ChatComponentStateCopyWithImpl<$Res> implements $ChatComponentStateCopyWith<$Res> { - _$ChatComponentStateCopyWithImpl(this._value, this._then); + _$ChatComponentStateCopyWithImpl(this._self, this._then); - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; + final ChatComponentState _self; + final $Res Function(ChatComponentState) _then; /// Create a copy of ChatComponentState /// with the given fields replaced by the non-null parameter values. @@ -87,44 +120,44 @@ class _$ChatComponentStateCopyWithImpl<$Res, $Val extends ChatComponentState> Object? messageWindow = null, Object? title = null, }) { - return _then(_value.copyWith( + return _then(_self.copyWith( chatKey: null == chatKey - ? _value.chatKey + ? _self.chatKey : chatKey // ignore: cast_nullable_to_non_nullable as GlobalKey, scrollController: null == scrollController - ? _value.scrollController + ? _self.scrollController : scrollController // ignore: cast_nullable_to_non_nullable as AutoScrollController, textEditingController: null == textEditingController - ? _value.textEditingController + ? _self.textEditingController : textEditingController // ignore: cast_nullable_to_non_nullable as InputTextFieldController, localUser: freezed == localUser - ? _value.localUser + ? _self.localUser : localUser // ignore: cast_nullable_to_non_nullable as User?, remoteUsers: null == remoteUsers - ? _value.remoteUsers + ? _self.remoteUsers! : remoteUsers // ignore: cast_nullable_to_non_nullable as IMap, User>, historicalRemoteUsers: null == historicalRemoteUsers - ? _value.historicalRemoteUsers + ? _self.historicalRemoteUsers! : historicalRemoteUsers // ignore: cast_nullable_to_non_nullable as IMap, User>, unknownUsers: null == unknownUsers - ? _value.unknownUsers + ? _self.unknownUsers! : unknownUsers // ignore: cast_nullable_to_non_nullable as IMap, User>, messageWindow: null == messageWindow - ? _value.messageWindow + ? _self.messageWindow : messageWindow // ignore: cast_nullable_to_non_nullable as AsyncValue>, title: null == title - ? _value.title + ? _self.title : title // ignore: cast_nullable_to_non_nullable as String, - ) as $Val); + )); } /// Create a copy of ChatComponentState @@ -132,104 +165,17 @@ class _$ChatComponentStateCopyWithImpl<$Res, $Val extends ChatComponentState> @override @pragma('vm:prefer-inline') $AsyncValueCopyWith, $Res> get messageWindow { - return $AsyncValueCopyWith, $Res>(_value.messageWindow, + return $AsyncValueCopyWith, $Res>(_self.messageWindow, (value) { - return _then(_value.copyWith(messageWindow: value) as $Val); + return _then(_self.copyWith(messageWindow: value)); }); } } /// @nodoc -abstract class _$$ChatComponentStateImplCopyWith<$Res> - implements $ChatComponentStateCopyWith<$Res> { - factory _$$ChatComponentStateImplCopyWith(_$ChatComponentStateImpl value, - $Res Function(_$ChatComponentStateImpl) then) = - __$$ChatComponentStateImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {GlobalKey chatKey, - AutoScrollController scrollController, - InputTextFieldController textEditingController, - User? localUser, - IMap, User> remoteUsers, - IMap, User> historicalRemoteUsers, - IMap, User> unknownUsers, - AsyncValue> messageWindow, - String title}); - @override - $AsyncValueCopyWith, $Res> get messageWindow; -} - -/// @nodoc -class __$$ChatComponentStateImplCopyWithImpl<$Res> - extends _$ChatComponentStateCopyWithImpl<$Res, _$ChatComponentStateImpl> - implements _$$ChatComponentStateImplCopyWith<$Res> { - __$$ChatComponentStateImplCopyWithImpl(_$ChatComponentStateImpl _value, - $Res Function(_$ChatComponentStateImpl) _then) - : super(_value, _then); - - /// Create a copy of ChatComponentState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? chatKey = null, - Object? scrollController = null, - Object? textEditingController = null, - Object? localUser = freezed, - Object? remoteUsers = null, - Object? historicalRemoteUsers = null, - Object? unknownUsers = null, - Object? messageWindow = null, - Object? title = null, - }) { - return _then(_$ChatComponentStateImpl( - chatKey: null == chatKey - ? _value.chatKey - : chatKey // ignore: cast_nullable_to_non_nullable - as GlobalKey, - scrollController: null == scrollController - ? _value.scrollController - : scrollController // ignore: cast_nullable_to_non_nullable - as AutoScrollController, - textEditingController: null == textEditingController - ? _value.textEditingController - : textEditingController // ignore: cast_nullable_to_non_nullable - as InputTextFieldController, - localUser: freezed == localUser - ? _value.localUser - : localUser // ignore: cast_nullable_to_non_nullable - as User?, - remoteUsers: null == remoteUsers - ? _value.remoteUsers - : remoteUsers // ignore: cast_nullable_to_non_nullable - as IMap, User>, - historicalRemoteUsers: null == historicalRemoteUsers - ? _value.historicalRemoteUsers - : historicalRemoteUsers // ignore: cast_nullable_to_non_nullable - as IMap, User>, - unknownUsers: null == unknownUsers - ? _value.unknownUsers - : unknownUsers // ignore: cast_nullable_to_non_nullable - as IMap, User>, - messageWindow: null == messageWindow - ? _value.messageWindow - : messageWindow // ignore: cast_nullable_to_non_nullable - as AsyncValue>, - title: null == title - ? _value.title - : title // ignore: cast_nullable_to_non_nullable - as String, - )); - } -} - -/// @nodoc - -class _$ChatComponentStateImpl implements _ChatComponentState { - const _$ChatComponentStateImpl( +class _ChatComponentState implements ChatComponentState { + const _ChatComponentState( {required this.chatKey, required this.scrollController, required this.textEditingController, @@ -268,16 +214,19 @@ class _$ChatComponentStateImpl implements _ChatComponentState { @override final String title; + /// Create a copy of ChatComponentState + /// with the given fields replaced by the non-null parameter values. @override - String toString() { - return 'ChatComponentState(chatKey: $chatKey, scrollController: $scrollController, textEditingController: $textEditingController, localUser: $localUser, remoteUsers: $remoteUsers, historicalRemoteUsers: $historicalRemoteUsers, unknownUsers: $unknownUsers, messageWindow: $messageWindow, title: $title)'; - } + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + _$ChatComponentStateCopyWith<_ChatComponentState> get copyWith => + __$ChatComponentStateCopyWithImpl<_ChatComponentState>(this, _$identity); @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$ChatComponentStateImpl && + other is _ChatComponentState && (identical(other.chatKey, chatKey) || other.chatKey == chatKey) && (identical(other.scrollController, scrollController) || other.scrollController == scrollController) && @@ -309,56 +258,108 @@ class _$ChatComponentStateImpl implements _ChatComponentState { messageWindow, title); + @override + String toString() { + return 'ChatComponentState(chatKey: $chatKey, scrollController: $scrollController, textEditingController: $textEditingController, localUser: $localUser, remoteUsers: $remoteUsers, historicalRemoteUsers: $historicalRemoteUsers, unknownUsers: $unknownUsers, messageWindow: $messageWindow, title: $title)'; + } +} + +/// @nodoc +abstract mixin class _$ChatComponentStateCopyWith<$Res> + implements $ChatComponentStateCopyWith<$Res> { + factory _$ChatComponentStateCopyWith( + _ChatComponentState value, $Res Function(_ChatComponentState) _then) = + __$ChatComponentStateCopyWithImpl; + @override + @useResult + $Res call( + {GlobalKey chatKey, + AutoScrollController scrollController, + InputTextFieldController textEditingController, + User? localUser, + IMap, User> remoteUsers, + IMap, User> historicalRemoteUsers, + IMap, User> unknownUsers, + AsyncValue> messageWindow, + String title}); + + @override + $AsyncValueCopyWith, $Res> get messageWindow; +} + +/// @nodoc +class __$ChatComponentStateCopyWithImpl<$Res> + implements _$ChatComponentStateCopyWith<$Res> { + __$ChatComponentStateCopyWithImpl(this._self, this._then); + + final _ChatComponentState _self; + final $Res Function(_ChatComponentState) _then; + /// Create a copy of ChatComponentState /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') - _$$ChatComponentStateImplCopyWith<_$ChatComponentStateImpl> get copyWith => - __$$ChatComponentStateImplCopyWithImpl<_$ChatComponentStateImpl>( - this, _$identity); -} - -abstract class _ChatComponentState implements ChatComponentState { - const factory _ChatComponentState( - {required final GlobalKey chatKey, - required final AutoScrollController scrollController, - required final InputTextFieldController textEditingController, - required final User? localUser, - required final IMap, User> remoteUsers, - required final IMap, User> - historicalRemoteUsers, - required final IMap, User> unknownUsers, - required final AsyncValue> messageWindow, - required final String title}) = _$ChatComponentStateImpl; - -// GlobalKey for the chat - @override - GlobalKey get chatKey; // ScrollController for the chat - @override - AutoScrollController - get scrollController; // TextEditingController for the chat - @override - InputTextFieldController get textEditingController; // Local user - @override - User? get localUser; // Active remote users - @override - IMap, User> - get remoteUsers; // Historical remote users - @override - IMap, User> - get historicalRemoteUsers; // Unknown users - @override - IMap, User> get unknownUsers; // Messages state - @override - AsyncValue> get messageWindow; // Title of the chat - @override - String get title; + $Res call({ + Object? chatKey = null, + Object? scrollController = null, + Object? textEditingController = null, + Object? localUser = freezed, + Object? remoteUsers = null, + Object? historicalRemoteUsers = null, + Object? unknownUsers = null, + Object? messageWindow = null, + Object? title = null, + }) { + return _then(_ChatComponentState( + chatKey: null == chatKey + ? _self.chatKey + : chatKey // ignore: cast_nullable_to_non_nullable + as GlobalKey, + scrollController: null == scrollController + ? _self.scrollController + : scrollController // ignore: cast_nullable_to_non_nullable + as AutoScrollController, + textEditingController: null == textEditingController + ? _self.textEditingController + : textEditingController // ignore: cast_nullable_to_non_nullable + as InputTextFieldController, + localUser: freezed == localUser + ? _self.localUser + : localUser // ignore: cast_nullable_to_non_nullable + as User?, + remoteUsers: null == remoteUsers + ? _self.remoteUsers + : remoteUsers // ignore: cast_nullable_to_non_nullable + as IMap, User>, + historicalRemoteUsers: null == historicalRemoteUsers + ? _self.historicalRemoteUsers + : historicalRemoteUsers // ignore: cast_nullable_to_non_nullable + as IMap, User>, + unknownUsers: null == unknownUsers + ? _self.unknownUsers + : unknownUsers // ignore: cast_nullable_to_non_nullable + as IMap, User>, + messageWindow: null == messageWindow + ? _self.messageWindow + : messageWindow // ignore: cast_nullable_to_non_nullable + as AsyncValue>, + title: null == title + ? _self.title + : title // ignore: cast_nullable_to_non_nullable + as String, + )); + } /// Create a copy of ChatComponentState /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$ChatComponentStateImplCopyWith<_$ChatComponentStateImpl> get copyWith => - throw _privateConstructorUsedError; + @pragma('vm:prefer-inline') + $AsyncValueCopyWith, $Res> get messageWindow { + return $AsyncValueCopyWith, $Res>(_self.messageWindow, + (value) { + return _then(_self.copyWith(messageWindow: value)); + }); + } } + +// dart format on diff --git a/lib/chat/models/message_state.dart b/lib/chat/models/message_state.dart index 8eacc8e..cf82021 100644 --- a/lib/chat/models/message_state.dart +++ b/lib/chat/models/message_state.dart @@ -24,7 +24,7 @@ enum MessageSendState { } @freezed -class MessageState with _$MessageState { +sealed class MessageState with _$MessageState { const factory MessageState({ // Content of the message @JsonKey(fromJson: messageFromJson, toJson: messageToJson) diff --git a/lib/chat/models/message_state.freezed.dart b/lib/chat/models/message_state.freezed.dart index baafea6..0900f8b 100644 --- a/lib/chat/models/message_state.freezed.dart +++ b/lib/chat/models/message_state.freezed.dart @@ -1,3 +1,4 @@ +// dart format width=80 // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint @@ -9,99 +10,69 @@ part of 'message_state.dart'; // FreezedGenerator // ************************************************************************** +// dart format off 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'); - -MessageState _$MessageStateFromJson(Map json) { - return _MessageState.fromJson(json); -} - /// @nodoc -mixin _$MessageState { +mixin _$MessageState implements DiagnosticableTreeMixin { // Content of the message @JsonKey(fromJson: messageFromJson, toJson: messageToJson) - proto.Message get content => - throw _privateConstructorUsedError; // Sent timestamp - Timestamp get sentTimestamp => - throw _privateConstructorUsedError; // Reconciled timestamp - Timestamp? get reconciledTimestamp => - throw _privateConstructorUsedError; // The state of the message - MessageSendState? get sendState => throw _privateConstructorUsedError; - - /// Serializes this MessageState to a JSON map. - Map toJson() => throw _privateConstructorUsedError; + proto.Message get content; // Sent timestamp + Timestamp get sentTimestamp; // Reconciled timestamp + Timestamp? get reconciledTimestamp; // The state of the message + MessageSendState? get sendState; /// Create a copy of MessageState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) - $MessageStateCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $MessageStateCopyWith<$Res> { - factory $MessageStateCopyWith( - MessageState value, $Res Function(MessageState) then) = - _$MessageStateCopyWithImpl<$Res, MessageState>; - @useResult - $Res call( - {@JsonKey(fromJson: messageFromJson, toJson: messageToJson) - proto.Message content, - Timestamp sentTimestamp, - Timestamp? reconciledTimestamp, - MessageSendState? sendState}); -} - -/// @nodoc -class _$MessageStateCopyWithImpl<$Res, $Val extends MessageState> - implements $MessageStateCopyWith<$Res> { - _$MessageStateCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of MessageState - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') + $MessageStateCopyWith get copyWith => + _$MessageStateCopyWithImpl( + this as MessageState, _$identity); + + /// Serializes this MessageState to a JSON map. + Map toJson(); + @override - $Res call({ - Object? content = null, - Object? sentTimestamp = null, - Object? reconciledTimestamp = freezed, - Object? sendState = freezed, - }) { - return _then(_value.copyWith( - content: null == content - ? _value.content - : content // ignore: cast_nullable_to_non_nullable - as proto.Message, - sentTimestamp: null == sentTimestamp - ? _value.sentTimestamp - : sentTimestamp // ignore: cast_nullable_to_non_nullable - as Timestamp, - reconciledTimestamp: freezed == reconciledTimestamp - ? _value.reconciledTimestamp - : reconciledTimestamp // ignore: cast_nullable_to_non_nullable - as Timestamp?, - sendState: freezed == sendState - ? _value.sendState - : sendState // ignore: cast_nullable_to_non_nullable - as MessageSendState?, - ) as $Val); + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'MessageState')) + ..add(DiagnosticsProperty('content', content)) + ..add(DiagnosticsProperty('sentTimestamp', sentTimestamp)) + ..add(DiagnosticsProperty('reconciledTimestamp', reconciledTimestamp)) + ..add(DiagnosticsProperty('sendState', sendState)); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is MessageState && + (identical(other.content, content) || other.content == content) && + (identical(other.sentTimestamp, sentTimestamp) || + other.sentTimestamp == sentTimestamp) && + (identical(other.reconciledTimestamp, reconciledTimestamp) || + other.reconciledTimestamp == reconciledTimestamp) && + (identical(other.sendState, sendState) || + other.sendState == sendState)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, content, sentTimestamp, reconciledTimestamp, sendState); + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'MessageState(content: $content, sentTimestamp: $sentTimestamp, reconciledTimestamp: $reconciledTimestamp, sendState: $sendState)'; } } /// @nodoc -abstract class _$$MessageStateImplCopyWith<$Res> - implements $MessageStateCopyWith<$Res> { - factory _$$MessageStateImplCopyWith( - _$MessageStateImpl value, $Res Function(_$MessageStateImpl) then) = - __$$MessageStateImplCopyWithImpl<$Res>; - @override +abstract mixin class $MessageStateCopyWith<$Res> { + factory $MessageStateCopyWith( + MessageState value, $Res Function(MessageState) _then) = + _$MessageStateCopyWithImpl; @useResult $Res call( {@JsonKey(fromJson: messageFromJson, toJson: messageToJson) @@ -112,12 +83,11 @@ abstract class _$$MessageStateImplCopyWith<$Res> } /// @nodoc -class __$$MessageStateImplCopyWithImpl<$Res> - extends _$MessageStateCopyWithImpl<$Res, _$MessageStateImpl> - implements _$$MessageStateImplCopyWith<$Res> { - __$$MessageStateImplCopyWithImpl( - _$MessageStateImpl _value, $Res Function(_$MessageStateImpl) _then) - : super(_value, _then); +class _$MessageStateCopyWithImpl<$Res> implements $MessageStateCopyWith<$Res> { + _$MessageStateCopyWithImpl(this._self, this._then); + + final MessageState _self; + final $Res Function(MessageState) _then; /// Create a copy of MessageState /// with the given fields replaced by the non-null parameter values. @@ -129,21 +99,21 @@ class __$$MessageStateImplCopyWithImpl<$Res> Object? reconciledTimestamp = freezed, Object? sendState = freezed, }) { - return _then(_$MessageStateImpl( + return _then(_self.copyWith( content: null == content - ? _value.content + ? _self.content : content // ignore: cast_nullable_to_non_nullable as proto.Message, sentTimestamp: null == sentTimestamp - ? _value.sentTimestamp + ? _self.sentTimestamp : sentTimestamp // ignore: cast_nullable_to_non_nullable as Timestamp, reconciledTimestamp: freezed == reconciledTimestamp - ? _value.reconciledTimestamp + ? _self.reconciledTimestamp : reconciledTimestamp // ignore: cast_nullable_to_non_nullable as Timestamp?, sendState: freezed == sendState - ? _value.sendState + ? _self.sendState : sendState // ignore: cast_nullable_to_non_nullable as MessageSendState?, )); @@ -152,16 +122,15 @@ class __$$MessageStateImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() -class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { - const _$MessageStateImpl( +class _MessageState with DiagnosticableTreeMixin implements MessageState { + const _MessageState( {@JsonKey(fromJson: messageFromJson, toJson: messageToJson) required this.content, required this.sentTimestamp, required this.reconciledTimestamp, required this.sendState}); - - factory _$MessageStateImpl.fromJson(Map json) => - _$$MessageStateImplFromJson(json); + factory _MessageState.fromJson(Map json) => + _$MessageStateFromJson(json); // Content of the message @override @@ -177,14 +146,23 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { @override final MessageSendState? sendState; + /// Create a copy of MessageState + /// with the given fields replaced by the non-null parameter values. @override - String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'MessageState(content: $content, sentTimestamp: $sentTimestamp, reconciledTimestamp: $reconciledTimestamp, sendState: $sendState)'; + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + _$MessageStateCopyWith<_MessageState> get copyWith => + __$MessageStateCopyWithImpl<_MessageState>(this, _$identity); + + @override + Map toJson() { + return _$MessageStateToJson( + this, + ); } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('type', 'MessageState')) ..add(DiagnosticsProperty('content', content)) @@ -197,7 +175,7 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$MessageStateImpl && + other is _MessageState && (identical(other.content, content) || other.content == content) && (identical(other.sentTimestamp, sentTimestamp) || other.sentTimestamp == sentTimestamp) && @@ -212,48 +190,65 @@ class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState { int get hashCode => Object.hash( runtimeType, content, sentTimestamp, reconciledTimestamp, sendState); - /// Create a copy of MessageState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) @override - @pragma('vm:prefer-inline') - _$$MessageStateImplCopyWith<_$MessageStateImpl> get copyWith => - __$$MessageStateImplCopyWithImpl<_$MessageStateImpl>(this, _$identity); - - @override - Map toJson() { - return _$$MessageStateImplToJson( - this, - ); + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'MessageState(content: $content, sentTimestamp: $sentTimestamp, reconciledTimestamp: $reconciledTimestamp, sendState: $sendState)'; } } -abstract class _MessageState implements MessageState { - const factory _MessageState( +/// @nodoc +abstract mixin class _$MessageStateCopyWith<$Res> + implements $MessageStateCopyWith<$Res> { + factory _$MessageStateCopyWith( + _MessageState value, $Res Function(_MessageState) _then) = + __$MessageStateCopyWithImpl; + @override + @useResult + $Res call( {@JsonKey(fromJson: messageFromJson, toJson: messageToJson) - required final proto.Message content, - required final Timestamp sentTimestamp, - required final Timestamp? reconciledTimestamp, - required final MessageSendState? sendState}) = _$MessageStateImpl; + proto.Message content, + Timestamp sentTimestamp, + Timestamp? reconciledTimestamp, + MessageSendState? sendState}); +} - factory _MessageState.fromJson(Map json) = - _$MessageStateImpl.fromJson; +/// @nodoc +class __$MessageStateCopyWithImpl<$Res> + implements _$MessageStateCopyWith<$Res> { + __$MessageStateCopyWithImpl(this._self, this._then); -// Content of the message - @override - @JsonKey(fromJson: messageFromJson, toJson: messageToJson) - proto.Message get content; // Sent timestamp - @override - Timestamp get sentTimestamp; // Reconciled timestamp - @override - Timestamp? get reconciledTimestamp; // The state of the message - @override - MessageSendState? get sendState; + final _MessageState _self; + final $Res Function(_MessageState) _then; /// Create a copy of MessageState /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$MessageStateImplCopyWith<_$MessageStateImpl> get copyWith => - throw _privateConstructorUsedError; + @pragma('vm:prefer-inline') + $Res call({ + Object? content = null, + Object? sentTimestamp = null, + Object? reconciledTimestamp = freezed, + Object? sendState = freezed, + }) { + return _then(_MessageState( + content: null == content + ? _self.content + : content // ignore: cast_nullable_to_non_nullable + as proto.Message, + sentTimestamp: null == sentTimestamp + ? _self.sentTimestamp + : sentTimestamp // ignore: cast_nullable_to_non_nullable + as Timestamp, + reconciledTimestamp: freezed == reconciledTimestamp + ? _self.reconciledTimestamp + : reconciledTimestamp // ignore: cast_nullable_to_non_nullable + as Timestamp?, + sendState: freezed == sendState + ? _self.sendState + : sendState // ignore: cast_nullable_to_non_nullable + as MessageSendState?, + )); + } } + +// dart format on diff --git a/lib/chat/models/message_state.g.dart b/lib/chat/models/message_state.g.dart index 99899a7..daae37f 100644 --- a/lib/chat/models/message_state.g.dart +++ b/lib/chat/models/message_state.g.dart @@ -6,8 +6,8 @@ part of 'message_state.dart'; // JsonSerializableGenerator // ************************************************************************** -_$MessageStateImpl _$$MessageStateImplFromJson(Map json) => - _$MessageStateImpl( +_MessageState _$MessageStateFromJson(Map json) => + _MessageState( content: messageFromJson(json['content'] as Map), sentTimestamp: Timestamp.fromJson(json['sent_timestamp']), reconciledTimestamp: json['reconciled_timestamp'] == null @@ -18,7 +18,7 @@ _$MessageStateImpl _$$MessageStateImplFromJson(Map json) => : MessageSendState.fromJson(json['send_state']), ); -Map _$$MessageStateImplToJson(_$MessageStateImpl instance) => +Map _$MessageStateToJson(_MessageState instance) => { 'content': messageToJson(instance.content), 'sent_timestamp': instance.sentTimestamp.toJson(), diff --git a/lib/chat/models/window_state.dart b/lib/chat/models/window_state.dart index 91cde8a..14a94a5 100644 --- a/lib/chat/models/window_state.dart +++ b/lib/chat/models/window_state.dart @@ -5,7 +5,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'window_state.freezed.dart'; @freezed -class WindowState with _$WindowState { +sealed class WindowState with _$WindowState { const factory WindowState({ // List of objects in the window required IList window, diff --git a/lib/chat/models/window_state.freezed.dart b/lib/chat/models/window_state.freezed.dart index 59ff754..38a2ec1 100644 --- a/lib/chat/models/window_state.freezed.dart +++ b/lib/chat/models/window_state.freezed.dart @@ -1,3 +1,4 @@ +// dart format width=80 // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint @@ -9,98 +10,71 @@ part of 'window_state.dart'; // FreezedGenerator // ************************************************************************** +// dart format off 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'); - /// @nodoc -mixin _$WindowState { +mixin _$WindowState implements DiagnosticableTreeMixin { // List of objects in the window - IList get window => - throw _privateConstructorUsedError; // Total number of objects (windowTail max) - int get length => - throw _privateConstructorUsedError; // One past the end of the last element - int get windowTail => - throw _privateConstructorUsedError; // The total number of elements to try to keep in the window - int get windowCount => - throw _privateConstructorUsedError; // If we should have the tail following the array - bool get follow => throw _privateConstructorUsedError; + IList get window; // Total number of objects (windowTail max) + int get length; // One past the end of the last element + int get windowTail; // The total number of elements to try to keep in the window + int get windowCount; // If we should have the tail following the array + bool get follow; /// Create a copy of WindowState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) - $WindowStateCopyWith> get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $WindowStateCopyWith { - factory $WindowStateCopyWith( - WindowState value, $Res Function(WindowState) then) = - _$WindowStateCopyWithImpl>; - @useResult - $Res call( - {IList window, - int length, - int windowTail, - int windowCount, - bool follow}); -} - -/// @nodoc -class _$WindowStateCopyWithImpl> - implements $WindowStateCopyWith { - _$WindowStateCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of WindowState - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') + $WindowStateCopyWith> get copyWith => + _$WindowStateCopyWithImpl>( + this as WindowState, _$identity); + @override - $Res call({ - Object? window = null, - Object? length = null, - Object? windowTail = null, - Object? windowCount = null, - Object? follow = null, - }) { - return _then(_value.copyWith( - window: null == window - ? _value.window - : window // ignore: cast_nullable_to_non_nullable - as IList, - length: null == length - ? _value.length - : length // ignore: cast_nullable_to_non_nullable - as int, - windowTail: null == windowTail - ? _value.windowTail - : windowTail // ignore: cast_nullable_to_non_nullable - as int, - windowCount: null == windowCount - ? _value.windowCount - : windowCount // ignore: cast_nullable_to_non_nullable - as int, - follow: null == follow - ? _value.follow - : follow // ignore: cast_nullable_to_non_nullable - as bool, - ) as $Val); + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'WindowState<$T>')) + ..add(DiagnosticsProperty('window', window)) + ..add(DiagnosticsProperty('length', length)) + ..add(DiagnosticsProperty('windowTail', windowTail)) + ..add(DiagnosticsProperty('windowCount', windowCount)) + ..add(DiagnosticsProperty('follow', follow)); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is WindowState && + const DeepCollectionEquality().equals(other.window, window) && + (identical(other.length, length) || other.length == length) && + (identical(other.windowTail, windowTail) || + other.windowTail == windowTail) && + (identical(other.windowCount, windowCount) || + other.windowCount == windowCount) && + (identical(other.follow, follow) || other.follow == follow)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(window), + length, + windowTail, + windowCount, + follow); + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'WindowState<$T>(window: $window, length: $length, windowTail: $windowTail, windowCount: $windowCount, follow: $follow)'; } } /// @nodoc -abstract class _$$WindowStateImplCopyWith - implements $WindowStateCopyWith { - factory _$$WindowStateImplCopyWith(_$WindowStateImpl value, - $Res Function(_$WindowStateImpl) then) = - __$$WindowStateImplCopyWithImpl; - @override +abstract mixin class $WindowStateCopyWith { + factory $WindowStateCopyWith( + WindowState value, $Res Function(WindowState) _then) = + _$WindowStateCopyWithImpl; @useResult $Res call( {IList window, @@ -111,12 +85,12 @@ abstract class _$$WindowStateImplCopyWith } /// @nodoc -class __$$WindowStateImplCopyWithImpl - extends _$WindowStateCopyWithImpl> - implements _$$WindowStateImplCopyWith { - __$$WindowStateImplCopyWithImpl( - _$WindowStateImpl _value, $Res Function(_$WindowStateImpl) _then) - : super(_value, _then); +class _$WindowStateCopyWithImpl + implements $WindowStateCopyWith { + _$WindowStateCopyWithImpl(this._self, this._then); + + final WindowState _self; + final $Res Function(WindowState) _then; /// Create a copy of WindowState /// with the given fields replaced by the non-null parameter values. @@ -129,25 +103,25 @@ class __$$WindowStateImplCopyWithImpl Object? windowCount = null, Object? follow = null, }) { - return _then(_$WindowStateImpl( + return _then(_self.copyWith( window: null == window - ? _value.window + ? _self.window : window // ignore: cast_nullable_to_non_nullable as IList, length: null == length - ? _value.length + ? _self.length : length // ignore: cast_nullable_to_non_nullable as int, windowTail: null == windowTail - ? _value.windowTail + ? _self.windowTail : windowTail // ignore: cast_nullable_to_non_nullable as int, windowCount: null == windowCount - ? _value.windowCount + ? _self.windowCount : windowCount // ignore: cast_nullable_to_non_nullable as int, follow: null == follow - ? _value.follow + ? _self.follow : follow // ignore: cast_nullable_to_non_nullable as bool, )); @@ -156,10 +130,8 @@ class __$$WindowStateImplCopyWithImpl /// @nodoc -class _$WindowStateImpl - with DiagnosticableTreeMixin - implements _WindowState { - const _$WindowStateImpl( +class _WindowState with DiagnosticableTreeMixin implements WindowState { + const _WindowState( {required this.window, required this.length, required this.windowTail, @@ -182,14 +154,16 @@ class _$WindowStateImpl @override final bool follow; + /// Create a copy of WindowState + /// with the given fields replaced by the non-null parameter values. @override - String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'WindowState<$T>(window: $window, length: $length, windowTail: $windowTail, windowCount: $windowCount, follow: $follow)'; - } + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + _$WindowStateCopyWith> get copyWith => + __$WindowStateCopyWithImpl>(this, _$identity); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('type', 'WindowState<$T>')) ..add(DiagnosticsProperty('window', window)) @@ -203,7 +177,7 @@ class _$WindowStateImpl bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$WindowStateImpl && + other is _WindowState && const DeepCollectionEquality().equals(other.window, window) && (identical(other.length, length) || other.length == length) && (identical(other.windowTail, windowTail) || @@ -222,40 +196,70 @@ class _$WindowStateImpl windowCount, follow); + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'WindowState<$T>(window: $window, length: $length, windowTail: $windowTail, windowCount: $windowCount, follow: $follow)'; + } +} + +/// @nodoc +abstract mixin class _$WindowStateCopyWith + implements $WindowStateCopyWith { + factory _$WindowStateCopyWith( + _WindowState value, $Res Function(_WindowState) _then) = + __$WindowStateCopyWithImpl; + @override + @useResult + $Res call( + {IList window, + int length, + int windowTail, + int windowCount, + bool follow}); +} + +/// @nodoc +class __$WindowStateCopyWithImpl + implements _$WindowStateCopyWith { + __$WindowStateCopyWithImpl(this._self, this._then); + + final _WindowState _self; + final $Res Function(_WindowState) _then; + /// Create a copy of WindowState /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') - _$$WindowStateImplCopyWith> get copyWith => - __$$WindowStateImplCopyWithImpl>( - this, _$identity); + $Res call({ + Object? window = null, + Object? length = null, + Object? windowTail = null, + Object? windowCount = null, + Object? follow = null, + }) { + return _then(_WindowState( + window: null == window + ? _self.window + : window // ignore: cast_nullable_to_non_nullable + as IList, + length: null == length + ? _self.length + : length // ignore: cast_nullable_to_non_nullable + as int, + windowTail: null == windowTail + ? _self.windowTail + : windowTail // ignore: cast_nullable_to_non_nullable + as int, + windowCount: null == windowCount + ? _self.windowCount + : windowCount // ignore: cast_nullable_to_non_nullable + as int, + follow: null == follow + ? _self.follow + : follow // ignore: cast_nullable_to_non_nullable + as bool, + )); + } } -abstract class _WindowState implements WindowState { - const factory _WindowState( - {required final IList window, - required final int length, - required final int windowTail, - required final int windowCount, - required final bool follow}) = _$WindowStateImpl; - -// List of objects in the window - @override - IList get window; // Total number of objects (windowTail max) - @override - int get length; // One past the end of the last element - @override - int get windowTail; // The total number of elements to try to keep in the window - @override - int get windowCount; // If we should have the tail following the array - @override - bool get follow; - - /// Create a copy of WindowState - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$WindowStateImplCopyWith> get copyWith => - throw _privateConstructorUsedError; -} +// dart format on diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index 3ae3db1..ae31f29 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -13,7 +13,7 @@ import '../../proto/proto.dart' as proto; ////////////////////////////////////////////////// // Mutable state for per-account chat list -typedef ChatListCubitState = DHTShortArrayBusyState; +typedef ChatListCubitState = DHTShortArrayCubitState; class ChatListCubit extends DHTShortArrayCubit with StateMapFollowable { diff --git a/lib/chat_list/views/chat_list_widget.dart b/lib/chat_list/views/chat_list_widget.dart index 73bd7e6..cce416c 100644 --- a/lib/chat_list/views/chat_list_widget.dart +++ b/lib/chat_list/views/chat_list_widget.dart @@ -8,7 +8,6 @@ import 'package:veilid_support/veilid_support.dart'; import '../../contacts/contacts.dart'; import '../../proto/proto.dart' as proto; -import '../../proto/proto.dart'; import '../../theme/theme.dart'; import '../chat_list.dart'; @@ -26,7 +25,7 @@ class ChatListWidget extends StatelessWidget { } List _itemFilter(IMap contactMap, - IList> chatList, String filter) { + IList> chatList, String filter) { final lowerValue = filter.toLowerCase(); return chatList.map((x) => x.value).where((c) { switch (c.whichKind()) { @@ -48,7 +47,6 @@ class ChatListWidget extends StatelessWidget { } @override - // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { final contactListV = context.watch().state; diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index 768cf2f..332341d 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -27,7 +27,7 @@ typedef GetEncryptionKeyCallback = Future Function( ////////////////////////////////////////////////// typedef ContactInvitiationListState - = DHTShortArrayBusyState; + = DHTShortArrayCubitState; ////////////////////////////////////////////////// // Mutable state for per-account contact invitations 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 953575d..197ba8c 100644 --- a/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart @@ -18,7 +18,7 @@ typedef WaitingInvitationsBlocMapState class WaitingInvitationsBlocMapCubit extends BlocMapCubit, WaitingInvitationCubit> with - StateMapFollower, + StateMapFollower, TypedKey, proto.ContactInvitationRecord> { WaitingInvitationsBlocMapCubit( {required AccountInfo accountInfo, diff --git a/lib/notifications/models/notifications_preference.dart b/lib/notifications/models/notifications_preference.dart index 35385c6..913cb25 100644 --- a/lib/notifications/models/notifications_preference.dart +++ b/lib/notifications/models/notifications_preference.dart @@ -5,7 +5,7 @@ part 'notifications_preference.freezed.dart'; part 'notifications_preference.g.dart'; @freezed -class NotificationsPreference with _$NotificationsPreference { +sealed class NotificationsPreference with _$NotificationsPreference { const factory NotificationsPreference({ @Default(true) bool displayBetaWarning, @Default(true) bool enableBadge, diff --git a/lib/notifications/models/notifications_preference.freezed.dart b/lib/notifications/models/notifications_preference.freezed.dart index 0335ad8..55f700a 100644 --- a/lib/notifications/models/notifications_preference.freezed.dart +++ b/lib/notifications/models/notifications_preference.freezed.dart @@ -1,3 +1,4 @@ +// dart format width=80 // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint @@ -9,270 +10,37 @@ part of 'notifications_preference.dart'; // FreezedGenerator // ************************************************************************** +// dart format off 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'); - -NotificationsPreference _$NotificationsPreferenceFromJson( - Map json) { - return _NotificationsPreference.fromJson(json); -} - /// @nodoc mixin _$NotificationsPreference { - bool get displayBetaWarning => throw _privateConstructorUsedError; - bool get enableBadge => throw _privateConstructorUsedError; - bool get enableNotifications => throw _privateConstructorUsedError; - MessageNotificationContent get messageNotificationContent => - throw _privateConstructorUsedError; - NotificationMode get onInvitationAcceptedMode => - throw _privateConstructorUsedError; - SoundEffect get onInvitationAcceptedSound => - throw _privateConstructorUsedError; - NotificationMode get onMessageReceivedMode => - throw _privateConstructorUsedError; - SoundEffect get onMessageReceivedSound => throw _privateConstructorUsedError; - SoundEffect get onMessageSentSound => throw _privateConstructorUsedError; - - /// Serializes this NotificationsPreference to a JSON map. - Map toJson() => throw _privateConstructorUsedError; + bool get displayBetaWarning; + bool get enableBadge; + bool get enableNotifications; + MessageNotificationContent get messageNotificationContent; + NotificationMode get onInvitationAcceptedMode; + SoundEffect get onInvitationAcceptedSound; + NotificationMode get onMessageReceivedMode; + SoundEffect get onMessageReceivedSound; + SoundEffect get onMessageSentSound; /// Create a copy of NotificationsPreference /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') $NotificationsPreferenceCopyWith get copyWith => - throw _privateConstructorUsedError; -} + _$NotificationsPreferenceCopyWithImpl( + this as NotificationsPreference, _$identity); -/// @nodoc -abstract class $NotificationsPreferenceCopyWith<$Res> { - factory $NotificationsPreferenceCopyWith(NotificationsPreference value, - $Res Function(NotificationsPreference) then) = - _$NotificationsPreferenceCopyWithImpl<$Res, NotificationsPreference>; - @useResult - $Res call( - {bool displayBetaWarning, - bool enableBadge, - bool enableNotifications, - MessageNotificationContent messageNotificationContent, - NotificationMode onInvitationAcceptedMode, - SoundEffect onInvitationAcceptedSound, - NotificationMode onMessageReceivedMode, - SoundEffect onMessageReceivedSound, - SoundEffect onMessageSentSound}); -} - -/// @nodoc -class _$NotificationsPreferenceCopyWithImpl<$Res, - $Val extends NotificationsPreference> - implements $NotificationsPreferenceCopyWith<$Res> { - _$NotificationsPreferenceCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of NotificationsPreference - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? displayBetaWarning = null, - Object? enableBadge = null, - Object? enableNotifications = null, - Object? messageNotificationContent = null, - Object? onInvitationAcceptedMode = null, - Object? onInvitationAcceptedSound = null, - Object? onMessageReceivedMode = null, - Object? onMessageReceivedSound = null, - Object? onMessageSentSound = null, - }) { - return _then(_value.copyWith( - displayBetaWarning: null == displayBetaWarning - ? _value.displayBetaWarning - : displayBetaWarning // ignore: cast_nullable_to_non_nullable - as bool, - enableBadge: null == enableBadge - ? _value.enableBadge - : enableBadge // ignore: cast_nullable_to_non_nullable - as bool, - enableNotifications: null == enableNotifications - ? _value.enableNotifications - : enableNotifications // ignore: cast_nullable_to_non_nullable - as bool, - messageNotificationContent: null == messageNotificationContent - ? _value.messageNotificationContent - : messageNotificationContent // ignore: cast_nullable_to_non_nullable - as MessageNotificationContent, - onInvitationAcceptedMode: null == onInvitationAcceptedMode - ? _value.onInvitationAcceptedMode - : onInvitationAcceptedMode // ignore: cast_nullable_to_non_nullable - as NotificationMode, - onInvitationAcceptedSound: null == onInvitationAcceptedSound - ? _value.onInvitationAcceptedSound - : onInvitationAcceptedSound // ignore: cast_nullable_to_non_nullable - as SoundEffect, - onMessageReceivedMode: null == onMessageReceivedMode - ? _value.onMessageReceivedMode - : onMessageReceivedMode // ignore: cast_nullable_to_non_nullable - as NotificationMode, - onMessageReceivedSound: null == onMessageReceivedSound - ? _value.onMessageReceivedSound - : onMessageReceivedSound // ignore: cast_nullable_to_non_nullable - as SoundEffect, - onMessageSentSound: null == onMessageSentSound - ? _value.onMessageSentSound - : onMessageSentSound // ignore: cast_nullable_to_non_nullable - as SoundEffect, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$NotificationsPreferenceImplCopyWith<$Res> - implements $NotificationsPreferenceCopyWith<$Res> { - factory _$$NotificationsPreferenceImplCopyWith( - _$NotificationsPreferenceImpl value, - $Res Function(_$NotificationsPreferenceImpl) then) = - __$$NotificationsPreferenceImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {bool displayBetaWarning, - bool enableBadge, - bool enableNotifications, - MessageNotificationContent messageNotificationContent, - NotificationMode onInvitationAcceptedMode, - SoundEffect onInvitationAcceptedSound, - NotificationMode onMessageReceivedMode, - SoundEffect onMessageReceivedSound, - SoundEffect onMessageSentSound}); -} - -/// @nodoc -class __$$NotificationsPreferenceImplCopyWithImpl<$Res> - extends _$NotificationsPreferenceCopyWithImpl<$Res, - _$NotificationsPreferenceImpl> - implements _$$NotificationsPreferenceImplCopyWith<$Res> { - __$$NotificationsPreferenceImplCopyWithImpl( - _$NotificationsPreferenceImpl _value, - $Res Function(_$NotificationsPreferenceImpl) _then) - : super(_value, _then); - - /// Create a copy of NotificationsPreference - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? displayBetaWarning = null, - Object? enableBadge = null, - Object? enableNotifications = null, - Object? messageNotificationContent = null, - Object? onInvitationAcceptedMode = null, - Object? onInvitationAcceptedSound = null, - Object? onMessageReceivedMode = null, - Object? onMessageReceivedSound = null, - Object? onMessageSentSound = null, - }) { - return _then(_$NotificationsPreferenceImpl( - displayBetaWarning: null == displayBetaWarning - ? _value.displayBetaWarning - : displayBetaWarning // ignore: cast_nullable_to_non_nullable - as bool, - enableBadge: null == enableBadge - ? _value.enableBadge - : enableBadge // ignore: cast_nullable_to_non_nullable - as bool, - enableNotifications: null == enableNotifications - ? _value.enableNotifications - : enableNotifications // ignore: cast_nullable_to_non_nullable - as bool, - messageNotificationContent: null == messageNotificationContent - ? _value.messageNotificationContent - : messageNotificationContent // ignore: cast_nullable_to_non_nullable - as MessageNotificationContent, - onInvitationAcceptedMode: null == onInvitationAcceptedMode - ? _value.onInvitationAcceptedMode - : onInvitationAcceptedMode // ignore: cast_nullable_to_non_nullable - as NotificationMode, - onInvitationAcceptedSound: null == onInvitationAcceptedSound - ? _value.onInvitationAcceptedSound - : onInvitationAcceptedSound // ignore: cast_nullable_to_non_nullable - as SoundEffect, - onMessageReceivedMode: null == onMessageReceivedMode - ? _value.onMessageReceivedMode - : onMessageReceivedMode // ignore: cast_nullable_to_non_nullable - as NotificationMode, - onMessageReceivedSound: null == onMessageReceivedSound - ? _value.onMessageReceivedSound - : onMessageReceivedSound // ignore: cast_nullable_to_non_nullable - as SoundEffect, - onMessageSentSound: null == onMessageSentSound - ? _value.onMessageSentSound - : onMessageSentSound // ignore: cast_nullable_to_non_nullable - as SoundEffect, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$NotificationsPreferenceImpl implements _NotificationsPreference { - const _$NotificationsPreferenceImpl( - {this.displayBetaWarning = true, - this.enableBadge = true, - this.enableNotifications = true, - this.messageNotificationContent = - MessageNotificationContent.nameAndContent, - this.onInvitationAcceptedMode = NotificationMode.inAppOrPush, - this.onInvitationAcceptedSound = SoundEffect.beepBaDeep, - this.onMessageReceivedMode = NotificationMode.inAppOrPush, - this.onMessageReceivedSound = SoundEffect.boop, - this.onMessageSentSound = SoundEffect.bonk}); - - factory _$NotificationsPreferenceImpl.fromJson(Map json) => - _$$NotificationsPreferenceImplFromJson(json); - - @override - @JsonKey() - final bool displayBetaWarning; - @override - @JsonKey() - final bool enableBadge; - @override - @JsonKey() - final bool enableNotifications; - @override - @JsonKey() - final MessageNotificationContent messageNotificationContent; - @override - @JsonKey() - final NotificationMode onInvitationAcceptedMode; - @override - @JsonKey() - final SoundEffect onInvitationAcceptedSound; - @override - @JsonKey() - final NotificationMode onMessageReceivedMode; - @override - @JsonKey() - final SoundEffect onMessageReceivedSound; - @override - @JsonKey() - final SoundEffect onMessageSentSound; - - @override - String toString() { - return 'NotificationsPreference(displayBetaWarning: $displayBetaWarning, enableBadge: $enableBadge, enableNotifications: $enableNotifications, messageNotificationContent: $messageNotificationContent, onInvitationAcceptedMode: $onInvitationAcceptedMode, onInvitationAcceptedSound: $onInvitationAcceptedSound, onMessageReceivedMode: $onMessageReceivedMode, onMessageReceivedSound: $onMessageReceivedSound, onMessageSentSound: $onMessageSentSound)'; - } + /// Serializes this NotificationsPreference to a JSON map. + Map toJson(); @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$NotificationsPreferenceImpl && + other is NotificationsPreference && (identical(other.displayBetaWarning, displayBetaWarning) || other.displayBetaWarning == displayBetaWarning) && (identical(other.enableBadge, enableBadge) || @@ -311,61 +79,286 @@ class _$NotificationsPreferenceImpl implements _NotificationsPreference { onMessageReceivedSound, onMessageSentSound); - /// Create a copy of NotificationsPreference - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) @override - @pragma('vm:prefer-inline') - _$$NotificationsPreferenceImplCopyWith<_$NotificationsPreferenceImpl> - get copyWith => __$$NotificationsPreferenceImplCopyWithImpl< - _$NotificationsPreferenceImpl>(this, _$identity); - - @override - Map toJson() { - return _$$NotificationsPreferenceImplToJson( - this, - ); + String toString() { + return 'NotificationsPreference(displayBetaWarning: $displayBetaWarning, enableBadge: $enableBadge, enableNotifications: $enableNotifications, messageNotificationContent: $messageNotificationContent, onInvitationAcceptedMode: $onInvitationAcceptedMode, onInvitationAcceptedSound: $onInvitationAcceptedSound, onMessageReceivedMode: $onMessageReceivedMode, onMessageReceivedSound: $onMessageReceivedSound, onMessageSentSound: $onMessageSentSound)'; } } -abstract class _NotificationsPreference implements NotificationsPreference { - const factory _NotificationsPreference( - {final bool displayBetaWarning, - final bool enableBadge, - final bool enableNotifications, - final MessageNotificationContent messageNotificationContent, - final NotificationMode onInvitationAcceptedMode, - final SoundEffect onInvitationAcceptedSound, - final NotificationMode onMessageReceivedMode, - final SoundEffect onMessageReceivedSound, - final SoundEffect onMessageSentSound}) = _$NotificationsPreferenceImpl; +/// @nodoc +abstract mixin class $NotificationsPreferenceCopyWith<$Res> { + factory $NotificationsPreferenceCopyWith(NotificationsPreference value, + $Res Function(NotificationsPreference) _then) = + _$NotificationsPreferenceCopyWithImpl; + @useResult + $Res call( + {bool displayBetaWarning, + bool enableBadge, + bool enableNotifications, + MessageNotificationContent messageNotificationContent, + NotificationMode onInvitationAcceptedMode, + SoundEffect onInvitationAcceptedSound, + NotificationMode onMessageReceivedMode, + SoundEffect onMessageReceivedSound, + SoundEffect onMessageSentSound}); +} - factory _NotificationsPreference.fromJson(Map json) = - _$NotificationsPreferenceImpl.fromJson; +/// @nodoc +class _$NotificationsPreferenceCopyWithImpl<$Res> + implements $NotificationsPreferenceCopyWith<$Res> { + _$NotificationsPreferenceCopyWithImpl(this._self, this._then); + + final NotificationsPreference _self; + final $Res Function(NotificationsPreference) _then; + + /// Create a copy of NotificationsPreference + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? displayBetaWarning = null, + Object? enableBadge = null, + Object? enableNotifications = null, + Object? messageNotificationContent = null, + Object? onInvitationAcceptedMode = null, + Object? onInvitationAcceptedSound = null, + Object? onMessageReceivedMode = null, + Object? onMessageReceivedSound = null, + Object? onMessageSentSound = null, + }) { + return _then(_self.copyWith( + displayBetaWarning: null == displayBetaWarning + ? _self.displayBetaWarning + : displayBetaWarning // ignore: cast_nullable_to_non_nullable + as bool, + enableBadge: null == enableBadge + ? _self.enableBadge + : enableBadge // ignore: cast_nullable_to_non_nullable + as bool, + enableNotifications: null == enableNotifications + ? _self.enableNotifications + : enableNotifications // ignore: cast_nullable_to_non_nullable + as bool, + messageNotificationContent: null == messageNotificationContent + ? _self.messageNotificationContent + : messageNotificationContent // ignore: cast_nullable_to_non_nullable + as MessageNotificationContent, + onInvitationAcceptedMode: null == onInvitationAcceptedMode + ? _self.onInvitationAcceptedMode + : onInvitationAcceptedMode // ignore: cast_nullable_to_non_nullable + as NotificationMode, + onInvitationAcceptedSound: null == onInvitationAcceptedSound + ? _self.onInvitationAcceptedSound + : onInvitationAcceptedSound // ignore: cast_nullable_to_non_nullable + as SoundEffect, + onMessageReceivedMode: null == onMessageReceivedMode + ? _self.onMessageReceivedMode + : onMessageReceivedMode // ignore: cast_nullable_to_non_nullable + as NotificationMode, + onMessageReceivedSound: null == onMessageReceivedSound + ? _self.onMessageReceivedSound + : onMessageReceivedSound // ignore: cast_nullable_to_non_nullable + as SoundEffect, + onMessageSentSound: null == onMessageSentSound + ? _self.onMessageSentSound + : onMessageSentSound // ignore: cast_nullable_to_non_nullable + as SoundEffect, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _NotificationsPreference implements NotificationsPreference { + const _NotificationsPreference( + {this.displayBetaWarning = true, + this.enableBadge = true, + this.enableNotifications = true, + this.messageNotificationContent = + MessageNotificationContent.nameAndContent, + this.onInvitationAcceptedMode = NotificationMode.inAppOrPush, + this.onInvitationAcceptedSound = SoundEffect.beepBaDeep, + this.onMessageReceivedMode = NotificationMode.inAppOrPush, + this.onMessageReceivedSound = SoundEffect.boop, + this.onMessageSentSound = SoundEffect.bonk}); + factory _NotificationsPreference.fromJson(Map json) => + _$NotificationsPreferenceFromJson(json); @override - bool get displayBetaWarning; + @JsonKey() + final bool displayBetaWarning; @override - bool get enableBadge; + @JsonKey() + final bool enableBadge; @override - bool get enableNotifications; + @JsonKey() + final bool enableNotifications; @override - MessageNotificationContent get messageNotificationContent; + @JsonKey() + final MessageNotificationContent messageNotificationContent; @override - NotificationMode get onInvitationAcceptedMode; + @JsonKey() + final NotificationMode onInvitationAcceptedMode; @override - SoundEffect get onInvitationAcceptedSound; + @JsonKey() + final SoundEffect onInvitationAcceptedSound; @override - NotificationMode get onMessageReceivedMode; + @JsonKey() + final NotificationMode onMessageReceivedMode; @override - SoundEffect get onMessageReceivedSound; + @JsonKey() + final SoundEffect onMessageReceivedSound; @override - SoundEffect get onMessageSentSound; + @JsonKey() + final SoundEffect onMessageSentSound; /// Create a copy of NotificationsPreference /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) - _$$NotificationsPreferenceImplCopyWith<_$NotificationsPreferenceImpl> - get copyWith => throw _privateConstructorUsedError; + @pragma('vm:prefer-inline') + _$NotificationsPreferenceCopyWith<_NotificationsPreference> get copyWith => + __$NotificationsPreferenceCopyWithImpl<_NotificationsPreference>( + this, _$identity); + + @override + Map toJson() { + return _$NotificationsPreferenceToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _NotificationsPreference && + (identical(other.displayBetaWarning, displayBetaWarning) || + other.displayBetaWarning == displayBetaWarning) && + (identical(other.enableBadge, enableBadge) || + other.enableBadge == enableBadge) && + (identical(other.enableNotifications, enableNotifications) || + other.enableNotifications == enableNotifications) && + (identical(other.messageNotificationContent, + messageNotificationContent) || + other.messageNotificationContent == + messageNotificationContent) && + (identical( + other.onInvitationAcceptedMode, onInvitationAcceptedMode) || + other.onInvitationAcceptedMode == onInvitationAcceptedMode) && + (identical(other.onInvitationAcceptedSound, + onInvitationAcceptedSound) || + other.onInvitationAcceptedSound == onInvitationAcceptedSound) && + (identical(other.onMessageReceivedMode, onMessageReceivedMode) || + other.onMessageReceivedMode == onMessageReceivedMode) && + (identical(other.onMessageReceivedSound, onMessageReceivedSound) || + other.onMessageReceivedSound == onMessageReceivedSound) && + (identical(other.onMessageSentSound, onMessageSentSound) || + other.onMessageSentSound == onMessageSentSound)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + displayBetaWarning, + enableBadge, + enableNotifications, + messageNotificationContent, + onInvitationAcceptedMode, + onInvitationAcceptedSound, + onMessageReceivedMode, + onMessageReceivedSound, + onMessageSentSound); + + @override + String toString() { + return 'NotificationsPreference(displayBetaWarning: $displayBetaWarning, enableBadge: $enableBadge, enableNotifications: $enableNotifications, messageNotificationContent: $messageNotificationContent, onInvitationAcceptedMode: $onInvitationAcceptedMode, onInvitationAcceptedSound: $onInvitationAcceptedSound, onMessageReceivedMode: $onMessageReceivedMode, onMessageReceivedSound: $onMessageReceivedSound, onMessageSentSound: $onMessageSentSound)'; + } } + +/// @nodoc +abstract mixin class _$NotificationsPreferenceCopyWith<$Res> + implements $NotificationsPreferenceCopyWith<$Res> { + factory _$NotificationsPreferenceCopyWith(_NotificationsPreference value, + $Res Function(_NotificationsPreference) _then) = + __$NotificationsPreferenceCopyWithImpl; + @override + @useResult + $Res call( + {bool displayBetaWarning, + bool enableBadge, + bool enableNotifications, + MessageNotificationContent messageNotificationContent, + NotificationMode onInvitationAcceptedMode, + SoundEffect onInvitationAcceptedSound, + NotificationMode onMessageReceivedMode, + SoundEffect onMessageReceivedSound, + SoundEffect onMessageSentSound}); +} + +/// @nodoc +class __$NotificationsPreferenceCopyWithImpl<$Res> + implements _$NotificationsPreferenceCopyWith<$Res> { + __$NotificationsPreferenceCopyWithImpl(this._self, this._then); + + final _NotificationsPreference _self; + final $Res Function(_NotificationsPreference) _then; + + /// Create a copy of NotificationsPreference + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? displayBetaWarning = null, + Object? enableBadge = null, + Object? enableNotifications = null, + Object? messageNotificationContent = null, + Object? onInvitationAcceptedMode = null, + Object? onInvitationAcceptedSound = null, + Object? onMessageReceivedMode = null, + Object? onMessageReceivedSound = null, + Object? onMessageSentSound = null, + }) { + return _then(_NotificationsPreference( + displayBetaWarning: null == displayBetaWarning + ? _self.displayBetaWarning + : displayBetaWarning // ignore: cast_nullable_to_non_nullable + as bool, + enableBadge: null == enableBadge + ? _self.enableBadge + : enableBadge // ignore: cast_nullable_to_non_nullable + as bool, + enableNotifications: null == enableNotifications + ? _self.enableNotifications + : enableNotifications // ignore: cast_nullable_to_non_nullable + as bool, + messageNotificationContent: null == messageNotificationContent + ? _self.messageNotificationContent + : messageNotificationContent // ignore: cast_nullable_to_non_nullable + as MessageNotificationContent, + onInvitationAcceptedMode: null == onInvitationAcceptedMode + ? _self.onInvitationAcceptedMode + : onInvitationAcceptedMode // ignore: cast_nullable_to_non_nullable + as NotificationMode, + onInvitationAcceptedSound: null == onInvitationAcceptedSound + ? _self.onInvitationAcceptedSound + : onInvitationAcceptedSound // ignore: cast_nullable_to_non_nullable + as SoundEffect, + onMessageReceivedMode: null == onMessageReceivedMode + ? _self.onMessageReceivedMode + : onMessageReceivedMode // ignore: cast_nullable_to_non_nullable + as NotificationMode, + onMessageReceivedSound: null == onMessageReceivedSound + ? _self.onMessageReceivedSound + : onMessageReceivedSound // ignore: cast_nullable_to_non_nullable + as SoundEffect, + onMessageSentSound: null == onMessageSentSound + ? _self.onMessageSentSound + : onMessageSentSound // ignore: cast_nullable_to_non_nullable + as SoundEffect, + )); + } +} + +// dart format on diff --git a/lib/notifications/models/notifications_preference.g.dart b/lib/notifications/models/notifications_preference.g.dart index d22b4b5..d1d5e89 100644 --- a/lib/notifications/models/notifications_preference.g.dart +++ b/lib/notifications/models/notifications_preference.g.dart @@ -6,9 +6,9 @@ part of 'notifications_preference.dart'; // JsonSerializableGenerator // ************************************************************************** -_$NotificationsPreferenceImpl _$$NotificationsPreferenceImplFromJson( +_NotificationsPreference _$NotificationsPreferenceFromJson( Map json) => - _$NotificationsPreferenceImpl( + _NotificationsPreference( displayBetaWarning: json['display_beta_warning'] as bool? ?? true, enableBadge: json['enable_badge'] as bool? ?? true, enableNotifications: json['enable_notifications'] as bool? ?? true, @@ -33,8 +33,8 @@ _$NotificationsPreferenceImpl _$$NotificationsPreferenceImplFromJson( : SoundEffect.fromJson(json['on_message_sent_sound']), ); -Map _$$NotificationsPreferenceImplToJson( - _$NotificationsPreferenceImpl instance) => +Map _$NotificationsPreferenceToJson( + _NotificationsPreference instance) => { 'display_beta_warning': instance.displayBetaWarning, 'enable_badge': instance.enableBadge, diff --git a/lib/notifications/models/notifications_state.dart b/lib/notifications/models/notifications_state.dart index d001ce2..6248bba 100644 --- a/lib/notifications/models/notifications_state.dart +++ b/lib/notifications/models/notifications_state.dart @@ -9,7 +9,7 @@ enum NotificationType { } @freezed -class NotificationItem with _$NotificationItem { +sealed class NotificationItem with _$NotificationItem { const factory NotificationItem( {required NotificationType type, required String text, @@ -17,7 +17,7 @@ class NotificationItem with _$NotificationItem { } @freezed -class NotificationsState with _$NotificationsState { +sealed class NotificationsState with _$NotificationsState { const factory NotificationsState({required IList queue}) = _NotificationsState; } diff --git a/lib/notifications/models/notifications_state.freezed.dart b/lib/notifications/models/notifications_state.freezed.dart index e052e7a..8633702 100644 --- a/lib/notifications/models/notifications_state.freezed.dart +++ b/lib/notifications/models/notifications_state.freezed.dart @@ -1,3 +1,4 @@ +// dart format width=80 // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint @@ -9,137 +10,28 @@ part of 'notifications_state.dart'; // FreezedGenerator // ************************************************************************** +// dart format off 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'); - /// @nodoc mixin _$NotificationItem { - NotificationType get type => throw _privateConstructorUsedError; - String get text => throw _privateConstructorUsedError; - String? get title => throw _privateConstructorUsedError; + NotificationType get type; + String get text; + String? get title; /// Create a copy of NotificationItem /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') $NotificationItemCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $NotificationItemCopyWith<$Res> { - factory $NotificationItemCopyWith( - NotificationItem value, $Res Function(NotificationItem) then) = - _$NotificationItemCopyWithImpl<$Res, NotificationItem>; - @useResult - $Res call({NotificationType type, String text, String? title}); -} - -/// @nodoc -class _$NotificationItemCopyWithImpl<$Res, $Val extends NotificationItem> - implements $NotificationItemCopyWith<$Res> { - _$NotificationItemCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of NotificationItem - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? type = null, - Object? text = null, - Object? title = freezed, - }) { - return _then(_value.copyWith( - type: null == type - ? _value.type - : type // ignore: cast_nullable_to_non_nullable - as NotificationType, - text: null == text - ? _value.text - : text // ignore: cast_nullable_to_non_nullable - as String, - title: freezed == title - ? _value.title - : title // ignore: cast_nullable_to_non_nullable - as String?, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$NotificationItemImplCopyWith<$Res> - implements $NotificationItemCopyWith<$Res> { - factory _$$NotificationItemImplCopyWith(_$NotificationItemImpl value, - $Res Function(_$NotificationItemImpl) then) = - __$$NotificationItemImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({NotificationType type, String text, String? title}); -} - -/// @nodoc -class __$$NotificationItemImplCopyWithImpl<$Res> - extends _$NotificationItemCopyWithImpl<$Res, _$NotificationItemImpl> - implements _$$NotificationItemImplCopyWith<$Res> { - __$$NotificationItemImplCopyWithImpl(_$NotificationItemImpl _value, - $Res Function(_$NotificationItemImpl) _then) - : super(_value, _then); - - /// Create a copy of NotificationItem - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? type = null, - Object? text = null, - Object? title = freezed, - }) { - return _then(_$NotificationItemImpl( - type: null == type - ? _value.type - : type // ignore: cast_nullable_to_non_nullable - as NotificationType, - text: null == text - ? _value.text - : text // ignore: cast_nullable_to_non_nullable - as String, - title: freezed == title - ? _value.title - : title // ignore: cast_nullable_to_non_nullable - as String?, - )); - } -} - -/// @nodoc - -class _$NotificationItemImpl implements _NotificationItem { - const _$NotificationItemImpl( - {required this.type, required this.text, this.title}); - - @override - final NotificationType type; - @override - final String text; - @override - final String? title; - - @override - String toString() { - return 'NotificationItem(type: $type, text: $text, title: $title)'; - } + _$NotificationItemCopyWithImpl( + this as NotificationItem, _$identity); @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$NotificationItemImpl && + other is NotificationItem && (identical(other.type, type) || other.type == type) && (identical(other.text, text) || other.text == text) && (identical(other.title, title) || other.title == title)); @@ -148,101 +40,185 @@ class _$NotificationItemImpl implements _NotificationItem { @override int get hashCode => Object.hash(runtimeType, type, text, title); - /// Create a copy of NotificationItem - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) @override - @pragma('vm:prefer-inline') - _$$NotificationItemImplCopyWith<_$NotificationItemImpl> get copyWith => - __$$NotificationItemImplCopyWithImpl<_$NotificationItemImpl>( - this, _$identity); -} - -abstract class _NotificationItem implements NotificationItem { - const factory _NotificationItem( - {required final NotificationType type, - required final String text, - final String? title}) = _$NotificationItemImpl; - - @override - NotificationType get type; - @override - String get text; - @override - String? get title; - - /// Create a copy of NotificationItem - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$NotificationItemImplCopyWith<_$NotificationItemImpl> get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -mixin _$NotificationsState { - IList get queue => throw _privateConstructorUsedError; - - /// Create a copy of NotificationsState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $NotificationsStateCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $NotificationsStateCopyWith<$Res> { - factory $NotificationsStateCopyWith( - NotificationsState value, $Res Function(NotificationsState) then) = - _$NotificationsStateCopyWithImpl<$Res, NotificationsState>; - @useResult - $Res call({IList queue}); -} - -/// @nodoc -class _$NotificationsStateCopyWithImpl<$Res, $Val extends NotificationsState> - implements $NotificationsStateCopyWith<$Res> { - _$NotificationsStateCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of NotificationsState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? queue = null, - }) { - return _then(_value.copyWith( - queue: null == queue - ? _value.queue - : queue // ignore: cast_nullable_to_non_nullable - as IList, - ) as $Val); + String toString() { + return 'NotificationItem(type: $type, text: $text, title: $title)'; } } /// @nodoc -abstract class _$$NotificationsStateImplCopyWith<$Res> - implements $NotificationsStateCopyWith<$Res> { - factory _$$NotificationsStateImplCopyWith(_$NotificationsStateImpl value, - $Res Function(_$NotificationsStateImpl) then) = - __$$NotificationsStateImplCopyWithImpl<$Res>; +abstract mixin class $NotificationItemCopyWith<$Res> { + factory $NotificationItemCopyWith( + NotificationItem value, $Res Function(NotificationItem) _then) = + _$NotificationItemCopyWithImpl; + @useResult + $Res call({NotificationType type, String text, String? title}); +} + +/// @nodoc +class _$NotificationItemCopyWithImpl<$Res> + implements $NotificationItemCopyWith<$Res> { + _$NotificationItemCopyWithImpl(this._self, this._then); + + final NotificationItem _self; + final $Res Function(NotificationItem) _then; + + /// Create a copy of NotificationItem + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') @override + $Res call({ + Object? type = null, + Object? text = null, + Object? title = freezed, + }) { + return _then(_self.copyWith( + type: null == type + ? _self.type + : type // ignore: cast_nullable_to_non_nullable + as NotificationType, + text: null == text + ? _self.text + : text // ignore: cast_nullable_to_non_nullable + as String, + title: freezed == title + ? _self.title + : title // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc + +class _NotificationItem implements NotificationItem { + const _NotificationItem({required this.type, required this.text, this.title}); + + @override + final NotificationType type; + @override + final String text; + @override + final String? title; + + /// Create a copy of NotificationItem + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + _$NotificationItemCopyWith<_NotificationItem> get copyWith => + __$NotificationItemCopyWithImpl<_NotificationItem>(this, _$identity); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _NotificationItem && + (identical(other.type, type) || other.type == type) && + (identical(other.text, text) || other.text == text) && + (identical(other.title, title) || other.title == title)); + } + + @override + int get hashCode => Object.hash(runtimeType, type, text, title); + + @override + String toString() { + return 'NotificationItem(type: $type, text: $text, title: $title)'; + } +} + +/// @nodoc +abstract mixin class _$NotificationItemCopyWith<$Res> + implements $NotificationItemCopyWith<$Res> { + factory _$NotificationItemCopyWith( + _NotificationItem value, $Res Function(_NotificationItem) _then) = + __$NotificationItemCopyWithImpl; + @override + @useResult + $Res call({NotificationType type, String text, String? title}); +} + +/// @nodoc +class __$NotificationItemCopyWithImpl<$Res> + implements _$NotificationItemCopyWith<$Res> { + __$NotificationItemCopyWithImpl(this._self, this._then); + + final _NotificationItem _self; + final $Res Function(_NotificationItem) _then; + + /// Create a copy of NotificationItem + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? type = null, + Object? text = null, + Object? title = freezed, + }) { + return _then(_NotificationItem( + type: null == type + ? _self.type + : type // ignore: cast_nullable_to_non_nullable + as NotificationType, + text: null == text + ? _self.text + : text // ignore: cast_nullable_to_non_nullable + as String, + title: freezed == title + ? _self.title + : title // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc +mixin _$NotificationsState { + IList get queue; + + /// Create a copy of NotificationsState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $NotificationsStateCopyWith get copyWith => + _$NotificationsStateCopyWithImpl( + this as NotificationsState, _$identity); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is NotificationsState && + const DeepCollectionEquality().equals(other.queue, queue)); + } + + @override + int get hashCode => + Object.hash(runtimeType, const DeepCollectionEquality().hash(queue)); + + @override + String toString() { + return 'NotificationsState(queue: $queue)'; + } +} + +/// @nodoc +abstract mixin class $NotificationsStateCopyWith<$Res> { + factory $NotificationsStateCopyWith( + NotificationsState value, $Res Function(NotificationsState) _then) = + _$NotificationsStateCopyWithImpl; @useResult $Res call({IList queue}); } /// @nodoc -class __$$NotificationsStateImplCopyWithImpl<$Res> - extends _$NotificationsStateCopyWithImpl<$Res, _$NotificationsStateImpl> - implements _$$NotificationsStateImplCopyWith<$Res> { - __$$NotificationsStateImplCopyWithImpl(_$NotificationsStateImpl _value, - $Res Function(_$NotificationsStateImpl) _then) - : super(_value, _then); +class _$NotificationsStateCopyWithImpl<$Res> + implements $NotificationsStateCopyWith<$Res> { + _$NotificationsStateCopyWithImpl(this._self, this._then); + + final NotificationsState _self; + final $Res Function(NotificationsState) _then; /// Create a copy of NotificationsState /// with the given fields replaced by the non-null parameter values. @@ -251,9 +227,9 @@ class __$$NotificationsStateImplCopyWithImpl<$Res> $Res call({ Object? queue = null, }) { - return _then(_$NotificationsStateImpl( + return _then(_self.copyWith( queue: null == queue - ? _value.queue + ? _self.queue : queue // ignore: cast_nullable_to_non_nullable as IList, )); @@ -262,22 +238,25 @@ class __$$NotificationsStateImplCopyWithImpl<$Res> /// @nodoc -class _$NotificationsStateImpl implements _NotificationsState { - const _$NotificationsStateImpl({required this.queue}); +class _NotificationsState implements NotificationsState { + const _NotificationsState({required this.queue}); @override final IList queue; + /// Create a copy of NotificationsState + /// with the given fields replaced by the non-null parameter values. @override - String toString() { - return 'NotificationsState(queue: $queue)'; - } + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + _$NotificationsStateCopyWith<_NotificationsState> get copyWith => + __$NotificationsStateCopyWithImpl<_NotificationsState>(this, _$identity); @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$NotificationsStateImpl && + other is _NotificationsState && const DeepCollectionEquality().equals(other.queue, queue)); } @@ -285,28 +264,45 @@ class _$NotificationsStateImpl implements _NotificationsState { int get hashCode => Object.hash(runtimeType, const DeepCollectionEquality().hash(queue)); + @override + String toString() { + return 'NotificationsState(queue: $queue)'; + } +} + +/// @nodoc +abstract mixin class _$NotificationsStateCopyWith<$Res> + implements $NotificationsStateCopyWith<$Res> { + factory _$NotificationsStateCopyWith( + _NotificationsState value, $Res Function(_NotificationsState) _then) = + __$NotificationsStateCopyWithImpl; + @override + @useResult + $Res call({IList queue}); +} + +/// @nodoc +class __$NotificationsStateCopyWithImpl<$Res> + implements _$NotificationsStateCopyWith<$Res> { + __$NotificationsStateCopyWithImpl(this._self, this._then); + + final _NotificationsState _self; + final $Res Function(_NotificationsState) _then; + /// Create a copy of NotificationsState /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') - _$$NotificationsStateImplCopyWith<_$NotificationsStateImpl> get copyWith => - __$$NotificationsStateImplCopyWithImpl<_$NotificationsStateImpl>( - this, _$identity); + $Res call({ + Object? queue = null, + }) { + return _then(_NotificationsState( + queue: null == queue + ? _self.queue + : queue // ignore: cast_nullable_to_non_nullable + as IList, + )); + } } -abstract class _NotificationsState implements NotificationsState { - const factory _NotificationsState( - {required final IList queue}) = - _$NotificationsStateImpl; - - @override - IList get queue; - - /// Create a copy of NotificationsState - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$NotificationsStateImplCopyWith<_$NotificationsStateImpl> get copyWith => - throw _privateConstructorUsedError; -} +// dart format on diff --git a/lib/proto/proto.dart b/lib/proto/proto.dart index 6ad8432..21c988a 100644 --- a/lib/proto/proto.dart +++ b/lib/proto/proto.dart @@ -1,3 +1,6 @@ +import 'package:veilid_support/veilid_support.dart'; +import 'veilidchat.pb.dart' as vcproto; + export 'package:veilid_support/dht_support/proto/proto.dart'; export 'package:veilid_support/proto/proto.dart'; @@ -6,3 +9,292 @@ export 'veilidchat.pb.dart'; export 'veilidchat.pbenum.dart'; export 'veilidchat.pbjson.dart'; export 'veilidchat.pbserver.dart'; + +void registerVeilidchatProtoToDebug() { + dynamic toDebug(dynamic obj) { + if (obj is vcproto.DHTDataReference) { + return { + 'dhtData': obj.dhtData, + 'hash': obj.hash, + }; + } + if (obj is vcproto.BlockStoreDataReference) { + return { + 'block': obj.block, + }; + } + if (obj is vcproto.DataReference) { + return { + 'kind': obj.whichKind(), + if (obj.whichKind() == vcproto.DataReference_Kind.dhtData) + 'dhtData': obj.dhtData, + if (obj.whichKind() == vcproto.DataReference_Kind.blockStoreData) + 'blockStoreData': obj.blockStoreData, + }; + } + if (obj is vcproto.Attachment) { + return { + 'kind': obj.whichKind(), + if (obj.whichKind() == vcproto.Attachment_Kind.media) + 'media': obj.media, + 'signature': obj.signature, + }; + } + if (obj is vcproto.AttachmentMedia) { + return { + 'mime': obj.mime, + 'name': obj.name, + 'content': obj.content, + }; + } + if (obj is vcproto.Permissions) { + return { + 'canAddMembers': obj.canAddMembers, + 'canEditInfo': obj.canEditInfo, + 'moderated': obj.moderated, + }; + } + if (obj is vcproto.Membership) { + return { + 'watchers': obj.watchers, + 'moderated': obj.moderated, + 'talkers': obj.talkers, + 'moderators': obj.moderators, + 'admins': obj.admins, + }; + } + if (obj is vcproto.ChatSettings) { + return { + 'title': obj.title, + 'description': obj.description, + 'icon': obj.icon, + 'defaultExpiration': obj.defaultExpiration, + }; + } + if (obj is vcproto.ChatSettings) { + return { + 'title': obj.title, + 'description': obj.description, + 'icon': obj.icon, + 'defaultExpiration': obj.defaultExpiration, + }; + } + if (obj is vcproto.Message) { + return { + 'id': obj.id, + 'author': obj.author, + 'timestamp': obj.timestamp, + 'kind': obj.whichKind(), + if (obj.whichKind() == vcproto.Message_Kind.text) 'text': obj.text, + if (obj.whichKind() == vcproto.Message_Kind.secret) + 'secret': obj.secret, + if (obj.whichKind() == vcproto.Message_Kind.delete) + 'delete': obj.delete, + if (obj.whichKind() == vcproto.Message_Kind.erase) 'erase': obj.erase, + if (obj.whichKind() == vcproto.Message_Kind.settings) + 'settings': obj.settings, + if (obj.whichKind() == vcproto.Message_Kind.permissions) + 'permissions': obj.permissions, + if (obj.whichKind() == vcproto.Message_Kind.membership) + 'membership': obj.membership, + if (obj.whichKind() == vcproto.Message_Kind.moderation) + 'moderation': obj.moderation, + 'signature': obj.signature, + }; + } + if (obj is vcproto.Message_Text) { + return { + 'text': obj.text, + 'topic': obj.topic, + 'replyId': obj.replyId, + 'expiration': obj.expiration, + 'viewLimit': obj.viewLimit, + 'attachments': obj.attachments, + }; + } + if (obj is vcproto.Message_Secret) { + return { + 'ciphertext': obj.ciphertext, + 'expiration': obj.expiration, + }; + } + if (obj is vcproto.Message_ControlDelete) { + return { + 'ids': obj.ids, + }; + } + if (obj is vcproto.Message_ControlErase) { + return { + 'timestamp': obj.timestamp, + }; + } + if (obj is vcproto.Message_ControlSettings) { + return { + 'settings': obj.settings, + }; + } + if (obj is vcproto.Message_ControlPermissions) { + return { + 'permissions': obj.permissions, + }; + } + if (obj is vcproto.Message_ControlMembership) { + return { + 'membership': obj.membership, + }; + } + if (obj is vcproto.Message_ControlModeration) { + return { + 'acceptedIds': obj.acceptedIds, + 'rejectdIds': obj.rejectedIds, + }; + } + if (obj is vcproto.Message_ControlModeration) { + return { + 'acceptedIds': obj.acceptedIds, + 'rejectdIds': obj.rejectedIds, + }; + } + if (obj is vcproto.Message_ControlReadReceipt) { + return { + 'readIds': obj.readIds, + }; + } + if (obj is vcproto.ReconciledMessage) { + return { + 'content': obj.content, + 'reconciledTime': obj.reconciledTime, + }; + } + if (obj is vcproto.Conversation) { + return { + 'profile': obj.profile, + 'superIdentityJson': obj.superIdentityJson, + 'messages': obj.messages + }; + } + if (obj is vcproto.ChatMember) { + return { + 'remoteIdentityPublicKey': obj.remoteIdentityPublicKey, + 'remoteConversationRecordKey': obj.remoteConversationRecordKey, + }; + } + if (obj is vcproto.DirectChat) { + return { + 'settings': obj.settings, + 'localConversationRecordKey': obj.localConversationRecordKey, + 'remoteMember': obj.remoteMember, + }; + } + if (obj is vcproto.GroupChat) { + return { + 'settings': obj.settings, + 'membership': obj.membership, + 'permissions': obj.permissions, + 'localConversationRecordKey': obj.localConversationRecordKey, + 'remoteMembers': obj.remoteMembers, + }; + } + if (obj is vcproto.Chat) { + return { + 'kind': obj.whichKind(), + if (obj.whichKind() == vcproto.Chat_Kind.direct) 'direct': obj.direct, + if (obj.whichKind() == vcproto.Chat_Kind.group) 'group': obj.group, + }; + } + if (obj is vcproto.Profile) { + return { + 'name': obj.name, + 'pronouns': obj.pronouns, + 'about': obj.about, + 'status': obj.status, + 'availability': obj.availability, + 'avatar': obj.avatar, + 'timestamp': obj.timestamp, + }; + } + if (obj is vcproto.Account) { + return { + 'profile': obj.profile, + 'invisible': obj.invisible, + 'autoAwayTimeoutMin': obj.autoAwayTimeoutMin, + 'contact_list': obj.contactList, + 'contactInvitationRecords': obj.contactInvitationRecords, + 'chatList': obj.chatList, + 'groupChatList': obj.groupChatList, + 'freeMessage': obj.freeMessage, + 'busyMessage': obj.busyMessage, + 'awayMessage': obj.awayMessage, + 'autodetectAway': obj.autodetectAway, + }; + } + if (obj is vcproto.Contact) { + return { + 'nickname': obj.nickname, + 'profile': obj.profile, + 'superIdentityJson': obj.superIdentityJson, + 'identityPublicKey': obj.identityPublicKey, + 'remoteConversationRecordKey': obj.remoteConversationRecordKey, + 'localConversationRecordKey': obj.localConversationRecordKey, + 'showAvailability': obj.showAvailability, + 'notes': obj.notes, + }; + } + if (obj is vcproto.ContactInvitation) { + return { + 'contactRequestInboxKey': obj.contactRequestInboxKey, + 'writerSecret': obj.writerSecret, + }; + } + if (obj is vcproto.SignedContactInvitation) { + return { + 'contactInvitation': obj.contactInvitation, + 'identitySignature': obj.identitySignature, + }; + } + if (obj is vcproto.ContactRequest) { + return { + 'encryptionKeyType': obj.encryptionKeyType, + 'private': obj.private, + }; + } + if (obj is vcproto.ContactRequestPrivate) { + return { + 'writerKey': obj.writerKey, + 'profile': obj.profile, + 'superIdentityRecordKey': obj.superIdentityRecordKey, + 'chatRecordKey': obj.chatRecordKey, + 'expiration': obj.expiration, + }; + } + if (obj is vcproto.ContactResponse) { + return { + 'accept': obj.accept, + 'superIdentityRecordKey': obj.superIdentityRecordKey, + 'remoteConversationRecordKey': obj.remoteConversationRecordKey, + }; + } + if (obj is vcproto.SignedContactResponse) { + return { + 'contactResponse': obj.contactResponse, + 'identitySignature': obj.identitySignature, + }; + } + if (obj is vcproto.ContactInvitationRecord) { + return { + 'contactRequestInbox': obj.contactRequestInbox, + 'writerKey': obj.writerKey, + 'writerSecret': obj.writerSecret, + 'localConversationRecordKey': obj.localConversationRecordKey, + 'expiration': obj.expiration, + 'invitation': obj.invitation, + 'message': obj.message, + 'recipient': obj.recipient, + }; + } + + return obj; + } + + DynamicDebug.registerToDebug(toDebug); +} diff --git a/lib/proto/veilidchat.pb.dart b/lib/proto/veilidchat.pb.dart index 245f9f3..67805ae 100644 --- a/lib/proto/veilidchat.pb.dart +++ b/lib/proto/veilidchat.pb.dart @@ -1216,6 +1216,7 @@ enum Message_Kind { permissions, membership, moderation, + readReceipt, notSet } @@ -1234,6 +1235,7 @@ class Message extends $pb.GeneratedMessage { Message_ControlMembership? membership, Message_ControlModeration? moderation, $0.Signature? signature, + Message_ControlReadReceipt? readReceipt, }) { final $result = create(); if (id != null) { @@ -1272,6 +1274,9 @@ class Message extends $pb.GeneratedMessage { if (signature != null) { $result.signature = signature; } + if (readReceipt != null) { + $result.readReceipt = readReceipt; + } return $result; } Message._() : super(); @@ -1287,10 +1292,11 @@ class Message extends $pb.GeneratedMessage { 9 : Message_Kind.permissions, 10 : Message_Kind.membership, 11 : Message_Kind.moderation, + 13 : Message_Kind.readReceipt, 0 : Message_Kind.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..oo(0, [4, 5, 6, 7, 8, 9, 10, 11]) + ..oo(0, [4, 5, 6, 7, 8, 9, 10, 11, 13]) ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'id', $pb.PbFieldType.OY) ..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'author', subBuilder: $0.TypedKey.create) ..a<$fixnum.Int64>(3, _omitFieldNames ? '' : 'timestamp', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) @@ -1303,6 +1309,7 @@ class Message extends $pb.GeneratedMessage { ..aOM(10, _omitFieldNames ? '' : 'membership', subBuilder: Message_ControlMembership.create) ..aOM(11, _omitFieldNames ? '' : 'moderation', subBuilder: Message_ControlModeration.create) ..aOM<$0.Signature>(12, _omitFieldNames ? '' : 'signature', subBuilder: $0.Signature.create) + ..aOM(13, _omitFieldNames ? '' : 'readReceipt', protoName: 'readReceipt', subBuilder: Message_ControlReadReceipt.create) ..hasRequiredFields = false ; @@ -1462,6 +1469,17 @@ class Message extends $pb.GeneratedMessage { void clearSignature() => clearField(12); @$pb.TagNumber(12) $0.Signature ensureSignature() => $_ensure(11); + + @$pb.TagNumber(13) + Message_ControlReadReceipt get readReceipt => $_getN(12); + @$pb.TagNumber(13) + set readReceipt(Message_ControlReadReceipt v) { setField(13, v); } + @$pb.TagNumber(13) + $core.bool hasReadReceipt() => $_has(12); + @$pb.TagNumber(13) + void clearReadReceipt() => clearField(13); + @$pb.TagNumber(13) + Message_ControlReadReceipt ensureReadReceipt() => $_ensure(12); } /// Locally stored messages for chats diff --git a/lib/proto/veilidchat.pbjson.dart b/lib/proto/veilidchat.pbjson.dart index 81bf741..0958343 100644 --- a/lib/proto/veilidchat.pbjson.dart +++ b/lib/proto/veilidchat.pbjson.dart @@ -215,6 +215,7 @@ const Message$json = { {'1': 'permissions', '3': 9, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlPermissions', '9': 0, '10': 'permissions'}, {'1': 'membership', '3': 10, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlMembership', '9': 0, '10': 'membership'}, {'1': 'moderation', '3': 11, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlModeration', '9': 0, '10': 'moderation'}, + {'1': 'readReceipt', '3': 13, '4': 1, '5': 11, '6': '.veilidchat.Message.ControlReadReceipt', '9': 0, '10': 'readReceipt'}, {'1': 'signature', '3': 12, '4': 1, '5': 11, '6': '.veilid.Signature', '10': 'signature'}, ], '3': [Message_Text$json, Message_Secret$json, Message_ControlDelete$json, Message_ControlErase$json, Message_ControlSettings$json, Message_ControlPermissions$json, Message_ControlMembership$json, Message_ControlModeration$json, Message_ControlReadReceipt$json], @@ -318,22 +319,24 @@ final $typed_data.Uint8List messageDescriptor = $convert.base64Decode( 'twZXJtaXNzaW9ucxgJIAEoCzImLnZlaWxpZGNoYXQuTWVzc2FnZS5Db250cm9sUGVybWlzc2lv' 'bnNIAFILcGVybWlzc2lvbnMSRwoKbWVtYmVyc2hpcBgKIAEoCzIlLnZlaWxpZGNoYXQuTWVzc2' 'FnZS5Db250cm9sTWVtYmVyc2hpcEgAUgptZW1iZXJzaGlwEkcKCm1vZGVyYXRpb24YCyABKAsy' - 'JS52ZWlsaWRjaGF0Lk1lc3NhZ2UuQ29udHJvbE1vZGVyYXRpb25IAFIKbW9kZXJhdGlvbhIvCg' - 'lzaWduYXR1cmUYDCABKAsyES52ZWlsaWQuU2lnbmF0dXJlUglzaWduYXR1cmUa5QEKBFRleHQS' - 'EgoEdGV4dBgBIAEoCVIEdGV4dBIZCgV0b3BpYxgCIAEoCUgAUgV0b3BpY4gBARIeCghyZXBseV' - '9pZBgDIAEoDEgBUgdyZXBseUlkiAEBEh4KCmV4cGlyYXRpb24YBCABKARSCmV4cGlyYXRpb24S' - 'HQoKdmlld19saW1pdBgFIAEoDVIJdmlld0xpbWl0EjgKC2F0dGFjaG1lbnRzGAYgAygLMhYudm' - 'VpbGlkY2hhdC5BdHRhY2htZW50UgthdHRhY2htZW50c0IICgZfdG9waWNCCwoJX3JlcGx5X2lk' - 'GkgKBlNlY3JldBIeCgpjaXBoZXJ0ZXh0GAEgASgMUgpjaXBoZXJ0ZXh0Eh4KCmV4cGlyYXRpb2' - '4YAiABKARSCmV4cGlyYXRpb24aIQoNQ29udHJvbERlbGV0ZRIQCgNpZHMYASADKAxSA2lkcxos' - 'CgxDb250cm9sRXJhc2USHAoJdGltZXN0YW1wGAEgASgEUgl0aW1lc3RhbXAaRwoPQ29udHJvbF' - 'NldHRpbmdzEjQKCHNldHRpbmdzGAEgASgLMhgudmVpbGlkY2hhdC5DaGF0U2V0dGluZ3NSCHNl' - 'dHRpbmdzGk8KEkNvbnRyb2xQZXJtaXNzaW9ucxI5CgtwZXJtaXNzaW9ucxgBIAEoCzIXLnZlaW' - 'xpZGNoYXQuUGVybWlzc2lvbnNSC3Blcm1pc3Npb25zGksKEUNvbnRyb2xNZW1iZXJzaGlwEjYK' - 'Cm1lbWJlcnNoaXAYASABKAsyFi52ZWlsaWRjaGF0Lk1lbWJlcnNoaXBSCm1lbWJlcnNoaXAaWQ' - 'oRQ29udHJvbE1vZGVyYXRpb24SIQoMYWNjZXB0ZWRfaWRzGAEgAygMUgthY2NlcHRlZElkcxIh' - 'CgxyZWplY3RlZF9pZHMYAiADKAxSC3JlamVjdGVkSWRzGi8KEkNvbnRyb2xSZWFkUmVjZWlwdB' - 'IZCghyZWFkX2lkcxgBIAMoDFIHcmVhZElkc0IGCgRraW5k'); + 'JS52ZWlsaWRjaGF0Lk1lc3NhZ2UuQ29udHJvbE1vZGVyYXRpb25IAFIKbW9kZXJhdGlvbhJKCg' + 'tyZWFkUmVjZWlwdBgNIAEoCzImLnZlaWxpZGNoYXQuTWVzc2FnZS5Db250cm9sUmVhZFJlY2Vp' + 'cHRIAFILcmVhZFJlY2VpcHQSLwoJc2lnbmF0dXJlGAwgASgLMhEudmVpbGlkLlNpZ25hdHVyZV' + 'IJc2lnbmF0dXJlGuUBCgRUZXh0EhIKBHRleHQYASABKAlSBHRleHQSGQoFdG9waWMYAiABKAlI' + 'AFIFdG9waWOIAQESHgoIcmVwbHlfaWQYAyABKAxIAVIHcmVwbHlJZIgBARIeCgpleHBpcmF0aW' + '9uGAQgASgEUgpleHBpcmF0aW9uEh0KCnZpZXdfbGltaXQYBSABKA1SCXZpZXdMaW1pdBI4Cgth' + 'dHRhY2htZW50cxgGIAMoCzIWLnZlaWxpZGNoYXQuQXR0YWNobWVudFILYXR0YWNobWVudHNCCA' + 'oGX3RvcGljQgsKCV9yZXBseV9pZBpICgZTZWNyZXQSHgoKY2lwaGVydGV4dBgBIAEoDFIKY2lw' + 'aGVydGV4dBIeCgpleHBpcmF0aW9uGAIgASgEUgpleHBpcmF0aW9uGiEKDUNvbnRyb2xEZWxldG' + 'USEAoDaWRzGAEgAygMUgNpZHMaLAoMQ29udHJvbEVyYXNlEhwKCXRpbWVzdGFtcBgBIAEoBFIJ' + 'dGltZXN0YW1wGkcKD0NvbnRyb2xTZXR0aW5ncxI0CghzZXR0aW5ncxgBIAEoCzIYLnZlaWxpZG' + 'NoYXQuQ2hhdFNldHRpbmdzUghzZXR0aW5ncxpPChJDb250cm9sUGVybWlzc2lvbnMSOQoLcGVy' + 'bWlzc2lvbnMYASABKAsyFy52ZWlsaWRjaGF0LlBlcm1pc3Npb25zUgtwZXJtaXNzaW9ucxpLCh' + 'FDb250cm9sTWVtYmVyc2hpcBI2CgptZW1iZXJzaGlwGAEgASgLMhYudmVpbGlkY2hhdC5NZW1i' + 'ZXJzaGlwUgptZW1iZXJzaGlwGlkKEUNvbnRyb2xNb2RlcmF0aW9uEiEKDGFjY2VwdGVkX2lkcx' + 'gBIAMoDFILYWNjZXB0ZWRJZHMSIQoMcmVqZWN0ZWRfaWRzGAIgAygMUgtyZWplY3RlZElkcxov' + 'ChJDb250cm9sUmVhZFJlY2VpcHQSGQoIcmVhZF9pZHMYASADKAxSB3JlYWRJZHNCBgoEa2luZA' + '=='); @$core.Deprecated('Use reconciledMessageDescriptor instead') const ReconciledMessage$json = { diff --git a/lib/proto/veilidchat.proto b/lib/proto/veilidchat.proto index e669959..5bff89c 100644 --- a/lib/proto/veilidchat.proto +++ b/lib/proto/veilidchat.proto @@ -228,6 +228,7 @@ message Message { ControlPermissions permissions = 9; ControlMembership membership = 10; ControlModeration moderation = 11; + ControlReadReceipt readReceipt = 13; } // Author signature over all of the fields and attachment signatures diff --git a/lib/router/cubits/router_cubit.dart b/lib/router/cubits/router_cubit.dart index 6684c45..1492c51 100644 --- a/lib/router/cubits/router_cubit.dart +++ b/lib/router/cubits/router_cubit.dart @@ -22,7 +22,7 @@ part 'router_cubit.g.dart'; final _rootNavKey = GlobalKey(debugLabel: 'rootNavKey'); @freezed -class RouterState with _$RouterState { +sealed class RouterState with _$RouterState { const factory RouterState({ required bool hasAnyAccount, }) = _RouterState; diff --git a/lib/router/cubits/router_cubit.freezed.dart b/lib/router/cubits/router_cubit.freezed.dart index 8377607..0f5b285 100644 --- a/lib/router/cubits/router_cubit.freezed.dart +++ b/lib/router/cubits/router_cubit.freezed.dart @@ -1,3 +1,4 @@ +// dart format width=80 // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint @@ -9,118 +10,25 @@ part of 'router_cubit.dart'; // FreezedGenerator // ************************************************************************** +// dart format off 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 hasAnyAccount => throw _privateConstructorUsedError; - - /// Serializes this RouterState to a JSON map. - Map toJson() => throw _privateConstructorUsedError; +mixin _$RouterState implements DiagnosticableTreeMixin { + bool get hasAnyAccount; /// Create a copy of RouterState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') $RouterStateCopyWith get copyWith => - throw _privateConstructorUsedError; -} + _$RouterStateCopyWithImpl(this as RouterState, _$identity); -/// @nodoc -abstract class $RouterStateCopyWith<$Res> { - factory $RouterStateCopyWith( - RouterState value, $Res Function(RouterState) then) = - _$RouterStateCopyWithImpl<$Res, RouterState>; - @useResult - $Res call({bool hasAnyAccount}); -} - -/// @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; - - /// Create a copy of RouterState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? hasAnyAccount = null, - }) { - return _then(_value.copyWith( - hasAnyAccount: null == hasAnyAccount - ? _value.hasAnyAccount - : hasAnyAccount // 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 hasAnyAccount}); -} - -/// @nodoc -class __$$RouterStateImplCopyWithImpl<$Res> - extends _$RouterStateCopyWithImpl<$Res, _$RouterStateImpl> - implements _$$RouterStateImplCopyWith<$Res> { - __$$RouterStateImplCopyWithImpl( - _$RouterStateImpl _value, $Res Function(_$RouterStateImpl) _then) - : super(_value, _then); - - /// Create a copy of RouterState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? hasAnyAccount = null, - }) { - return _then(_$RouterStateImpl( - hasAnyAccount: null == hasAnyAccount - ? _value.hasAnyAccount - : hasAnyAccount // ignore: cast_nullable_to_non_nullable - as bool, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState { - const _$RouterStateImpl({required this.hasAnyAccount}); - - factory _$RouterStateImpl.fromJson(Map json) => - _$$RouterStateImplFromJson(json); - - @override - final bool hasAnyAccount; - - @override - String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'RouterState(hasAnyAccount: $hasAnyAccount)'; - } + /// Serializes this RouterState to a JSON map. + Map toJson(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('type', 'RouterState')) ..add(DiagnosticsProperty('hasAnyAccount', hasAnyAccount)); @@ -130,7 +38,7 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState { bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$RouterStateImpl && + other is RouterState && (identical(other.hasAnyAccount, hasAnyAccount) || other.hasAnyAccount == hasAnyAccount)); } @@ -139,36 +47,127 @@ class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState { @override int get hashCode => Object.hash(runtimeType, hasAnyAccount); - /// Create a copy of RouterState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) @override - @pragma('vm:prefer-inline') - _$$RouterStateImplCopyWith<_$RouterStateImpl> get copyWith => - __$$RouterStateImplCopyWithImpl<_$RouterStateImpl>(this, _$identity); - - @override - Map toJson() { - return _$$RouterStateImplToJson( - this, - ); + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'RouterState(hasAnyAccount: $hasAnyAccount)'; } } -abstract class _RouterState implements RouterState { - const factory _RouterState({required final bool hasAnyAccount}) = - _$RouterStateImpl; +/// @nodoc +abstract mixin class $RouterStateCopyWith<$Res> { + factory $RouterStateCopyWith( + RouterState value, $Res Function(RouterState) _then) = + _$RouterStateCopyWithImpl; + @useResult + $Res call({bool hasAnyAccount}); +} - factory _RouterState.fromJson(Map json) = - _$RouterStateImpl.fromJson; +/// @nodoc +class _$RouterStateCopyWithImpl<$Res> implements $RouterStateCopyWith<$Res> { + _$RouterStateCopyWithImpl(this._self, this._then); + + final RouterState _self; + final $Res Function(RouterState) _then; + + /// Create a copy of RouterState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? hasAnyAccount = null, + }) { + return _then(_self.copyWith( + hasAnyAccount: null == hasAnyAccount + ? _self.hasAnyAccount + : hasAnyAccount // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _RouterState with DiagnosticableTreeMixin implements RouterState { + const _RouterState({required this.hasAnyAccount}); + factory _RouterState.fromJson(Map json) => + _$RouterStateFromJson(json); @override - bool get hasAnyAccount; + final bool hasAnyAccount; /// Create a copy of RouterState /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) - _$$RouterStateImplCopyWith<_$RouterStateImpl> get copyWith => - throw _privateConstructorUsedError; + @pragma('vm:prefer-inline') + _$RouterStateCopyWith<_RouterState> get copyWith => + __$RouterStateCopyWithImpl<_RouterState>(this, _$identity); + + @override + Map toJson() { + return _$RouterStateToJson( + this, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'RouterState')) + ..add(DiagnosticsProperty('hasAnyAccount', hasAnyAccount)); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _RouterState && + (identical(other.hasAnyAccount, hasAnyAccount) || + other.hasAnyAccount == hasAnyAccount)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, hasAnyAccount); + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'RouterState(hasAnyAccount: $hasAnyAccount)'; + } } + +/// @nodoc +abstract mixin class _$RouterStateCopyWith<$Res> + implements $RouterStateCopyWith<$Res> { + factory _$RouterStateCopyWith( + _RouterState value, $Res Function(_RouterState) _then) = + __$RouterStateCopyWithImpl; + @override + @useResult + $Res call({bool hasAnyAccount}); +} + +/// @nodoc +class __$RouterStateCopyWithImpl<$Res> implements _$RouterStateCopyWith<$Res> { + __$RouterStateCopyWithImpl(this._self, this._then); + + final _RouterState _self; + final $Res Function(_RouterState) _then; + + /// Create a copy of RouterState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? hasAnyAccount = null, + }) { + return _then(_RouterState( + hasAnyAccount: null == hasAnyAccount + ? _self.hasAnyAccount + : hasAnyAccount // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +// dart format on diff --git a/lib/router/cubits/router_cubit.g.dart b/lib/router/cubits/router_cubit.g.dart index 4d9241c..3623d0e 100644 --- a/lib/router/cubits/router_cubit.g.dart +++ b/lib/router/cubits/router_cubit.g.dart @@ -6,12 +6,11 @@ part of 'router_cubit.dart'; // JsonSerializableGenerator // ************************************************************************** -_$RouterStateImpl _$$RouterStateImplFromJson(Map json) => - _$RouterStateImpl( +_RouterState _$RouterStateFromJson(Map json) => _RouterState( hasAnyAccount: json['has_any_account'] as bool, ); -Map _$$RouterStateImplToJson(_$RouterStateImpl instance) => +Map _$RouterStateToJson(_RouterState instance) => { 'has_any_account': instance.hasAnyAccount, }; diff --git a/lib/settings/models/preferences.dart b/lib/settings/models/preferences.dart index e646c61..3ef683e 100644 --- a/lib/settings/models/preferences.dart +++ b/lib/settings/models/preferences.dart @@ -10,7 +10,7 @@ part 'preferences.g.dart'; // Lock preference changes how frequently the messenger locks its // interface and requires the identitySecretKey to be entered (pin/password/etc) @freezed -class LockPreference with _$LockPreference { +sealed class LockPreference with _$LockPreference { const factory LockPreference({ @Default(0) int inactivityLockSecs, @Default(false) bool lockWhenSwitching, @@ -37,7 +37,7 @@ enum LanguagePreference { // Preferences are stored in a table locally and globally affect all // accounts imported/added and the app in general @freezed -class Preferences with _$Preferences { +sealed class Preferences with _$Preferences { const factory Preferences({ @Default(ThemePreferences.defaults) ThemePreferences themePreference, @Default(LanguagePreference.defaults) LanguagePreference languagePreference, diff --git a/lib/settings/models/preferences.freezed.dart b/lib/settings/models/preferences.freezed.dart index a7ebed3..9e090f5 100644 --- a/lib/settings/models/preferences.freezed.dart +++ b/lib/settings/models/preferences.freezed.dart @@ -1,3 +1,4 @@ +// dart format width=80 // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint @@ -9,158 +10,31 @@ part of 'preferences.dart'; // FreezedGenerator // ************************************************************************** +// dart format off 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'); - -LockPreference _$LockPreferenceFromJson(Map json) { - return _LockPreference.fromJson(json); -} - /// @nodoc mixin _$LockPreference { - int get inactivityLockSecs => throw _privateConstructorUsedError; - bool get lockWhenSwitching => throw _privateConstructorUsedError; - bool get lockWithSystemLock => throw _privateConstructorUsedError; - - /// Serializes this LockPreference to a JSON map. - Map toJson() => throw _privateConstructorUsedError; + int get inactivityLockSecs; + bool get lockWhenSwitching; + bool get lockWithSystemLock; /// Create a copy of LockPreference /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') $LockPreferenceCopyWith get copyWith => - throw _privateConstructorUsedError; -} + _$LockPreferenceCopyWithImpl( + this as LockPreference, _$identity); -/// @nodoc -abstract class $LockPreferenceCopyWith<$Res> { - factory $LockPreferenceCopyWith( - LockPreference value, $Res Function(LockPreference) then) = - _$LockPreferenceCopyWithImpl<$Res, LockPreference>; - @useResult - $Res call( - {int inactivityLockSecs, - bool lockWhenSwitching, - bool lockWithSystemLock}); -} - -/// @nodoc -class _$LockPreferenceCopyWithImpl<$Res, $Val extends LockPreference> - implements $LockPreferenceCopyWith<$Res> { - _$LockPreferenceCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of LockPreference - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? inactivityLockSecs = null, - Object? lockWhenSwitching = null, - Object? lockWithSystemLock = null, - }) { - return _then(_value.copyWith( - inactivityLockSecs: null == inactivityLockSecs - ? _value.inactivityLockSecs - : inactivityLockSecs // ignore: cast_nullable_to_non_nullable - as int, - lockWhenSwitching: null == lockWhenSwitching - ? _value.lockWhenSwitching - : lockWhenSwitching // ignore: cast_nullable_to_non_nullable - as bool, - lockWithSystemLock: null == lockWithSystemLock - ? _value.lockWithSystemLock - : lockWithSystemLock // ignore: cast_nullable_to_non_nullable - as bool, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$LockPreferenceImplCopyWith<$Res> - implements $LockPreferenceCopyWith<$Res> { - factory _$$LockPreferenceImplCopyWith(_$LockPreferenceImpl value, - $Res Function(_$LockPreferenceImpl) then) = - __$$LockPreferenceImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {int inactivityLockSecs, - bool lockWhenSwitching, - bool lockWithSystemLock}); -} - -/// @nodoc -class __$$LockPreferenceImplCopyWithImpl<$Res> - extends _$LockPreferenceCopyWithImpl<$Res, _$LockPreferenceImpl> - implements _$$LockPreferenceImplCopyWith<$Res> { - __$$LockPreferenceImplCopyWithImpl( - _$LockPreferenceImpl _value, $Res Function(_$LockPreferenceImpl) _then) - : super(_value, _then); - - /// Create a copy of LockPreference - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? inactivityLockSecs = null, - Object? lockWhenSwitching = null, - Object? lockWithSystemLock = null, - }) { - return _then(_$LockPreferenceImpl( - inactivityLockSecs: null == inactivityLockSecs - ? _value.inactivityLockSecs - : inactivityLockSecs // ignore: cast_nullable_to_non_nullable - as int, - lockWhenSwitching: null == lockWhenSwitching - ? _value.lockWhenSwitching - : lockWhenSwitching // ignore: cast_nullable_to_non_nullable - as bool, - lockWithSystemLock: null == lockWithSystemLock - ? _value.lockWithSystemLock - : lockWithSystemLock // ignore: cast_nullable_to_non_nullable - as bool, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$LockPreferenceImpl implements _LockPreference { - const _$LockPreferenceImpl( - {this.inactivityLockSecs = 0, - this.lockWhenSwitching = false, - this.lockWithSystemLock = false}); - - factory _$LockPreferenceImpl.fromJson(Map json) => - _$$LockPreferenceImplFromJson(json); - - @override - @JsonKey() - final int inactivityLockSecs; - @override - @JsonKey() - final bool lockWhenSwitching; - @override - @JsonKey() - final bool lockWithSystemLock; - - @override - String toString() { - return 'LockPreference(inactivityLockSecs: $inactivityLockSecs, lockWhenSwitching: $lockWhenSwitching, lockWithSystemLock: $lockWithSystemLock)'; - } + /// Serializes this LockPreference to a JSON map. + Map toJson(); @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$LockPreferenceImpl && + other is LockPreference && (identical(other.inactivityLockSecs, inactivityLockSecs) || other.inactivityLockSecs == inactivityLockSecs) && (identical(other.lockWhenSwitching, lockWhenSwitching) || @@ -174,255 +48,187 @@ class _$LockPreferenceImpl implements _LockPreference { int get hashCode => Object.hash( runtimeType, inactivityLockSecs, lockWhenSwitching, lockWithSystemLock); - /// Create a copy of LockPreference - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) @override - @pragma('vm:prefer-inline') - _$$LockPreferenceImplCopyWith<_$LockPreferenceImpl> get copyWith => - __$$LockPreferenceImplCopyWithImpl<_$LockPreferenceImpl>( - this, _$identity); - - @override - Map toJson() { - return _$$LockPreferenceImplToJson( - this, - ); + String toString() { + return 'LockPreference(inactivityLockSecs: $inactivityLockSecs, lockWhenSwitching: $lockWhenSwitching, lockWithSystemLock: $lockWithSystemLock)'; } } -abstract class _LockPreference implements LockPreference { - const factory _LockPreference( - {final int inactivityLockSecs, - final bool lockWhenSwitching, - final bool lockWithSystemLock}) = _$LockPreferenceImpl; - - factory _LockPreference.fromJson(Map json) = - _$LockPreferenceImpl.fromJson; - - @override - int get inactivityLockSecs; - @override - bool get lockWhenSwitching; - @override - bool get lockWithSystemLock; - - /// Create a copy of LockPreference - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$LockPreferenceImplCopyWith<_$LockPreferenceImpl> get copyWith => - throw _privateConstructorUsedError; -} - -Preferences _$PreferencesFromJson(Map json) { - return _Preferences.fromJson(json); -} - /// @nodoc -mixin _$Preferences { - ThemePreferences get themePreference => throw _privateConstructorUsedError; - LanguagePreference get languagePreference => - throw _privateConstructorUsedError; - LockPreference get lockPreference => throw _privateConstructorUsedError; - NotificationsPreference get notificationsPreference => - throw _privateConstructorUsedError; - - /// Serializes this Preferences to a JSON map. - Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of Preferences - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $PreferencesCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $PreferencesCopyWith<$Res> { - factory $PreferencesCopyWith( - Preferences value, $Res Function(Preferences) then) = - _$PreferencesCopyWithImpl<$Res, Preferences>; +abstract mixin class $LockPreferenceCopyWith<$Res> { + factory $LockPreferenceCopyWith( + LockPreference value, $Res Function(LockPreference) _then) = + _$LockPreferenceCopyWithImpl; @useResult $Res call( - {ThemePreferences themePreference, - LanguagePreference languagePreference, - LockPreference lockPreference, - NotificationsPreference notificationsPreference}); - - $ThemePreferencesCopyWith<$Res> get themePreference; - $LockPreferenceCopyWith<$Res> get lockPreference; - $NotificationsPreferenceCopyWith<$Res> get notificationsPreference; + {int inactivityLockSecs, + bool lockWhenSwitching, + bool lockWithSystemLock}); } /// @nodoc -class _$PreferencesCopyWithImpl<$Res, $Val extends Preferences> - implements $PreferencesCopyWith<$Res> { - _$PreferencesCopyWithImpl(this._value, this._then); +class _$LockPreferenceCopyWithImpl<$Res> + implements $LockPreferenceCopyWith<$Res> { + _$LockPreferenceCopyWithImpl(this._self, this._then); - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; + final LockPreference _self; + final $Res Function(LockPreference) _then; - /// Create a copy of Preferences + /// Create a copy of LockPreference /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ - Object? themePreference = null, - Object? languagePreference = null, - Object? lockPreference = null, - Object? notificationsPreference = null, + Object? inactivityLockSecs = null, + Object? lockWhenSwitching = null, + Object? lockWithSystemLock = null, }) { - return _then(_value.copyWith( - themePreference: null == themePreference - ? _value.themePreference - : themePreference // ignore: cast_nullable_to_non_nullable - as ThemePreferences, - languagePreference: null == languagePreference - ? _value.languagePreference - : languagePreference // ignore: cast_nullable_to_non_nullable - as LanguagePreference, - lockPreference: null == lockPreference - ? _value.lockPreference - : lockPreference // ignore: cast_nullable_to_non_nullable - as LockPreference, - notificationsPreference: null == notificationsPreference - ? _value.notificationsPreference - : notificationsPreference // ignore: cast_nullable_to_non_nullable - as NotificationsPreference, - ) as $Val); - } - - /// Create a copy of Preferences - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $ThemePreferencesCopyWith<$Res> get themePreference { - return $ThemePreferencesCopyWith<$Res>(_value.themePreference, (value) { - return _then(_value.copyWith(themePreference: value) as $Val); - }); - } - - /// Create a copy of Preferences - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $LockPreferenceCopyWith<$Res> get lockPreference { - return $LockPreferenceCopyWith<$Res>(_value.lockPreference, (value) { - return _then(_value.copyWith(lockPreference: value) as $Val); - }); - } - - /// Create a copy of Preferences - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $NotificationsPreferenceCopyWith<$Res> get notificationsPreference { - return $NotificationsPreferenceCopyWith<$Res>( - _value.notificationsPreference, (value) { - return _then(_value.copyWith(notificationsPreference: value) as $Val); - }); - } -} - -/// @nodoc -abstract class _$$PreferencesImplCopyWith<$Res> - implements $PreferencesCopyWith<$Res> { - factory _$$PreferencesImplCopyWith( - _$PreferencesImpl value, $Res Function(_$PreferencesImpl) then) = - __$$PreferencesImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {ThemePreferences themePreference, - LanguagePreference languagePreference, - LockPreference lockPreference, - NotificationsPreference notificationsPreference}); - - @override - $ThemePreferencesCopyWith<$Res> get themePreference; - @override - $LockPreferenceCopyWith<$Res> get lockPreference; - @override - $NotificationsPreferenceCopyWith<$Res> get notificationsPreference; -} - -/// @nodoc -class __$$PreferencesImplCopyWithImpl<$Res> - extends _$PreferencesCopyWithImpl<$Res, _$PreferencesImpl> - implements _$$PreferencesImplCopyWith<$Res> { - __$$PreferencesImplCopyWithImpl( - _$PreferencesImpl _value, $Res Function(_$PreferencesImpl) _then) - : super(_value, _then); - - /// Create a copy of Preferences - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? themePreference = null, - Object? languagePreference = null, - Object? lockPreference = null, - Object? notificationsPreference = null, - }) { - return _then(_$PreferencesImpl( - themePreference: null == themePreference - ? _value.themePreference - : themePreference // ignore: cast_nullable_to_non_nullable - as ThemePreferences, - languagePreference: null == languagePreference - ? _value.languagePreference - : languagePreference // ignore: cast_nullable_to_non_nullable - as LanguagePreference, - lockPreference: null == lockPreference - ? _value.lockPreference - : lockPreference // ignore: cast_nullable_to_non_nullable - as LockPreference, - notificationsPreference: null == notificationsPreference - ? _value.notificationsPreference - : notificationsPreference // ignore: cast_nullable_to_non_nullable - as NotificationsPreference, + return _then(_self.copyWith( + inactivityLockSecs: null == inactivityLockSecs + ? _self.inactivityLockSecs + : inactivityLockSecs // ignore: cast_nullable_to_non_nullable + as int, + lockWhenSwitching: null == lockWhenSwitching + ? _self.lockWhenSwitching + : lockWhenSwitching // ignore: cast_nullable_to_non_nullable + as bool, + lockWithSystemLock: null == lockWithSystemLock + ? _self.lockWithSystemLock + : lockWithSystemLock // ignore: cast_nullable_to_non_nullable + as bool, )); } } /// @nodoc @JsonSerializable() -class _$PreferencesImpl implements _Preferences { - const _$PreferencesImpl( - {this.themePreference = ThemePreferences.defaults, - this.languagePreference = LanguagePreference.defaults, - this.lockPreference = LockPreference.defaults, - this.notificationsPreference = NotificationsPreference.defaults}); - - factory _$PreferencesImpl.fromJson(Map json) => - _$$PreferencesImplFromJson(json); +class _LockPreference implements LockPreference { + const _LockPreference( + {this.inactivityLockSecs = 0, + this.lockWhenSwitching = false, + this.lockWithSystemLock = false}); + factory _LockPreference.fromJson(Map json) => + _$LockPreferenceFromJson(json); @override @JsonKey() - final ThemePreferences themePreference; + final int inactivityLockSecs; @override @JsonKey() - final LanguagePreference languagePreference; + final bool lockWhenSwitching; @override @JsonKey() - final LockPreference lockPreference; + final bool lockWithSystemLock; + + /// Create a copy of LockPreference + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey() - final NotificationsPreference notificationsPreference; + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + _$LockPreferenceCopyWith<_LockPreference> get copyWith => + __$LockPreferenceCopyWithImpl<_LockPreference>(this, _$identity); @override - String toString() { - return 'Preferences(themePreference: $themePreference, languagePreference: $languagePreference, lockPreference: $lockPreference, notificationsPreference: $notificationsPreference)'; + Map toJson() { + return _$LockPreferenceToJson( + this, + ); } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$PreferencesImpl && + other is _LockPreference && + (identical(other.inactivityLockSecs, inactivityLockSecs) || + other.inactivityLockSecs == inactivityLockSecs) && + (identical(other.lockWhenSwitching, lockWhenSwitching) || + other.lockWhenSwitching == lockWhenSwitching) && + (identical(other.lockWithSystemLock, lockWithSystemLock) || + other.lockWithSystemLock == lockWithSystemLock)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, inactivityLockSecs, lockWhenSwitching, lockWithSystemLock); + + @override + String toString() { + return 'LockPreference(inactivityLockSecs: $inactivityLockSecs, lockWhenSwitching: $lockWhenSwitching, lockWithSystemLock: $lockWithSystemLock)'; + } +} + +/// @nodoc +abstract mixin class _$LockPreferenceCopyWith<$Res> + implements $LockPreferenceCopyWith<$Res> { + factory _$LockPreferenceCopyWith( + _LockPreference value, $Res Function(_LockPreference) _then) = + __$LockPreferenceCopyWithImpl; + @override + @useResult + $Res call( + {int inactivityLockSecs, + bool lockWhenSwitching, + bool lockWithSystemLock}); +} + +/// @nodoc +class __$LockPreferenceCopyWithImpl<$Res> + implements _$LockPreferenceCopyWith<$Res> { + __$LockPreferenceCopyWithImpl(this._self, this._then); + + final _LockPreference _self; + final $Res Function(_LockPreference) _then; + + /// Create a copy of LockPreference + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? inactivityLockSecs = null, + Object? lockWhenSwitching = null, + Object? lockWithSystemLock = null, + }) { + return _then(_LockPreference( + inactivityLockSecs: null == inactivityLockSecs + ? _self.inactivityLockSecs + : inactivityLockSecs // ignore: cast_nullable_to_non_nullable + as int, + lockWhenSwitching: null == lockWhenSwitching + ? _self.lockWhenSwitching + : lockWhenSwitching // ignore: cast_nullable_to_non_nullable + as bool, + lockWithSystemLock: null == lockWithSystemLock + ? _self.lockWithSystemLock + : lockWithSystemLock // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc +mixin _$Preferences { + ThemePreferences get themePreference; + LanguagePreference get languagePreference; + LockPreference get lockPreference; + NotificationsPreference get notificationsPreference; + + /// Create a copy of Preferences + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $PreferencesCopyWith get copyWith => + _$PreferencesCopyWithImpl(this as Preferences, _$identity); + + /// Serializes this Preferences to a JSON map. + Map toJson(); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is Preferences && (identical(other.themePreference, themePreference) || other.themePreference == themePreference) && (identical(other.languagePreference, languagePreference) || @@ -439,46 +245,253 @@ class _$PreferencesImpl implements _Preferences { int get hashCode => Object.hash(runtimeType, themePreference, languagePreference, lockPreference, notificationsPreference); - /// Create a copy of Preferences - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) @override - @pragma('vm:prefer-inline') - _$$PreferencesImplCopyWith<_$PreferencesImpl> get copyWith => - __$$PreferencesImplCopyWithImpl<_$PreferencesImpl>(this, _$identity); - - @override - Map toJson() { - return _$$PreferencesImplToJson( - this, - ); + String toString() { + return 'Preferences(themePreference: $themePreference, languagePreference: $languagePreference, lockPreference: $lockPreference, notificationsPreference: $notificationsPreference)'; } } -abstract class _Preferences implements Preferences { - const factory _Preferences( - {final ThemePreferences themePreference, - final LanguagePreference languagePreference, - final LockPreference lockPreference, - final NotificationsPreference notificationsPreference}) = - _$PreferencesImpl; +/// @nodoc +abstract mixin class $PreferencesCopyWith<$Res> { + factory $PreferencesCopyWith( + Preferences value, $Res Function(Preferences) _then) = + _$PreferencesCopyWithImpl; + @useResult + $Res call( + {ThemePreferences themePreference, + LanguagePreference languagePreference, + LockPreference lockPreference, + NotificationsPreference notificationsPreference}); - factory _Preferences.fromJson(Map json) = - _$PreferencesImpl.fromJson; + $ThemePreferencesCopyWith<$Res> get themePreference; + $LockPreferenceCopyWith<$Res> get lockPreference; + $NotificationsPreferenceCopyWith<$Res> get notificationsPreference; +} + +/// @nodoc +class _$PreferencesCopyWithImpl<$Res> implements $PreferencesCopyWith<$Res> { + _$PreferencesCopyWithImpl(this._self, this._then); + + final Preferences _self; + final $Res Function(Preferences) _then; + + /// Create a copy of Preferences + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? themePreference = null, + Object? languagePreference = null, + Object? lockPreference = null, + Object? notificationsPreference = null, + }) { + return _then(_self.copyWith( + themePreference: null == themePreference + ? _self.themePreference + : themePreference // ignore: cast_nullable_to_non_nullable + as ThemePreferences, + languagePreference: null == languagePreference + ? _self.languagePreference + : languagePreference // ignore: cast_nullable_to_non_nullable + as LanguagePreference, + lockPreference: null == lockPreference + ? _self.lockPreference + : lockPreference // ignore: cast_nullable_to_non_nullable + as LockPreference, + notificationsPreference: null == notificationsPreference + ? _self.notificationsPreference + : notificationsPreference // ignore: cast_nullable_to_non_nullable + as NotificationsPreference, + )); + } + + /// Create a copy of Preferences + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $ThemePreferencesCopyWith<$Res> get themePreference { + return $ThemePreferencesCopyWith<$Res>(_self.themePreference, (value) { + return _then(_self.copyWith(themePreference: value)); + }); + } + + /// Create a copy of Preferences + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $LockPreferenceCopyWith<$Res> get lockPreference { + return $LockPreferenceCopyWith<$Res>(_self.lockPreference, (value) { + return _then(_self.copyWith(lockPreference: value)); + }); + } + + /// Create a copy of Preferences + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $NotificationsPreferenceCopyWith<$Res> get notificationsPreference { + return $NotificationsPreferenceCopyWith<$Res>(_self.notificationsPreference, + (value) { + return _then(_self.copyWith(notificationsPreference: value)); + }); + } +} + +/// @nodoc +@JsonSerializable() +class _Preferences implements Preferences { + const _Preferences( + {this.themePreference = ThemePreferences.defaults, + this.languagePreference = LanguagePreference.defaults, + this.lockPreference = LockPreference.defaults, + this.notificationsPreference = NotificationsPreference.defaults}); + factory _Preferences.fromJson(Map json) => + _$PreferencesFromJson(json); @override - ThemePreferences get themePreference; + @JsonKey() + final ThemePreferences themePreference; @override - LanguagePreference get languagePreference; + @JsonKey() + final LanguagePreference languagePreference; @override - LockPreference get lockPreference; + @JsonKey() + final LockPreference lockPreference; @override - NotificationsPreference get notificationsPreference; + @JsonKey() + final NotificationsPreference notificationsPreference; /// Create a copy of Preferences /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) - _$$PreferencesImplCopyWith<_$PreferencesImpl> get copyWith => - throw _privateConstructorUsedError; + @pragma('vm:prefer-inline') + _$PreferencesCopyWith<_Preferences> get copyWith => + __$PreferencesCopyWithImpl<_Preferences>(this, _$identity); + + @override + Map toJson() { + return _$PreferencesToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _Preferences && + (identical(other.themePreference, themePreference) || + other.themePreference == themePreference) && + (identical(other.languagePreference, languagePreference) || + other.languagePreference == languagePreference) && + (identical(other.lockPreference, lockPreference) || + other.lockPreference == lockPreference) && + (identical( + other.notificationsPreference, notificationsPreference) || + other.notificationsPreference == notificationsPreference)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, themePreference, + languagePreference, lockPreference, notificationsPreference); + + @override + String toString() { + return 'Preferences(themePreference: $themePreference, languagePreference: $languagePreference, lockPreference: $lockPreference, notificationsPreference: $notificationsPreference)'; + } } + +/// @nodoc +abstract mixin class _$PreferencesCopyWith<$Res> + implements $PreferencesCopyWith<$Res> { + factory _$PreferencesCopyWith( + _Preferences value, $Res Function(_Preferences) _then) = + __$PreferencesCopyWithImpl; + @override + @useResult + $Res call( + {ThemePreferences themePreference, + LanguagePreference languagePreference, + LockPreference lockPreference, + NotificationsPreference notificationsPreference}); + + @override + $ThemePreferencesCopyWith<$Res> get themePreference; + @override + $LockPreferenceCopyWith<$Res> get lockPreference; + @override + $NotificationsPreferenceCopyWith<$Res> get notificationsPreference; +} + +/// @nodoc +class __$PreferencesCopyWithImpl<$Res> implements _$PreferencesCopyWith<$Res> { + __$PreferencesCopyWithImpl(this._self, this._then); + + final _Preferences _self; + final $Res Function(_Preferences) _then; + + /// Create a copy of Preferences + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? themePreference = null, + Object? languagePreference = null, + Object? lockPreference = null, + Object? notificationsPreference = null, + }) { + return _then(_Preferences( + themePreference: null == themePreference + ? _self.themePreference + : themePreference // ignore: cast_nullable_to_non_nullable + as ThemePreferences, + languagePreference: null == languagePreference + ? _self.languagePreference + : languagePreference // ignore: cast_nullable_to_non_nullable + as LanguagePreference, + lockPreference: null == lockPreference + ? _self.lockPreference + : lockPreference // ignore: cast_nullable_to_non_nullable + as LockPreference, + notificationsPreference: null == notificationsPreference + ? _self.notificationsPreference + : notificationsPreference // ignore: cast_nullable_to_non_nullable + as NotificationsPreference, + )); + } + + /// Create a copy of Preferences + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $ThemePreferencesCopyWith<$Res> get themePreference { + return $ThemePreferencesCopyWith<$Res>(_self.themePreference, (value) { + return _then(_self.copyWith(themePreference: value)); + }); + } + + /// Create a copy of Preferences + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $LockPreferenceCopyWith<$Res> get lockPreference { + return $LockPreferenceCopyWith<$Res>(_self.lockPreference, (value) { + return _then(_self.copyWith(lockPreference: value)); + }); + } + + /// Create a copy of Preferences + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $NotificationsPreferenceCopyWith<$Res> get notificationsPreference { + return $NotificationsPreferenceCopyWith<$Res>(_self.notificationsPreference, + (value) { + return _then(_self.copyWith(notificationsPreference: value)); + }); + } +} + +// dart format on diff --git a/lib/settings/models/preferences.g.dart b/lib/settings/models/preferences.g.dart index 5813f67..55f21a7 100644 --- a/lib/settings/models/preferences.g.dart +++ b/lib/settings/models/preferences.g.dart @@ -6,23 +6,21 @@ part of 'preferences.dart'; // JsonSerializableGenerator // ************************************************************************** -_$LockPreferenceImpl _$$LockPreferenceImplFromJson(Map json) => - _$LockPreferenceImpl( +_LockPreference _$LockPreferenceFromJson(Map json) => + _LockPreference( inactivityLockSecs: (json['inactivity_lock_secs'] as num?)?.toInt() ?? 0, lockWhenSwitching: json['lock_when_switching'] as bool? ?? false, lockWithSystemLock: json['lock_with_system_lock'] as bool? ?? false, ); -Map _$$LockPreferenceImplToJson( - _$LockPreferenceImpl instance) => +Map _$LockPreferenceToJson(_LockPreference instance) => { 'inactivity_lock_secs': instance.inactivityLockSecs, 'lock_when_switching': instance.lockWhenSwitching, 'lock_with_system_lock': instance.lockWithSystemLock, }; -_$PreferencesImpl _$$PreferencesImplFromJson(Map json) => - _$PreferencesImpl( +_Preferences _$PreferencesFromJson(Map json) => _Preferences( themePreference: json['theme_preference'] == null ? ThemePreferences.defaults : ThemePreferences.fromJson(json['theme_preference']), @@ -37,7 +35,7 @@ _$PreferencesImpl _$$PreferencesImplFromJson(Map json) => : NotificationsPreference.fromJson(json['notifications_preference']), ); -Map _$$PreferencesImplToJson(_$PreferencesImpl instance) => +Map _$PreferencesToJson(_Preferences instance) => { 'theme_preference': instance.themePreference.toJson(), 'language_preference': instance.languagePreference.toJson(), diff --git a/lib/theme/models/theme_preference.dart b/lib/theme/models/theme_preference.dart index 4be6b4e..aaad52d 100644 --- a/lib/theme/models/theme_preference.dart +++ b/lib/theme/models/theme_preference.dart @@ -49,7 +49,7 @@ enum ColorPreference { } @freezed -class ThemePreferences with _$ThemePreferences { +sealed class ThemePreferences with _$ThemePreferences { const factory ThemePreferences({ @Default(BrightnessPreference.system) BrightnessPreference brightnessPreference, diff --git a/lib/theme/models/theme_preference.freezed.dart b/lib/theme/models/theme_preference.freezed.dart index d96ed38..c915bca 100644 --- a/lib/theme/models/theme_preference.freezed.dart +++ b/lib/theme/models/theme_preference.freezed.dart @@ -1,3 +1,4 @@ +// dart format width=80 // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint @@ -9,176 +10,32 @@ part of 'theme_preference.dart'; // FreezedGenerator // ************************************************************************** +// dart format off 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'); - -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; - bool get enableWallpaper => throw _privateConstructorUsedError; - - /// Serializes this ThemePreferences to a JSON map. - Map toJson() => throw _privateConstructorUsedError; + BrightnessPreference get brightnessPreference; + ColorPreference get colorPreference; + double get displayScale; + bool get enableWallpaper; /// Create a copy of ThemePreferences /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') $ThemePreferencesCopyWith get copyWith => - throw _privateConstructorUsedError; -} + _$ThemePreferencesCopyWithImpl( + this as ThemePreferences, _$identity); -/// @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, - bool enableWallpaper}); -} - -/// @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; - - /// Create a copy of ThemePreferences - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? brightnessPreference = null, - Object? colorPreference = null, - Object? displayScale = null, - Object? enableWallpaper = 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, - enableWallpaper: null == enableWallpaper - ? _value.enableWallpaper - : enableWallpaper // ignore: cast_nullable_to_non_nullable - as bool, - ) 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, - bool enableWallpaper}); -} - -/// @nodoc -class __$$ThemePreferencesImplCopyWithImpl<$Res> - extends _$ThemePreferencesCopyWithImpl<$Res, _$ThemePreferencesImpl> - implements _$$ThemePreferencesImplCopyWith<$Res> { - __$$ThemePreferencesImplCopyWithImpl(_$ThemePreferencesImpl _value, - $Res Function(_$ThemePreferencesImpl) _then) - : super(_value, _then); - - /// Create a copy of ThemePreferences - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? brightnessPreference = null, - Object? colorPreference = null, - Object? displayScale = null, - Object? enableWallpaper = 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, - enableWallpaper: null == enableWallpaper - ? _value.enableWallpaper - : enableWallpaper // ignore: cast_nullable_to_non_nullable - as bool, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$ThemePreferencesImpl implements _ThemePreferences { - const _$ThemePreferencesImpl( - {this.brightnessPreference = BrightnessPreference.system, - this.colorPreference = ColorPreference.vapor, - this.displayScale = 1, - this.enableWallpaper = true}); - - factory _$ThemePreferencesImpl.fromJson(Map json) => - _$$ThemePreferencesImplFromJson(json); - - @override - @JsonKey() - final BrightnessPreference brightnessPreference; - @override - @JsonKey() - final ColorPreference colorPreference; - @override - @JsonKey() - final double displayScale; - @override - @JsonKey() - final bool enableWallpaper; - - @override - String toString() { - return 'ThemePreferences(brightnessPreference: $brightnessPreference, colorPreference: $colorPreference, displayScale: $displayScale, enableWallpaper: $enableWallpaper)'; - } + /// Serializes this ThemePreferences to a JSON map. + Map toJson(); @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$ThemePreferencesImpl && + other is ThemePreferences && (identical(other.brightnessPreference, brightnessPreference) || other.brightnessPreference == brightnessPreference) && (identical(other.colorPreference, colorPreference) || @@ -194,46 +51,181 @@ class _$ThemePreferencesImpl implements _ThemePreferences { int get hashCode => Object.hash(runtimeType, brightnessPreference, colorPreference, displayScale, enableWallpaper); - /// Create a copy of ThemePreferences - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) @override - @pragma('vm:prefer-inline') - _$$ThemePreferencesImplCopyWith<_$ThemePreferencesImpl> get copyWith => - __$$ThemePreferencesImplCopyWithImpl<_$ThemePreferencesImpl>( - this, _$identity); - - @override - Map toJson() { - return _$$ThemePreferencesImplToJson( - this, - ); + String toString() { + return 'ThemePreferences(brightnessPreference: $brightnessPreference, colorPreference: $colorPreference, displayScale: $displayScale, enableWallpaper: $enableWallpaper)'; } } -abstract class _ThemePreferences implements ThemePreferences { - const factory _ThemePreferences( - {final BrightnessPreference brightnessPreference, - final ColorPreference colorPreference, - final double displayScale, - final bool enableWallpaper}) = _$ThemePreferencesImpl; +/// @nodoc +abstract mixin class $ThemePreferencesCopyWith<$Res> { + factory $ThemePreferencesCopyWith( + ThemePreferences value, $Res Function(ThemePreferences) _then) = + _$ThemePreferencesCopyWithImpl; + @useResult + $Res call( + {BrightnessPreference brightnessPreference, + ColorPreference colorPreference, + double displayScale, + bool enableWallpaper}); +} - factory _ThemePreferences.fromJson(Map json) = - _$ThemePreferencesImpl.fromJson; +/// @nodoc +class _$ThemePreferencesCopyWithImpl<$Res> + implements $ThemePreferencesCopyWith<$Res> { + _$ThemePreferencesCopyWithImpl(this._self, this._then); + + final ThemePreferences _self; + final $Res Function(ThemePreferences) _then; + + /// Create a copy of ThemePreferences + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? brightnessPreference = null, + Object? colorPreference = null, + Object? displayScale = null, + Object? enableWallpaper = null, + }) { + return _then(_self.copyWith( + brightnessPreference: null == brightnessPreference + ? _self.brightnessPreference + : brightnessPreference // ignore: cast_nullable_to_non_nullable + as BrightnessPreference, + colorPreference: null == colorPreference + ? _self.colorPreference + : colorPreference // ignore: cast_nullable_to_non_nullable + as ColorPreference, + displayScale: null == displayScale + ? _self.displayScale + : displayScale // ignore: cast_nullable_to_non_nullable + as double, + enableWallpaper: null == enableWallpaper + ? _self.enableWallpaper + : enableWallpaper // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _ThemePreferences implements ThemePreferences { + const _ThemePreferences( + {this.brightnessPreference = BrightnessPreference.system, + this.colorPreference = ColorPreference.vapor, + this.displayScale = 1, + this.enableWallpaper = true}); + factory _ThemePreferences.fromJson(Map json) => + _$ThemePreferencesFromJson(json); @override - BrightnessPreference get brightnessPreference; + @JsonKey() + final BrightnessPreference brightnessPreference; @override - ColorPreference get colorPreference; + @JsonKey() + final ColorPreference colorPreference; @override - double get displayScale; + @JsonKey() + final double displayScale; @override - bool get enableWallpaper; + @JsonKey() + final bool enableWallpaper; /// Create a copy of ThemePreferences /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) - _$$ThemePreferencesImplCopyWith<_$ThemePreferencesImpl> get copyWith => - throw _privateConstructorUsedError; + @pragma('vm:prefer-inline') + _$ThemePreferencesCopyWith<_ThemePreferences> get copyWith => + __$ThemePreferencesCopyWithImpl<_ThemePreferences>(this, _$identity); + + @override + Map toJson() { + return _$ThemePreferencesToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _ThemePreferences && + (identical(other.brightnessPreference, brightnessPreference) || + other.brightnessPreference == brightnessPreference) && + (identical(other.colorPreference, colorPreference) || + other.colorPreference == colorPreference) && + (identical(other.displayScale, displayScale) || + other.displayScale == displayScale) && + (identical(other.enableWallpaper, enableWallpaper) || + other.enableWallpaper == enableWallpaper)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, brightnessPreference, + colorPreference, displayScale, enableWallpaper); + + @override + String toString() { + return 'ThemePreferences(brightnessPreference: $brightnessPreference, colorPreference: $colorPreference, displayScale: $displayScale, enableWallpaper: $enableWallpaper)'; + } } + +/// @nodoc +abstract mixin class _$ThemePreferencesCopyWith<$Res> + implements $ThemePreferencesCopyWith<$Res> { + factory _$ThemePreferencesCopyWith( + _ThemePreferences value, $Res Function(_ThemePreferences) _then) = + __$ThemePreferencesCopyWithImpl; + @override + @useResult + $Res call( + {BrightnessPreference brightnessPreference, + ColorPreference colorPreference, + double displayScale, + bool enableWallpaper}); +} + +/// @nodoc +class __$ThemePreferencesCopyWithImpl<$Res> + implements _$ThemePreferencesCopyWith<$Res> { + __$ThemePreferencesCopyWithImpl(this._self, this._then); + + final _ThemePreferences _self; + final $Res Function(_ThemePreferences) _then; + + /// Create a copy of ThemePreferences + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? brightnessPreference = null, + Object? colorPreference = null, + Object? displayScale = null, + Object? enableWallpaper = null, + }) { + return _then(_ThemePreferences( + brightnessPreference: null == brightnessPreference + ? _self.brightnessPreference + : brightnessPreference // ignore: cast_nullable_to_non_nullable + as BrightnessPreference, + colorPreference: null == colorPreference + ? _self.colorPreference + : colorPreference // ignore: cast_nullable_to_non_nullable + as ColorPreference, + displayScale: null == displayScale + ? _self.displayScale + : displayScale // ignore: cast_nullable_to_non_nullable + as double, + enableWallpaper: null == enableWallpaper + ? _self.enableWallpaper + : enableWallpaper // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +// dart format on diff --git a/lib/theme/models/theme_preference.g.dart b/lib/theme/models/theme_preference.g.dart index 23c3d38..f052e2c 100644 --- a/lib/theme/models/theme_preference.g.dart +++ b/lib/theme/models/theme_preference.g.dart @@ -6,9 +6,8 @@ part of 'theme_preference.dart'; // JsonSerializableGenerator // ************************************************************************** -_$ThemePreferencesImpl _$$ThemePreferencesImplFromJson( - Map json) => - _$ThemePreferencesImpl( +_ThemePreferences _$ThemePreferencesFromJson(Map json) => + _ThemePreferences( brightnessPreference: json['brightness_preference'] == null ? BrightnessPreference.system : BrightnessPreference.fromJson(json['brightness_preference']), @@ -19,8 +18,7 @@ _$ThemePreferencesImpl _$$ThemePreferencesImplFromJson( enableWallpaper: json['enable_wallpaper'] as bool? ?? true, ); -Map _$$ThemePreferencesImplToJson( - _$ThemePreferencesImpl instance) => +Map _$ThemePreferencesToJson(_ThemePreferences instance) => { 'brightness_preference': instance.brightnessPreference.toJson(), 'color_preference': instance.colorPreference.toJson(), diff --git a/lib/tools/loggy.dart b/lib/tools/loggy.dart index 2730888..47a1ffd 100644 --- a/lib/tools/loggy.dart +++ b/lib/tools/loggy.dart @@ -8,6 +8,7 @@ import 'package:intl/intl.dart'; import 'package:loggy/loggy.dart'; import 'package:veilid_support/veilid_support.dart'; +import '../proto/proto.dart'; import '../veilid_processor/views/developer.dart'; import 'state_logger.dart'; @@ -121,6 +122,7 @@ class CallbackPrinter extends LoggyPrinter { callback?.call(record); } + // Change callback function // ignore: use_setters_to_change_properties void setCallback(void Function(LogRecord)? cb) { callback = cb; @@ -147,6 +149,7 @@ void initLoggy() { logOptions: getLogOptions(null), ); + // Allow trace logging from the command line // ignore: do_not_use_environment const isTrace = String.fromEnvironment('LOG_TRACE') != ''; LogLevel logLevel; @@ -159,5 +162,8 @@ void initLoggy() { Loggy('').level = getLogOptions(logLevel); // Create state logger + registerVeilidProtoToDebug(); + registerVeilidDHTProtoToDebug(); + registerVeilidchatProtoToDebug(); Bloc.observer = const StateLogger(); } diff --git a/lib/tools/state_logger.dart b/lib/tools/state_logger.dart index 08e32b3..50dec46 100644 --- a/lib/tools/state_logger.dart +++ b/lib/tools/state_logger.dart @@ -1,5 +1,8 @@ +import 'dart:convert'; + import 'package:bloc/bloc.dart'; import 'package:loggy/loggy.dart'; +import 'package:veilid_support/veilid_support.dart'; import 'loggy.dart'; const Map _blocChangeLogLevels = { @@ -38,7 +41,12 @@ class StateLogger extends BlocObserver { void onChange(BlocBase bloc, Change change) { super.onChange(bloc, change); _checkLogLevel(_blocChangeLogLevels, LogLevel.debug, bloc, (logLevel) { - log.log(logLevel, 'Change: ${bloc.runtimeType} $change'); + const encoder = JsonEncoder.withIndent(' ', DynamicDebug.toDebug); + log.log( + logLevel, + 'Change: ${bloc.runtimeType}\n' + 'currentState: ${encoder.convert(change.currentState)}\n' + 'nextState: ${encoder.convert(change.nextState)}\n'); }); } diff --git a/lib/veilid_processor/models/processor_connection_state.dart b/lib/veilid_processor/models/processor_connection_state.dart index e92ebdc..6b68a8e 100644 --- a/lib/veilid_processor/models/processor_connection_state.dart +++ b/lib/veilid_processor/models/processor_connection_state.dart @@ -4,7 +4,7 @@ import 'package:veilid_support/veilid_support.dart'; part 'processor_connection_state.freezed.dart'; @freezed -class ProcessorConnectionState with _$ProcessorConnectionState { +sealed class ProcessorConnectionState with _$ProcessorConnectionState { const factory ProcessorConnectionState({ required VeilidStateAttachment attachment, required VeilidStateNetwork network, diff --git a/lib/veilid_processor/models/processor_connection_state.freezed.dart b/lib/veilid_processor/models/processor_connection_state.freezed.dart index 87ad295..c7c5288 100644 --- a/lib/veilid_processor/models/processor_connection_state.freezed.dart +++ b/lib/veilid_processor/models/processor_connection_state.freezed.dart @@ -1,3 +1,4 @@ +// dart format width=80 // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint @@ -9,157 +10,27 @@ part of 'processor_connection_state.dart'; // FreezedGenerator // ************************************************************************** +// dart format off 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'); - /// @nodoc mixin _$ProcessorConnectionState { - VeilidStateAttachment get attachment => throw _privateConstructorUsedError; - VeilidStateNetwork get network => throw _privateConstructorUsedError; + VeilidStateAttachment get attachment; + VeilidStateNetwork get network; /// Create a copy of ProcessorConnectionState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') $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; - - /// Create a copy of ProcessorConnectionState - /// with the given fields replaced by the non-null parameter values. - @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); - } - - /// Create a copy of ProcessorConnectionState - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $VeilidStateAttachmentCopyWith<$Res> get attachment { - return $VeilidStateAttachmentCopyWith<$Res>(_value.attachment, (value) { - return _then(_value.copyWith(attachment: value) as $Val); - }); - } - - /// Create a copy of ProcessorConnectionState - /// with the given fields replaced by the non-null parameter values. - @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); - - /// Create a copy of ProcessorConnectionState - /// with the given fields replaced by the non-null parameter values. - @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)'; - } + _$ProcessorConnectionStateCopyWithImpl( + this as ProcessorConnectionState, _$identity); @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$ProcessorConnectionStateImpl && + other is ProcessorConnectionState && (identical(other.attachment, attachment) || other.attachment == attachment) && (identical(other.network, network) || other.network == network)); @@ -168,32 +39,176 @@ class _$ProcessorConnectionStateImpl extends _ProcessorConnectionState { @override int get hashCode => Object.hash(runtimeType, attachment, network); + @override + String toString() { + return 'ProcessorConnectionState(attachment: $attachment, network: $network)'; + } +} + +/// @nodoc +abstract mixin class $ProcessorConnectionStateCopyWith<$Res> { + factory $ProcessorConnectionStateCopyWith(ProcessorConnectionState value, + $Res Function(ProcessorConnectionState) _then) = + _$ProcessorConnectionStateCopyWithImpl; + @useResult + $Res call({VeilidStateAttachment attachment, VeilidStateNetwork network}); + + $VeilidStateAttachmentCopyWith<$Res> get attachment; + $VeilidStateNetworkCopyWith<$Res> get network; +} + +/// @nodoc +class _$ProcessorConnectionStateCopyWithImpl<$Res> + implements $ProcessorConnectionStateCopyWith<$Res> { + _$ProcessorConnectionStateCopyWithImpl(this._self, this._then); + + final ProcessorConnectionState _self; + final $Res Function(ProcessorConnectionState) _then; + + /// Create a copy of ProcessorConnectionState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? attachment = null, + Object? network = null, + }) { + return _then(_self.copyWith( + attachment: null == attachment + ? _self.attachment + : attachment // ignore: cast_nullable_to_non_nullable + as VeilidStateAttachment, + network: null == network + ? _self.network + : network // ignore: cast_nullable_to_non_nullable + as VeilidStateNetwork, + )); + } + /// Create a copy of ProcessorConnectionState /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') - _$$ProcessorConnectionStateImplCopyWith<_$ProcessorConnectionStateImpl> - get copyWith => __$$ProcessorConnectionStateImplCopyWithImpl< - _$ProcessorConnectionStateImpl>(this, _$identity); + $VeilidStateAttachmentCopyWith<$Res> get attachment { + return $VeilidStateAttachmentCopyWith<$Res>(_self.attachment, (value) { + return _then(_self.copyWith(attachment: value)); + }); + } + + /// Create a copy of ProcessorConnectionState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $VeilidStateNetworkCopyWith<$Res> get network { + return $VeilidStateNetworkCopyWith<$Res>(_self.network, (value) { + return _then(_self.copyWith(network: value)); + }); + } } -abstract class _ProcessorConnectionState extends ProcessorConnectionState { - const factory _ProcessorConnectionState( - {required final VeilidStateAttachment attachment, - required final VeilidStateNetwork network}) = - _$ProcessorConnectionStateImpl; - const _ProcessorConnectionState._() : super._(); +/// @nodoc + +class _ProcessorConnectionState extends ProcessorConnectionState { + const _ProcessorConnectionState( + {required this.attachment, required this.network}) + : super._(); @override - VeilidStateAttachment get attachment; + final VeilidStateAttachment attachment; @override - VeilidStateNetwork get network; + final VeilidStateNetwork network; /// Create a copy of ProcessorConnectionState /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) - _$$ProcessorConnectionStateImplCopyWith<_$ProcessorConnectionStateImpl> - get copyWith => throw _privateConstructorUsedError; + @pragma('vm:prefer-inline') + _$ProcessorConnectionStateCopyWith<_ProcessorConnectionState> get copyWith => + __$ProcessorConnectionStateCopyWithImpl<_ProcessorConnectionState>( + this, _$identity); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _ProcessorConnectionState && + (identical(other.attachment, attachment) || + other.attachment == attachment) && + (identical(other.network, network) || other.network == network)); + } + + @override + int get hashCode => Object.hash(runtimeType, attachment, network); + + @override + String toString() { + return 'ProcessorConnectionState(attachment: $attachment, network: $network)'; + } } + +/// @nodoc +abstract mixin class _$ProcessorConnectionStateCopyWith<$Res> + implements $ProcessorConnectionStateCopyWith<$Res> { + factory _$ProcessorConnectionStateCopyWith(_ProcessorConnectionState value, + $Res Function(_ProcessorConnectionState) _then) = + __$ProcessorConnectionStateCopyWithImpl; + @override + @useResult + $Res call({VeilidStateAttachment attachment, VeilidStateNetwork network}); + + @override + $VeilidStateAttachmentCopyWith<$Res> get attachment; + @override + $VeilidStateNetworkCopyWith<$Res> get network; +} + +/// @nodoc +class __$ProcessorConnectionStateCopyWithImpl<$Res> + implements _$ProcessorConnectionStateCopyWith<$Res> { + __$ProcessorConnectionStateCopyWithImpl(this._self, this._then); + + final _ProcessorConnectionState _self; + final $Res Function(_ProcessorConnectionState) _then; + + /// Create a copy of ProcessorConnectionState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? attachment = null, + Object? network = null, + }) { + return _then(_ProcessorConnectionState( + attachment: null == attachment + ? _self.attachment + : attachment // ignore: cast_nullable_to_non_nullable + as VeilidStateAttachment, + network: null == network + ? _self.network + : network // ignore: cast_nullable_to_non_nullable + as VeilidStateNetwork, + )); + } + + /// Create a copy of ProcessorConnectionState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $VeilidStateAttachmentCopyWith<$Res> get attachment { + return $VeilidStateAttachmentCopyWith<$Res>(_self.attachment, (value) { + return _then(_self.copyWith(attachment: value)); + }); + } + + /// Create a copy of ProcessorConnectionState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $VeilidStateNetworkCopyWith<$Res> get network { + return $VeilidStateNetworkCopyWith<$Res>(_self.network, (value) { + return _then(_self.copyWith(network: value)); + }); + } +} + +// dart format on diff --git a/packages/veilid_support/lib/dht_support/proto/proto.dart b/packages/veilid_support/lib/dht_support/proto/proto.dart index 6b36970..ceac3d5 100644 --- a/packages/veilid_support/lib/dht_support/proto/proto.dart +++ b/packages/veilid_support/lib/dht_support/proto/proto.dart @@ -1,5 +1,6 @@ import '../../proto/dht.pb.dart' as dhtproto; import '../../proto/proto.dart' as veilidproto; +import '../../src/dynamic_debug.dart'; import '../dht_support.dart'; export '../../proto/dht.pb.dart'; @@ -23,3 +24,44 @@ extension ProtoOwnedDHTRecordPointer on dhtproto.OwnedDHTRecordPointer { OwnedDHTRecordPointer toVeilid() => OwnedDHTRecordPointer( recordKey: recordKey.toVeilid(), owner: owner.toVeilid()); } + +void registerVeilidDHTProtoToDebug() { + dynamic toDebug(dynamic obj) { + if (obj is dhtproto.OwnedDHTRecordPointer) { + return { + r'$runtimeType': obj.runtimeType, + 'recordKey': obj.recordKey, + 'owner': obj.owner, + }; + } + if (obj is dhtproto.DHTData) { + return { + r'$runtimeType': obj.runtimeType, + 'keys': obj.keys, + 'hash': obj.hash, + 'chunk': obj.chunk, + 'size': obj.size + }; + } + if (obj is dhtproto.DHTLog) { + return { + r'$runtimeType': obj.runtimeType, + 'head': obj.head, + 'tail': obj.tail, + 'stride': obj.stride, + }; + } + if (obj is dhtproto.DHTShortArray) { + return { + r'$runtimeType': obj.runtimeType, + 'keys': obj.keys, + 'index': obj.index, + 'seqs': obj.seqs, + }; + } + + return obj; + } + + DynamicDebug.registerToDebug(toDebug); +} diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart index c299bdc..492312f 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart @@ -31,6 +31,14 @@ class DHTLogStateData extends Equatable { @override List get props => [length, window, windowTail, windowSize, follow]; + + @override + String toString() => 'DHTLogStateData(' + 'length: $length, ' + 'windowTail: $windowTail, ' + 'windowSize: $windowSize, ' + 'follow: $follow, ' + 'window: ${DynamicDebug.toDebug(window)})'; } typedef DHTLogState = AsyncValue>; diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index 8eff1b6..bb27e04 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -126,10 +126,7 @@ class _DHTLogSpine { Future delete() async => _spineMutex.protect(_spineRecord.delete); Future operate(Future Function(_DHTLogSpine) closure) async => - // ignore: prefer_expression_function_bodies - _spineMutex.protect(() async { - return closure(this); - }); + _spineMutex.protect(() async => closure(this)); Future operateAppend(Future Function(_DHTLogSpine) closure) async => _spineMutex.protect(() async { 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 0a51ba1..4e632fc 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 @@ -511,7 +511,7 @@ class DHTRecord implements DHTDeleteable { key, subkeys: [ValueSubkeyRange.single(subkey)], ); - return rr.localSeqs.firstOrNull ?? 0xFFFFFFFF; + return rr.localSeqs.firstOrNull ?? emptySeq; } void _addValueChange( @@ -566,4 +566,6 @@ class DHTRecord implements DHTDeleteable { int _openCount; StreamController? _watchController; _WatchState? _watchState; + + static const int emptySeq = 0xFFFFFFFF; } 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 15c955d..9027799 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 @@ -9,6 +9,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:protobuf/protobuf.dart'; import '../../../../veilid_support.dart'; +import 'extensions.dart'; export 'package:fast_immutable_collections/fast_immutable_collections.dart' show Output; @@ -32,7 +33,7 @@ typedef DHTRecordPoolLogger = void Function(String message); /// Record pool that managed DHTRecords and allows for tagged deletion /// String versions of keys due to IMap<> json unsupported in key @freezed -class DHTRecordPoolAllocations with _$DHTRecordPoolAllocations { +sealed class DHTRecordPoolAllocations with _$DHTRecordPoolAllocations { const factory DHTRecordPoolAllocations({ @Default(IMapConst>({})) IMap> childrenByParent, @@ -49,7 +50,7 @@ class DHTRecordPoolAllocations with _$DHTRecordPoolAllocations { /// Pointer to an owned record, with key, owner key and owner secret /// Ensure that these are only serialized encrypted @freezed -class OwnedDHTRecordPointer with _$OwnedDHTRecordPointer { +sealed class OwnedDHTRecordPointer with _$OwnedDHTRecordPointer { const factory OwnedDHTRecordPointer({ required TypedKey recordKey, required KeyPair owner, @@ -843,8 +844,12 @@ class DHTRecordPool with TableDBBackedJson { log('Timeout in watch cancel for key=$openedRecordKey'); } on VeilidAPIException catch (e) { // Failed to cancel DHT watch, try again next tick - log('Exception in watch cancel for key=$openedRecordKey: $e'); + log('VeilidAPIException in watch cancel for key=$openedRecordKey: $e'); + } catch (e) { + log('Unhandled exception in watch cancel for key=$openedRecordKey: $e'); + rethrow; } + return; } @@ -887,7 +892,10 @@ class DHTRecordPool with TableDBBackedJson { log('Timeout in watch update for key=$openedRecordKey'); } on VeilidAPIException catch (e) { // Failed to update DHT watch, try again next tick - log('Exception in watch update for key=$openedRecordKey: $e'); + log('VeilidAPIException in watch update for key=$openedRecordKey: $e'); + } catch (e) { + log('Unhandled exception in watch update for key=$openedRecordKey: $e'); + rethrow; } // If we still need a state update after this then do a poll instead @@ -904,28 +912,29 @@ class DHTRecordPool with TableDBBackedJson { singleFuture((this, _sfPollWatch, openedRecordKey), () async { final dhtctx = openedRecordInfo.shared.defaultRoutingContext; - // Get single subkey to poll - // XXX: veilid api limits this for now until everyone supports - // inspectDHTRecord - final pollSubkey = unionWatchState.subkeys?.firstSubkey; - if (pollSubkey == null) { - return; + final currentReport = await dhtctx.inspectDHTRecord(openedRecordKey, + subkeys: unionWatchState.subkeys, scope: DHTReportScope.syncGet); + + final fsc = currentReport.firstSeqChange; + if (fsc == null) { + return null; } - final pollSubkeys = [ValueSubkeyRange.single(pollSubkey)]; + final newerSubkeys = currentReport.newerSubkeys; - final currentReport = - await dhtctx.inspectDHTRecord(openedRecordKey, subkeys: pollSubkeys); - final currentSeq = currentReport.localSeqs.firstOrNull ?? -1; - - final valueData = await dhtctx.getDHTValue(openedRecordKey, pollSubkey, + final valueData = await dhtctx.getDHTValue(openedRecordKey, fsc.subkey, forceRefresh: true); if (valueData == null) { return; } - if (valueData.seq > currentSeq) { + + if (valueData.seq < fsc.newSeq) { + log('inspect returned a newer seq than get: ${valueData.seq} < $fsc'); + } + + if (valueData.seq > fsc.oldSeq && valueData.seq != DHTRecord.emptySeq) { processRemoteValueChange(VeilidUpdateValueChange( key: openedRecordKey, - subkeys: pollSubkeys, + subkeys: newerSubkeys, count: 0xFFFFFFFF, value: valueData)); } diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.freezed.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.freezed.dart index 9e51ef8..48372bb 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.freezed.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.freezed.dart @@ -1,3 +1,4 @@ +// dart format width=80 // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint @@ -9,183 +10,32 @@ part of 'dht_record_pool.dart'; // FreezedGenerator // ************************************************************************** +// dart format off 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'); - -DHTRecordPoolAllocations _$DHTRecordPoolAllocationsFromJson( - Map json) { - return _DHTRecordPoolAllocations.fromJson(json); -} - /// @nodoc mixin _$DHTRecordPoolAllocations { - IMap>> get childrenByParent => - throw _privateConstructorUsedError; - IMap> get parentByChild => - throw _privateConstructorUsedError; - ISet> get rootRecords => - throw _privateConstructorUsedError; - IMap get debugNames => throw _privateConstructorUsedError; - - /// Serializes this DHTRecordPoolAllocations to a JSON map. - Map toJson() => throw _privateConstructorUsedError; + IMap> get childrenByParent; + IMap get parentByChild; + ISet get rootRecords; + IMap get debugNames; /// Create a copy of DHTRecordPoolAllocations /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') $DHTRecordPoolAllocationsCopyWith get copyWith => - throw _privateConstructorUsedError; -} + _$DHTRecordPoolAllocationsCopyWithImpl( + this as DHTRecordPoolAllocations, _$identity); -/// @nodoc -abstract class $DHTRecordPoolAllocationsCopyWith<$Res> { - factory $DHTRecordPoolAllocationsCopyWith(DHTRecordPoolAllocations value, - $Res Function(DHTRecordPoolAllocations) then) = - _$DHTRecordPoolAllocationsCopyWithImpl<$Res, DHTRecordPoolAllocations>; - @useResult - $Res call( - {IMap>> childrenByParent, - IMap> parentByChild, - ISet> rootRecords, - IMap debugNames}); -} - -/// @nodoc -class _$DHTRecordPoolAllocationsCopyWithImpl<$Res, - $Val extends DHTRecordPoolAllocations> - implements $DHTRecordPoolAllocationsCopyWith<$Res> { - _$DHTRecordPoolAllocationsCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of DHTRecordPoolAllocations - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? childrenByParent = null, - Object? parentByChild = null, - Object? rootRecords = null, - Object? debugNames = null, - }) { - return _then(_value.copyWith( - childrenByParent: null == childrenByParent - ? _value.childrenByParent - : childrenByParent // ignore: cast_nullable_to_non_nullable - as IMap>>, - parentByChild: null == parentByChild - ? _value.parentByChild - : parentByChild // ignore: cast_nullable_to_non_nullable - as IMap>, - rootRecords: null == rootRecords - ? _value.rootRecords - : rootRecords // ignore: cast_nullable_to_non_nullable - as ISet>, - debugNames: null == debugNames - ? _value.debugNames - : debugNames // ignore: cast_nullable_to_non_nullable - as IMap, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$DHTRecordPoolAllocationsImplCopyWith<$Res> - implements $DHTRecordPoolAllocationsCopyWith<$Res> { - factory _$$DHTRecordPoolAllocationsImplCopyWith( - _$DHTRecordPoolAllocationsImpl value, - $Res Function(_$DHTRecordPoolAllocationsImpl) then) = - __$$DHTRecordPoolAllocationsImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {IMap>> childrenByParent, - IMap> parentByChild, - ISet> rootRecords, - IMap debugNames}); -} - -/// @nodoc -class __$$DHTRecordPoolAllocationsImplCopyWithImpl<$Res> - extends _$DHTRecordPoolAllocationsCopyWithImpl<$Res, - _$DHTRecordPoolAllocationsImpl> - implements _$$DHTRecordPoolAllocationsImplCopyWith<$Res> { - __$$DHTRecordPoolAllocationsImplCopyWithImpl( - _$DHTRecordPoolAllocationsImpl _value, - $Res Function(_$DHTRecordPoolAllocationsImpl) _then) - : super(_value, _then); - - /// Create a copy of DHTRecordPoolAllocations - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? childrenByParent = null, - Object? parentByChild = null, - Object? rootRecords = null, - Object? debugNames = null, - }) { - return _then(_$DHTRecordPoolAllocationsImpl( - childrenByParent: null == childrenByParent - ? _value.childrenByParent - : childrenByParent // ignore: cast_nullable_to_non_nullable - as IMap>>, - parentByChild: null == parentByChild - ? _value.parentByChild - : parentByChild // ignore: cast_nullable_to_non_nullable - as IMap>, - rootRecords: null == rootRecords - ? _value.rootRecords - : rootRecords // ignore: cast_nullable_to_non_nullable - as ISet>, - debugNames: null == debugNames - ? _value.debugNames - : debugNames // ignore: cast_nullable_to_non_nullable - as IMap, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$DHTRecordPoolAllocationsImpl implements _DHTRecordPoolAllocations { - const _$DHTRecordPoolAllocationsImpl( - {this.childrenByParent = const IMapConst>({}), - this.parentByChild = const IMapConst({}), - this.rootRecords = const ISetConst({}), - this.debugNames = const IMapConst({})}); - - factory _$DHTRecordPoolAllocationsImpl.fromJson(Map json) => - _$$DHTRecordPoolAllocationsImplFromJson(json); - - @override - @JsonKey() - final IMap>> childrenByParent; - @override - @JsonKey() - final IMap> parentByChild; - @override - @JsonKey() - final ISet> rootRecords; - @override - @JsonKey() - final IMap debugNames; - - @override - String toString() { - return 'DHTRecordPoolAllocations(childrenByParent: $childrenByParent, parentByChild: $parentByChild, rootRecords: $rootRecords, debugNames: $debugNames)'; - } + /// Serializes this DHTRecordPoolAllocations to a JSON map. + Map toJson(); @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$DHTRecordPoolAllocationsImpl && + other is DHTRecordPoolAllocations && (identical(other.childrenByParent, childrenByParent) || other.childrenByParent == childrenByParent) && (identical(other.parentByChild, parentByChild) || @@ -201,178 +51,205 @@ class _$DHTRecordPoolAllocationsImpl implements _DHTRecordPoolAllocations { int get hashCode => Object.hash(runtimeType, childrenByParent, parentByChild, const DeepCollectionEquality().hash(rootRecords), debugNames); - /// Create a copy of DHTRecordPoolAllocations - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) @override - @pragma('vm:prefer-inline') - _$$DHTRecordPoolAllocationsImplCopyWith<_$DHTRecordPoolAllocationsImpl> - get copyWith => __$$DHTRecordPoolAllocationsImplCopyWithImpl< - _$DHTRecordPoolAllocationsImpl>(this, _$identity); - - @override - Map toJson() { - return _$$DHTRecordPoolAllocationsImplToJson( - this, - ); - } -} - -abstract class _DHTRecordPoolAllocations implements DHTRecordPoolAllocations { - const factory _DHTRecordPoolAllocations( - {final IMap>> childrenByParent, - final IMap> parentByChild, - final ISet> rootRecords, - final IMap debugNames}) = _$DHTRecordPoolAllocationsImpl; - - factory _DHTRecordPoolAllocations.fromJson(Map json) = - _$DHTRecordPoolAllocationsImpl.fromJson; - - @override - IMap>> get childrenByParent; - @override - IMap> get parentByChild; - @override - ISet> get rootRecords; - @override - IMap get debugNames; - - /// Create a copy of DHTRecordPoolAllocations - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$DHTRecordPoolAllocationsImplCopyWith<_$DHTRecordPoolAllocationsImpl> - get copyWith => throw _privateConstructorUsedError; -} - -OwnedDHTRecordPointer _$OwnedDHTRecordPointerFromJson( - Map json) { - return _OwnedDHTRecordPointer.fromJson(json); -} - -/// @nodoc -mixin _$OwnedDHTRecordPointer { - Typed get recordKey => - throw _privateConstructorUsedError; - KeyPair get owner => throw _privateConstructorUsedError; - - /// Serializes this OwnedDHTRecordPointer to a JSON map. - Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of OwnedDHTRecordPointer - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $OwnedDHTRecordPointerCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $OwnedDHTRecordPointerCopyWith<$Res> { - factory $OwnedDHTRecordPointerCopyWith(OwnedDHTRecordPointer value, - $Res Function(OwnedDHTRecordPointer) then) = - _$OwnedDHTRecordPointerCopyWithImpl<$Res, OwnedDHTRecordPointer>; - @useResult - $Res call({Typed recordKey, KeyPair owner}); -} - -/// @nodoc -class _$OwnedDHTRecordPointerCopyWithImpl<$Res, - $Val extends OwnedDHTRecordPointer> - implements $OwnedDHTRecordPointerCopyWith<$Res> { - _$OwnedDHTRecordPointerCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of OwnedDHTRecordPointer - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? recordKey = null, - Object? owner = null, - }) { - return _then(_value.copyWith( - recordKey: null == recordKey - ? _value.recordKey - : recordKey // ignore: cast_nullable_to_non_nullable - as Typed, - owner: null == owner - ? _value.owner - : owner // ignore: cast_nullable_to_non_nullable - as KeyPair, - ) as $Val); + String toString() { + return 'DHTRecordPoolAllocations(childrenByParent: $childrenByParent, parentByChild: $parentByChild, rootRecords: $rootRecords, debugNames: $debugNames)'; } } /// @nodoc -abstract class _$$OwnedDHTRecordPointerImplCopyWith<$Res> - implements $OwnedDHTRecordPointerCopyWith<$Res> { - factory _$$OwnedDHTRecordPointerImplCopyWith( - _$OwnedDHTRecordPointerImpl value, - $Res Function(_$OwnedDHTRecordPointerImpl) then) = - __$$OwnedDHTRecordPointerImplCopyWithImpl<$Res>; - @override +abstract mixin class $DHTRecordPoolAllocationsCopyWith<$Res> { + factory $DHTRecordPoolAllocationsCopyWith(DHTRecordPoolAllocations value, + $Res Function(DHTRecordPoolAllocations) _then) = + _$DHTRecordPoolAllocationsCopyWithImpl; @useResult - $Res call({Typed recordKey, KeyPair owner}); + $Res call( + {IMap>> childrenByParent, + IMap> parentByChild, + ISet> rootRecords, + IMap debugNames}); } /// @nodoc -class __$$OwnedDHTRecordPointerImplCopyWithImpl<$Res> - extends _$OwnedDHTRecordPointerCopyWithImpl<$Res, - _$OwnedDHTRecordPointerImpl> - implements _$$OwnedDHTRecordPointerImplCopyWith<$Res> { - __$$OwnedDHTRecordPointerImplCopyWithImpl(_$OwnedDHTRecordPointerImpl _value, - $Res Function(_$OwnedDHTRecordPointerImpl) _then) - : super(_value, _then); +class _$DHTRecordPoolAllocationsCopyWithImpl<$Res> + implements $DHTRecordPoolAllocationsCopyWith<$Res> { + _$DHTRecordPoolAllocationsCopyWithImpl(this._self, this._then); - /// Create a copy of OwnedDHTRecordPointer + final DHTRecordPoolAllocations _self; + final $Res Function(DHTRecordPoolAllocations) _then; + + /// Create a copy of DHTRecordPoolAllocations /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ - Object? recordKey = null, - Object? owner = null, + Object? childrenByParent = null, + Object? parentByChild = null, + Object? rootRecords = null, + Object? debugNames = null, }) { - return _then(_$OwnedDHTRecordPointerImpl( - recordKey: null == recordKey - ? _value.recordKey - : recordKey // ignore: cast_nullable_to_non_nullable - as Typed, - owner: null == owner - ? _value.owner - : owner // ignore: cast_nullable_to_non_nullable - as KeyPair, + return _then(_self.copyWith( + childrenByParent: null == childrenByParent + ? _self.childrenByParent! + : childrenByParent // ignore: cast_nullable_to_non_nullable + as IMap>>, + parentByChild: null == parentByChild + ? _self.parentByChild! + : parentByChild // ignore: cast_nullable_to_non_nullable + as IMap>, + rootRecords: null == rootRecords + ? _self.rootRecords! + : rootRecords // ignore: cast_nullable_to_non_nullable + as ISet>, + debugNames: null == debugNames + ? _self.debugNames + : debugNames // ignore: cast_nullable_to_non_nullable + as IMap, )); } } /// @nodoc @JsonSerializable() -class _$OwnedDHTRecordPointerImpl implements _OwnedDHTRecordPointer { - const _$OwnedDHTRecordPointerImpl( - {required this.recordKey, required this.owner}); - - factory _$OwnedDHTRecordPointerImpl.fromJson(Map json) => - _$$OwnedDHTRecordPointerImplFromJson(json); +class _DHTRecordPoolAllocations implements DHTRecordPoolAllocations { + const _DHTRecordPoolAllocations( + {this.childrenByParent = const IMapConst>({}), + this.parentByChild = const IMapConst({}), + this.rootRecords = const ISetConst({}), + this.debugNames = const IMapConst({})}); + factory _DHTRecordPoolAllocations.fromJson(Map json) => + _$DHTRecordPoolAllocationsFromJson(json); @override - final Typed recordKey; + @JsonKey() + final IMap>> childrenByParent; @override - final KeyPair owner; + @JsonKey() + final IMap> parentByChild; + @override + @JsonKey() + final ISet> rootRecords; + @override + @JsonKey() + final IMap debugNames; + + /// Create a copy of DHTRecordPoolAllocations + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + _$DHTRecordPoolAllocationsCopyWith<_DHTRecordPoolAllocations> get copyWith => + __$DHTRecordPoolAllocationsCopyWithImpl<_DHTRecordPoolAllocations>( + this, _$identity); @override - String toString() { - return 'OwnedDHTRecordPointer(recordKey: $recordKey, owner: $owner)'; + Map toJson() { + return _$DHTRecordPoolAllocationsToJson( + this, + ); } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$OwnedDHTRecordPointerImpl && + other is _DHTRecordPoolAllocations && + (identical(other.childrenByParent, childrenByParent) || + other.childrenByParent == childrenByParent) && + (identical(other.parentByChild, parentByChild) || + other.parentByChild == parentByChild) && + const DeepCollectionEquality() + .equals(other.rootRecords, rootRecords) && + (identical(other.debugNames, debugNames) || + other.debugNames == debugNames)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, childrenByParent, parentByChild, + const DeepCollectionEquality().hash(rootRecords), debugNames); + + @override + String toString() { + return 'DHTRecordPoolAllocations(childrenByParent: $childrenByParent, parentByChild: $parentByChild, rootRecords: $rootRecords, debugNames: $debugNames)'; + } +} + +/// @nodoc +abstract mixin class _$DHTRecordPoolAllocationsCopyWith<$Res> + implements $DHTRecordPoolAllocationsCopyWith<$Res> { + factory _$DHTRecordPoolAllocationsCopyWith(_DHTRecordPoolAllocations value, + $Res Function(_DHTRecordPoolAllocations) _then) = + __$DHTRecordPoolAllocationsCopyWithImpl; + @override + @useResult + $Res call( + {IMap>> childrenByParent, + IMap> parentByChild, + ISet> rootRecords, + IMap debugNames}); +} + +/// @nodoc +class __$DHTRecordPoolAllocationsCopyWithImpl<$Res> + implements _$DHTRecordPoolAllocationsCopyWith<$Res> { + __$DHTRecordPoolAllocationsCopyWithImpl(this._self, this._then); + + final _DHTRecordPoolAllocations _self; + final $Res Function(_DHTRecordPoolAllocations) _then; + + /// Create a copy of DHTRecordPoolAllocations + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? childrenByParent = null, + Object? parentByChild = null, + Object? rootRecords = null, + Object? debugNames = null, + }) { + return _then(_DHTRecordPoolAllocations( + childrenByParent: null == childrenByParent + ? _self.childrenByParent + : childrenByParent // ignore: cast_nullable_to_non_nullable + as IMap>>, + parentByChild: null == parentByChild + ? _self.parentByChild + : parentByChild // ignore: cast_nullable_to_non_nullable + as IMap>, + rootRecords: null == rootRecords + ? _self.rootRecords + : rootRecords // ignore: cast_nullable_to_non_nullable + as ISet>, + debugNames: null == debugNames + ? _self.debugNames + : debugNames // ignore: cast_nullable_to_non_nullable + as IMap, + )); + } +} + +/// @nodoc +mixin _$OwnedDHTRecordPointer { + TypedKey get recordKey; + KeyPair get owner; + + /// Create a copy of OwnedDHTRecordPointer + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $OwnedDHTRecordPointerCopyWith get copyWith => + _$OwnedDHTRecordPointerCopyWithImpl( + this as OwnedDHTRecordPointer, _$identity); + + /// Serializes this OwnedDHTRecordPointer to a JSON map. + Map toJson(); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is OwnedDHTRecordPointer && (identical(other.recordKey, recordKey) || other.recordKey == recordKey) && (identical(other.owner, owner) || other.owner == owner)); @@ -382,40 +259,136 @@ class _$OwnedDHTRecordPointerImpl implements _OwnedDHTRecordPointer { @override int get hashCode => Object.hash(runtimeType, recordKey, owner); - /// Create a copy of OwnedDHTRecordPointer - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) @override - @pragma('vm:prefer-inline') - _$$OwnedDHTRecordPointerImplCopyWith<_$OwnedDHTRecordPointerImpl> - get copyWith => __$$OwnedDHTRecordPointerImplCopyWithImpl< - _$OwnedDHTRecordPointerImpl>(this, _$identity); - - @override - Map toJson() { - return _$$OwnedDHTRecordPointerImplToJson( - this, - ); + String toString() { + return 'OwnedDHTRecordPointer(recordKey: $recordKey, owner: $owner)'; } } -abstract class _OwnedDHTRecordPointer implements OwnedDHTRecordPointer { - const factory _OwnedDHTRecordPointer( - {required final Typed recordKey, - required final KeyPair owner}) = _$OwnedDHTRecordPointerImpl; +/// @nodoc +abstract mixin class $OwnedDHTRecordPointerCopyWith<$Res> { + factory $OwnedDHTRecordPointerCopyWith(OwnedDHTRecordPointer value, + $Res Function(OwnedDHTRecordPointer) _then) = + _$OwnedDHTRecordPointerCopyWithImpl; + @useResult + $Res call({Typed recordKey, KeyPair owner}); +} - factory _OwnedDHTRecordPointer.fromJson(Map json) = - _$OwnedDHTRecordPointerImpl.fromJson; +/// @nodoc +class _$OwnedDHTRecordPointerCopyWithImpl<$Res> + implements $OwnedDHTRecordPointerCopyWith<$Res> { + _$OwnedDHTRecordPointerCopyWithImpl(this._self, this._then); + + final OwnedDHTRecordPointer _self; + final $Res Function(OwnedDHTRecordPointer) _then; + + /// Create a copy of OwnedDHTRecordPointer + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? recordKey = null, + Object? owner = null, + }) { + return _then(_self.copyWith( + recordKey: null == recordKey + ? _self.recordKey! + : recordKey // ignore: cast_nullable_to_non_nullable + as Typed, + owner: null == owner + ? _self.owner + : owner // ignore: cast_nullable_to_non_nullable + as KeyPair, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _OwnedDHTRecordPointer implements OwnedDHTRecordPointer { + const _OwnedDHTRecordPointer({required this.recordKey, required this.owner}); + factory _OwnedDHTRecordPointer.fromJson(Map json) => + _$OwnedDHTRecordPointerFromJson(json); @override - Typed get recordKey; + final Typed recordKey; @override - KeyPair get owner; + final KeyPair owner; /// Create a copy of OwnedDHTRecordPointer /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) - _$$OwnedDHTRecordPointerImplCopyWith<_$OwnedDHTRecordPointerImpl> - get copyWith => throw _privateConstructorUsedError; + @pragma('vm:prefer-inline') + _$OwnedDHTRecordPointerCopyWith<_OwnedDHTRecordPointer> get copyWith => + __$OwnedDHTRecordPointerCopyWithImpl<_OwnedDHTRecordPointer>( + this, _$identity); + + @override + Map toJson() { + return _$OwnedDHTRecordPointerToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _OwnedDHTRecordPointer && + (identical(other.recordKey, recordKey) || + other.recordKey == recordKey) && + (identical(other.owner, owner) || other.owner == owner)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, recordKey, owner); + + @override + String toString() { + return 'OwnedDHTRecordPointer(recordKey: $recordKey, owner: $owner)'; + } } + +/// @nodoc +abstract mixin class _$OwnedDHTRecordPointerCopyWith<$Res> + implements $OwnedDHTRecordPointerCopyWith<$Res> { + factory _$OwnedDHTRecordPointerCopyWith(_OwnedDHTRecordPointer value, + $Res Function(_OwnedDHTRecordPointer) _then) = + __$OwnedDHTRecordPointerCopyWithImpl; + @override + @useResult + $Res call({Typed recordKey, KeyPair owner}); +} + +/// @nodoc +class __$OwnedDHTRecordPointerCopyWithImpl<$Res> + implements _$OwnedDHTRecordPointerCopyWith<$Res> { + __$OwnedDHTRecordPointerCopyWithImpl(this._self, this._then); + + final _OwnedDHTRecordPointer _self; + final $Res Function(_OwnedDHTRecordPointer) _then; + + /// Create a copy of OwnedDHTRecordPointer + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? recordKey = null, + Object? owner = null, + }) { + return _then(_OwnedDHTRecordPointer( + recordKey: null == recordKey + ? _self.recordKey + : recordKey // ignore: cast_nullable_to_non_nullable + as Typed, + owner: null == owner + ? _self.owner + : owner // ignore: cast_nullable_to_non_nullable + as KeyPair, + )); + } +} + +// dart format on diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.g.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.g.dart index 12b3a1e..c2c031f 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.g.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.g.dart @@ -6,9 +6,9 @@ part of 'dht_record_pool.dart'; // JsonSerializableGenerator // ************************************************************************** -_$DHTRecordPoolAllocationsImpl _$$DHTRecordPoolAllocationsImplFromJson( +_DHTRecordPoolAllocations _$DHTRecordPoolAllocationsFromJson( Map json) => - _$DHTRecordPoolAllocationsImpl( + _DHTRecordPoolAllocations( childrenByParent: json['children_by_parent'] == null ? const IMapConst>({}) : IMap>>.fromJson( @@ -34,8 +34,8 @@ _$DHTRecordPoolAllocationsImpl _$$DHTRecordPoolAllocationsImplFromJson( (value) => value as String), ); -Map _$$DHTRecordPoolAllocationsImplToJson( - _$DHTRecordPoolAllocationsImpl instance) => +Map _$DHTRecordPoolAllocationsToJson( + _DHTRecordPoolAllocations instance) => { 'children_by_parent': instance.childrenByParent.toJson( (value) => value, @@ -56,15 +56,15 @@ Map _$$DHTRecordPoolAllocationsImplToJson( ), }; -_$OwnedDHTRecordPointerImpl _$$OwnedDHTRecordPointerImplFromJson( +_OwnedDHTRecordPointer _$OwnedDHTRecordPointerFromJson( Map json) => - _$OwnedDHTRecordPointerImpl( + _OwnedDHTRecordPointer( recordKey: Typed.fromJson(json['record_key']), owner: KeyPair.fromJson(json['owner']), ); -Map _$$OwnedDHTRecordPointerImplToJson( - _$OwnedDHTRecordPointerImpl instance) => +Map _$OwnedDHTRecordPointerToJson( + _OwnedDHTRecordPointer instance) => { 'record_key': instance.recordKey.toJson(), 'owner': instance.owner.toJson(), diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/extensions.dart b/packages/veilid_support/lib/dht_support/src/dht_record/extensions.dart new file mode 100644 index 0000000..e62403e --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/dht_record/extensions.dart @@ -0,0 +1,57 @@ +import 'package:veilid/veilid.dart'; +import 'dht_record_pool.dart'; + +class DHTSeqChange { + const DHTSeqChange(this.subkey, this.oldSeq, this.newSeq); + final int subkey; + final int oldSeq; + final int newSeq; +} + +extension DHTReportReportExt on DHTRecordReport { + List get newerSubkeys { + if (networkSeqs.isEmpty || localSeqs.isEmpty || subkeys.isEmpty) { + return []; + } + + final currentSubkeys = []; + + var i = 0; + for (final skr in subkeys) { + for (var sk = skr.low; sk <= skr.high; sk++) { + if (networkSeqs[i] > localSeqs[i] && + networkSeqs[i] != DHTRecord.emptySeq) { + if (currentSubkeys.isNotEmpty && + currentSubkeys.last.high == (sk - 1)) { + currentSubkeys.add(ValueSubkeyRange( + low: currentSubkeys.removeLast().low, high: sk)); + } else { + currentSubkeys.add(ValueSubkeyRange.single(sk)); + } + } + i++; + } + } + + return currentSubkeys; + } + + DHTSeqChange? get firstSeqChange { + if (networkSeqs.isEmpty || localSeqs.isEmpty || subkeys.isEmpty) { + return null; + } + + var i = 0; + for (final skr in subkeys) { + for (var sk = skr.low; sk <= skr.high; sk++) { + if (networkSeqs[i] > localSeqs[i] && + networkSeqs[i] != DHTRecord.emptySeq) { + return DHTSeqChange(sk, localSeqs[i], networkSeqs[i]); + } + i++; + } + } + + return null; + } +} 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 ab56c77..6ff6d95 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 @@ -3,27 +3,14 @@ import 'dart:async'; import 'package:async_tools/async_tools.dart'; import 'package:bloc/bloc.dart'; import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; -import 'package:equatable/equatable.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:meta/meta.dart'; import '../../../veilid_support.dart'; -@immutable -class DHTShortArrayElementState extends Equatable { - const DHTShortArrayElementState( - {required this.value, required this.isOffline}); - final T value; - final bool isOffline; +typedef DHTShortArrayState = AsyncValue>>; +typedef DHTShortArrayCubitState = BlocBusyState>; - @override - List get props => [value, isOffline]; -} - -typedef DHTShortArrayState = AsyncValue>>; -typedef DHTShortArrayBusyState = BlocBusyState>; - -class DHTShortArrayCubit extends Cubit> +class DHTShortArrayCubit extends Cubit> with BlocBusyWrapper>, RefreshableCubit { DHTShortArrayCubit({ required Future Function() open, @@ -46,7 +33,7 @@ class DHTShortArrayCubit extends Cubit> } } on Exception catch (e, st) { addError(e, st); - emit(DHTShortArrayBusyState(AsyncValue.error(e, st))); + emit(DHTShortArrayCubitState(AsyncValue.error(e, st))); return; } @@ -83,7 +70,7 @@ class DHTShortArrayCubit extends Cubit> // Get the items final allItems = (await reader.getRange(0, forceRefresh: forceRefresh)) ?.indexed - .map((x) => DHTShortArrayElementState( + .map((x) => OnlineElementState( value: _decodeElement(x.$2), isOffline: offlinePositions?.contains(x.$1) ?? false)) .toIList(); 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 0aaed19..49659cd 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 @@ -333,7 +333,7 @@ class _DHTShortArrayHead { } Future lookupIndex(int idx, bool allowCreate) async { - final seq = idx < _seqs.length ? _seqs[idx] : 0xFFFFFFFF; + final seq = idx < _seqs.length ? _seqs[idx] : DHTRecord.emptySeq; final recordNumber = idx ~/ _stride; final record = await _getOrCreateLinkedRecord(recordNumber, allowCreate); final recordSubkey = (idx % _stride) + ((recordNumber == 0) ? 1 : 0); @@ -427,14 +427,14 @@ class _DHTShortArrayHead { // 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 < idx || _localSeqs[idx] == 0xFFFFFFFF) { + if (_localSeqs.length < idx || _localSeqs[idx] == DHTRecord.emptySeq) { 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 < idx || _seqs[idx] == 0xFFFFFFFF) { + if (_seqs.length < idx || _seqs[idx] == DHTRecord.emptySeq) { return false; } @@ -448,12 +448,12 @@ class _DHTShortArrayHead { final idx = _index[pos]; while (_localSeqs.length <= idx) { - _localSeqs.add(0xFFFFFFFF); + _localSeqs.add(DHTRecord.emptySeq); } _localSeqs[idx] = newSeq; if (write) { while (_seqs.length <= idx) { - _seqs.add(0xFFFFFFFF); + _seqs.add(DHTRecord.emptySeq); } _seqs[idx] = newSeq; } 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 index f3e1ac3..51950f6 100644 --- 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 @@ -122,7 +122,7 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead final outSeqNum = Output(); - final result = lookup.seq == 0xFFFFFFFF + final result = lookup.seq == DHTRecord.emptySeq ? null : await lookup.record.get(subkey: lookup.recordSubkey); @@ -151,7 +151,7 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead final lookup = await _head.lookupPosition(pos, true); final outSeqNumRead = Output(); - final oldValue = lookup.seq == 0xFFFFFFFF + final oldValue = lookup.seq == DHTRecord.emptySeq ? null : await lookup.record .get(subkey: lookup.recordSubkey, outSeqNum: outSeqNumRead); diff --git a/packages/veilid_support/lib/identity_support/account_record_info.dart b/packages/veilid_support/lib/identity_support/account_record_info.dart index 60accf9..c74baac 100644 --- a/packages/veilid_support/lib/identity_support/account_record_info.dart +++ b/packages/veilid_support/lib/identity_support/account_record_info.dart @@ -8,7 +8,7 @@ part 'account_record_info.g.dart'; /// AccountRecordInfo is the key and owner info for the account dht record that /// is stored in the identity instance record @freezed -class AccountRecordInfo with _$AccountRecordInfo { +sealed class AccountRecordInfo with _$AccountRecordInfo { const factory AccountRecordInfo({ // Top level account keys and secrets required OwnedDHTRecordPointer accountRecord, diff --git a/packages/veilid_support/lib/identity_support/account_record_info.freezed.dart b/packages/veilid_support/lib/identity_support/account_record_info.freezed.dart index a266230..b1796f6 100644 --- a/packages/veilid_support/lib/identity_support/account_record_info.freezed.dart +++ b/packages/veilid_support/lib/identity_support/account_record_info.freezed.dart @@ -1,3 +1,4 @@ +// dart format width=80 // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint @@ -9,137 +10,30 @@ part of 'account_record_info.dart'; // FreezedGenerator // ************************************************************************** +// dart format off 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'); - -AccountRecordInfo _$AccountRecordInfoFromJson(Map json) { - return _AccountRecordInfo.fromJson(json); -} - /// @nodoc mixin _$AccountRecordInfo { // Top level account keys and secrets - OwnedDHTRecordPointer get accountRecord => throw _privateConstructorUsedError; - - /// Serializes this AccountRecordInfo to a JSON map. - Map toJson() => throw _privateConstructorUsedError; + OwnedDHTRecordPointer get accountRecord; /// Create a copy of AccountRecordInfo /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') $AccountRecordInfoCopyWith get copyWith => - throw _privateConstructorUsedError; -} + _$AccountRecordInfoCopyWithImpl( + this as AccountRecordInfo, _$identity); -/// @nodoc -abstract class $AccountRecordInfoCopyWith<$Res> { - factory $AccountRecordInfoCopyWith( - AccountRecordInfo value, $Res Function(AccountRecordInfo) then) = - _$AccountRecordInfoCopyWithImpl<$Res, AccountRecordInfo>; - @useResult - $Res call({OwnedDHTRecordPointer accountRecord}); - - $OwnedDHTRecordPointerCopyWith<$Res> get accountRecord; -} - -/// @nodoc -class _$AccountRecordInfoCopyWithImpl<$Res, $Val extends AccountRecordInfo> - implements $AccountRecordInfoCopyWith<$Res> { - _$AccountRecordInfoCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of AccountRecordInfo - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? accountRecord = null, - }) { - return _then(_value.copyWith( - accountRecord: null == accountRecord - ? _value.accountRecord - : accountRecord // ignore: cast_nullable_to_non_nullable - as OwnedDHTRecordPointer, - ) as $Val); - } - - /// Create a copy of AccountRecordInfo - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $OwnedDHTRecordPointerCopyWith<$Res> get accountRecord { - return $OwnedDHTRecordPointerCopyWith<$Res>(_value.accountRecord, (value) { - return _then(_value.copyWith(accountRecord: value) as $Val); - }); - } -} - -/// @nodoc -abstract class _$$AccountRecordInfoImplCopyWith<$Res> - implements $AccountRecordInfoCopyWith<$Res> { - factory _$$AccountRecordInfoImplCopyWith(_$AccountRecordInfoImpl value, - $Res Function(_$AccountRecordInfoImpl) then) = - __$$AccountRecordInfoImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({OwnedDHTRecordPointer accountRecord}); - - @override - $OwnedDHTRecordPointerCopyWith<$Res> get accountRecord; -} - -/// @nodoc -class __$$AccountRecordInfoImplCopyWithImpl<$Res> - extends _$AccountRecordInfoCopyWithImpl<$Res, _$AccountRecordInfoImpl> - implements _$$AccountRecordInfoImplCopyWith<$Res> { - __$$AccountRecordInfoImplCopyWithImpl(_$AccountRecordInfoImpl _value, - $Res Function(_$AccountRecordInfoImpl) _then) - : super(_value, _then); - - /// Create a copy of AccountRecordInfo - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? accountRecord = null, - }) { - return _then(_$AccountRecordInfoImpl( - accountRecord: null == accountRecord - ? _value.accountRecord - : accountRecord // ignore: cast_nullable_to_non_nullable - as OwnedDHTRecordPointer, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$AccountRecordInfoImpl implements _AccountRecordInfo { - const _$AccountRecordInfoImpl({required this.accountRecord}); - - factory _$AccountRecordInfoImpl.fromJson(Map json) => - _$$AccountRecordInfoImplFromJson(json); - -// Top level account keys and secrets - @override - final OwnedDHTRecordPointer accountRecord; - - @override - String toString() { - return 'AccountRecordInfo(accountRecord: $accountRecord)'; - } + /// Serializes this AccountRecordInfo to a JSON map. + Map toJson(); @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$AccountRecordInfoImpl && + other is AccountRecordInfo && (identical(other.accountRecord, accountRecord) || other.accountRecord == accountRecord)); } @@ -148,39 +42,148 @@ class _$AccountRecordInfoImpl implements _AccountRecordInfo { @override int get hashCode => Object.hash(runtimeType, accountRecord); - /// Create a copy of AccountRecordInfo - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) @override - @pragma('vm:prefer-inline') - _$$AccountRecordInfoImplCopyWith<_$AccountRecordInfoImpl> get copyWith => - __$$AccountRecordInfoImplCopyWithImpl<_$AccountRecordInfoImpl>( - this, _$identity); - - @override - Map toJson() { - return _$$AccountRecordInfoImplToJson( - this, - ); + String toString() { + return 'AccountRecordInfo(accountRecord: $accountRecord)'; } } -abstract class _AccountRecordInfo implements AccountRecordInfo { - const factory _AccountRecordInfo( - {required final OwnedDHTRecordPointer accountRecord}) = - _$AccountRecordInfoImpl; +/// @nodoc +abstract mixin class $AccountRecordInfoCopyWith<$Res> { + factory $AccountRecordInfoCopyWith( + AccountRecordInfo value, $Res Function(AccountRecordInfo) _then) = + _$AccountRecordInfoCopyWithImpl; + @useResult + $Res call({OwnedDHTRecordPointer accountRecord}); - factory _AccountRecordInfo.fromJson(Map json) = - _$AccountRecordInfoImpl.fromJson; + $OwnedDHTRecordPointerCopyWith<$Res> get accountRecord; +} + +/// @nodoc +class _$AccountRecordInfoCopyWithImpl<$Res> + implements $AccountRecordInfoCopyWith<$Res> { + _$AccountRecordInfoCopyWithImpl(this._self, this._then); + + final AccountRecordInfo _self; + final $Res Function(AccountRecordInfo) _then; + + /// Create a copy of AccountRecordInfo + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? accountRecord = null, + }) { + return _then(_self.copyWith( + accountRecord: null == accountRecord + ? _self.accountRecord + : accountRecord // ignore: cast_nullable_to_non_nullable + as OwnedDHTRecordPointer, + )); + } + + /// Create a copy of AccountRecordInfo + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $OwnedDHTRecordPointerCopyWith<$Res> get accountRecord { + return $OwnedDHTRecordPointerCopyWith<$Res>(_self.accountRecord, (value) { + return _then(_self.copyWith(accountRecord: value)); + }); + } +} + +/// @nodoc +@JsonSerializable() +class _AccountRecordInfo implements AccountRecordInfo { + const _AccountRecordInfo({required this.accountRecord}); + factory _AccountRecordInfo.fromJson(Map json) => + _$AccountRecordInfoFromJson(json); // Top level account keys and secrets @override - OwnedDHTRecordPointer get accountRecord; + final OwnedDHTRecordPointer accountRecord; /// Create a copy of AccountRecordInfo /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) - _$$AccountRecordInfoImplCopyWith<_$AccountRecordInfoImpl> get copyWith => - throw _privateConstructorUsedError; + @pragma('vm:prefer-inline') + _$AccountRecordInfoCopyWith<_AccountRecordInfo> get copyWith => + __$AccountRecordInfoCopyWithImpl<_AccountRecordInfo>(this, _$identity); + + @override + Map toJson() { + return _$AccountRecordInfoToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _AccountRecordInfo && + (identical(other.accountRecord, accountRecord) || + other.accountRecord == accountRecord)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, accountRecord); + + @override + String toString() { + return 'AccountRecordInfo(accountRecord: $accountRecord)'; + } } + +/// @nodoc +abstract mixin class _$AccountRecordInfoCopyWith<$Res> + implements $AccountRecordInfoCopyWith<$Res> { + factory _$AccountRecordInfoCopyWith( + _AccountRecordInfo value, $Res Function(_AccountRecordInfo) _then) = + __$AccountRecordInfoCopyWithImpl; + @override + @useResult + $Res call({OwnedDHTRecordPointer accountRecord}); + + @override + $OwnedDHTRecordPointerCopyWith<$Res> get accountRecord; +} + +/// @nodoc +class __$AccountRecordInfoCopyWithImpl<$Res> + implements _$AccountRecordInfoCopyWith<$Res> { + __$AccountRecordInfoCopyWithImpl(this._self, this._then); + + final _AccountRecordInfo _self; + final $Res Function(_AccountRecordInfo) _then; + + /// Create a copy of AccountRecordInfo + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? accountRecord = null, + }) { + return _then(_AccountRecordInfo( + accountRecord: null == accountRecord + ? _self.accountRecord + : accountRecord // ignore: cast_nullable_to_non_nullable + as OwnedDHTRecordPointer, + )); + } + + /// Create a copy of AccountRecordInfo + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $OwnedDHTRecordPointerCopyWith<$Res> get accountRecord { + return $OwnedDHTRecordPointerCopyWith<$Res>(_self.accountRecord, (value) { + return _then(_self.copyWith(accountRecord: value)); + }); + } +} + +// dart format on diff --git a/packages/veilid_support/lib/identity_support/account_record_info.g.dart b/packages/veilid_support/lib/identity_support/account_record_info.g.dart index ad9318c..429f9d0 100644 --- a/packages/veilid_support/lib/identity_support/account_record_info.g.dart +++ b/packages/veilid_support/lib/identity_support/account_record_info.g.dart @@ -6,14 +6,12 @@ part of 'account_record_info.dart'; // JsonSerializableGenerator // ************************************************************************** -_$AccountRecordInfoImpl _$$AccountRecordInfoImplFromJson( - Map json) => - _$AccountRecordInfoImpl( +_AccountRecordInfo _$AccountRecordInfoFromJson(Map json) => + _AccountRecordInfo( accountRecord: OwnedDHTRecordPointer.fromJson(json['account_record']), ); -Map _$$AccountRecordInfoImplToJson( - _$AccountRecordInfoImpl instance) => +Map _$AccountRecordInfoToJson(_AccountRecordInfo instance) => { 'account_record': instance.accountRecord.toJson(), }; diff --git a/packages/veilid_support/lib/identity_support/identity.dart b/packages/veilid_support/lib/identity_support/identity.dart index ea9c38c..c1c7113 100644 --- a/packages/veilid_support/lib/identity_support/identity.dart +++ b/packages/veilid_support/lib/identity_support/identity.dart @@ -14,7 +14,7 @@ part 'identity.g.dart'; /// DHT Secret: IdentityInstance Secret Key (stored encrypted with unlock code /// in local table store) @freezed -class Identity with _$Identity { +sealed class Identity with _$Identity { const factory Identity({ // Top level account keys and secrets required IMap> accountRecords, diff --git a/packages/veilid_support/lib/identity_support/identity.freezed.dart b/packages/veilid_support/lib/identity_support/identity.freezed.dart index 3a276b0..d9f08f9 100644 --- a/packages/veilid_support/lib/identity_support/identity.freezed.dart +++ b/packages/veilid_support/lib/identity_support/identity.freezed.dart @@ -1,3 +1,4 @@ +// dart format width=80 // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint @@ -9,122 +10,29 @@ part of 'identity.dart'; // FreezedGenerator // ************************************************************************** +// dart format off 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'); - -Identity _$IdentityFromJson(Map json) { - return _Identity.fromJson(json); -} - /// @nodoc mixin _$Identity { // Top level account keys and secrets - IMap> get accountRecords => - throw _privateConstructorUsedError; - - /// Serializes this Identity to a JSON map. - Map toJson() => throw _privateConstructorUsedError; + IMap> get accountRecords; /// Create a copy of Identity /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') $IdentityCopyWith get copyWith => - throw _privateConstructorUsedError; -} + _$IdentityCopyWithImpl(this as Identity, _$identity); -/// @nodoc -abstract class $IdentityCopyWith<$Res> { - factory $IdentityCopyWith(Identity value, $Res Function(Identity) then) = - _$IdentityCopyWithImpl<$Res, Identity>; - @useResult - $Res call({IMap> accountRecords}); -} - -/// @nodoc -class _$IdentityCopyWithImpl<$Res, $Val extends Identity> - implements $IdentityCopyWith<$Res> { - _$IdentityCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of Identity - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? accountRecords = null, - }) { - return _then(_value.copyWith( - accountRecords: null == accountRecords - ? _value.accountRecords - : accountRecords // ignore: cast_nullable_to_non_nullable - as IMap>, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$IdentityImplCopyWith<$Res> - implements $IdentityCopyWith<$Res> { - factory _$$IdentityImplCopyWith( - _$IdentityImpl value, $Res Function(_$IdentityImpl) then) = - __$$IdentityImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({IMap> accountRecords}); -} - -/// @nodoc -class __$$IdentityImplCopyWithImpl<$Res> - extends _$IdentityCopyWithImpl<$Res, _$IdentityImpl> - implements _$$IdentityImplCopyWith<$Res> { - __$$IdentityImplCopyWithImpl( - _$IdentityImpl _value, $Res Function(_$IdentityImpl) _then) - : super(_value, _then); - - /// Create a copy of Identity - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? accountRecords = null, - }) { - return _then(_$IdentityImpl( - accountRecords: null == accountRecords - ? _value.accountRecords - : accountRecords // ignore: cast_nullable_to_non_nullable - as IMap>, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$IdentityImpl implements _Identity { - const _$IdentityImpl({required this.accountRecords}); - - factory _$IdentityImpl.fromJson(Map json) => - _$$IdentityImplFromJson(json); - -// Top level account keys and secrets - @override - final IMap> accountRecords; - - @override - String toString() { - return 'Identity(accountRecords: $accountRecords)'; - } + /// Serializes this Identity to a JSON map. + Map toJson(); @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$IdentityImpl && + other is Identity && (identical(other.accountRecords, accountRecords) || other.accountRecords == accountRecords)); } @@ -133,38 +41,119 @@ class _$IdentityImpl implements _Identity { @override int get hashCode => Object.hash(runtimeType, accountRecords); - /// Create a copy of Identity - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) @override - @pragma('vm:prefer-inline') - _$$IdentityImplCopyWith<_$IdentityImpl> get copyWith => - __$$IdentityImplCopyWithImpl<_$IdentityImpl>(this, _$identity); - - @override - Map toJson() { - return _$$IdentityImplToJson( - this, - ); + String toString() { + return 'Identity(accountRecords: $accountRecords)'; } } -abstract class _Identity implements Identity { - const factory _Identity( - {required final IMap> - accountRecords}) = _$IdentityImpl; +/// @nodoc +abstract mixin class $IdentityCopyWith<$Res> { + factory $IdentityCopyWith(Identity value, $Res Function(Identity) _then) = + _$IdentityCopyWithImpl; + @useResult + $Res call({IMap> accountRecords}); +} - factory _Identity.fromJson(Map json) = - _$IdentityImpl.fromJson; +/// @nodoc +class _$IdentityCopyWithImpl<$Res> implements $IdentityCopyWith<$Res> { + _$IdentityCopyWithImpl(this._self, this._then); + + final Identity _self; + final $Res Function(Identity) _then; + + /// Create a copy of Identity + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? accountRecords = null, + }) { + return _then(_self.copyWith( + accountRecords: null == accountRecords + ? _self.accountRecords + : accountRecords // ignore: cast_nullable_to_non_nullable + as IMap>, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _Identity implements Identity { + const _Identity({required this.accountRecords}); + factory _Identity.fromJson(Map json) => + _$IdentityFromJson(json); // Top level account keys and secrets @override - IMap> get accountRecords; + final IMap> accountRecords; /// Create a copy of Identity /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) - _$$IdentityImplCopyWith<_$IdentityImpl> get copyWith => - throw _privateConstructorUsedError; + @pragma('vm:prefer-inline') + _$IdentityCopyWith<_Identity> get copyWith => + __$IdentityCopyWithImpl<_Identity>(this, _$identity); + + @override + Map toJson() { + return _$IdentityToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _Identity && + (identical(other.accountRecords, accountRecords) || + other.accountRecords == accountRecords)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, accountRecords); + + @override + String toString() { + return 'Identity(accountRecords: $accountRecords)'; + } } + +/// @nodoc +abstract mixin class _$IdentityCopyWith<$Res> + implements $IdentityCopyWith<$Res> { + factory _$IdentityCopyWith(_Identity value, $Res Function(_Identity) _then) = + __$IdentityCopyWithImpl; + @override + @useResult + $Res call({IMap> accountRecords}); +} + +/// @nodoc +class __$IdentityCopyWithImpl<$Res> implements _$IdentityCopyWith<$Res> { + __$IdentityCopyWithImpl(this._self, this._then); + + final _Identity _self; + final $Res Function(_Identity) _then; + + /// Create a copy of Identity + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? accountRecords = null, + }) { + return _then(_Identity( + accountRecords: null == accountRecords + ? _self.accountRecords + : accountRecords // ignore: cast_nullable_to_non_nullable + as IMap>, + )); + } +} + +// dart format on diff --git a/packages/veilid_support/lib/identity_support/identity.g.dart b/packages/veilid_support/lib/identity_support/identity.g.dart index afc9088..1ee10b8 100644 --- a/packages/veilid_support/lib/identity_support/identity.g.dart +++ b/packages/veilid_support/lib/identity_support/identity.g.dart @@ -6,8 +6,7 @@ part of 'identity.dart'; // JsonSerializableGenerator // ************************************************************************** -_$IdentityImpl _$$IdentityImplFromJson(Map json) => - _$IdentityImpl( +_Identity _$IdentityFromJson(Map json) => _Identity( accountRecords: IMap>.fromJson( json['account_records'] as Map, (value) => value as String, @@ -15,8 +14,7 @@ _$IdentityImpl _$$IdentityImplFromJson(Map json) => value, (value) => AccountRecordInfo.fromJson(value))), ); -Map _$$IdentityImplToJson(_$IdentityImpl instance) => - { +Map _$IdentityToJson(_Identity instance) => { 'account_records': instance.accountRecords.toJson( (value) => value, (value) => value.toJson( diff --git a/packages/veilid_support/lib/identity_support/identity_instance.dart b/packages/veilid_support/lib/identity_support/identity_instance.dart index 1b6bf1f..d2bc323 100644 --- a/packages/veilid_support/lib/identity_support/identity_instance.dart +++ b/packages/veilid_support/lib/identity_support/identity_instance.dart @@ -10,7 +10,7 @@ part 'identity_instance.freezed.dart'; part 'identity_instance.g.dart'; @freezed -class IdentityInstance with _$IdentityInstance { +sealed class IdentityInstance with _$IdentityInstance { const factory IdentityInstance({ // Private DHT record storing identity account mapping required TypedKey recordKey, diff --git a/packages/veilid_support/lib/identity_support/identity_instance.freezed.dart b/packages/veilid_support/lib/identity_support/identity_instance.freezed.dart index 28bbad4..42522d4 100644 --- a/packages/veilid_support/lib/identity_support/identity_instance.freezed.dart +++ b/packages/veilid_support/lib/identity_support/identity_instance.freezed.dart @@ -1,3 +1,4 @@ +// dart format width=80 // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint @@ -9,112 +10,76 @@ part of 'identity_instance.dart'; // FreezedGenerator // ************************************************************************** +// dart format off 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'); - -IdentityInstance _$IdentityInstanceFromJson(Map json) { - return _IdentityInstance.fromJson(json); -} - /// @nodoc mixin _$IdentityInstance { // Private DHT record storing identity account mapping - Typed get recordKey => - throw _privateConstructorUsedError; // Public key of identity instance - FixedEncodedString43 get publicKey => - throw _privateConstructorUsedError; // Secret key of identity instance + TypedKey get recordKey; // Public key of identity instance + PublicKey get publicKey; // Secret key of identity instance // Encrypted with appended salt, key is DeriveSharedSecret( // password = SuperIdentity.secret, // salt = publicKey) // Used to recover accounts without generating a new instance @Uint8ListJsonConverter() - Uint8List get encryptedSecretKey => - throw _privateConstructorUsedError; // Signature of SuperInstance recordKey and SuperInstance publicKey + Uint8List + get encryptedSecretKey; // Signature of SuperInstance recordKey and SuperInstance publicKey // by publicKey - FixedEncodedString86 get superSignature => - throw _privateConstructorUsedError; // Signature of recordKey, publicKey, encryptedSecretKey, and superSignature + Signature + get superSignature; // Signature of recordKey, publicKey, encryptedSecretKey, and superSignature // by SuperIdentity publicKey - FixedEncodedString86 get signature => throw _privateConstructorUsedError; - - /// Serializes this IdentityInstance to a JSON map. - Map toJson() => throw _privateConstructorUsedError; + Signature get signature; /// Create a copy of IdentityInstance /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) - $IdentityInstanceCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $IdentityInstanceCopyWith<$Res> { - factory $IdentityInstanceCopyWith( - IdentityInstance value, $Res Function(IdentityInstance) then) = - _$IdentityInstanceCopyWithImpl<$Res, IdentityInstance>; - @useResult - $Res call( - {Typed recordKey, - FixedEncodedString43 publicKey, - @Uint8ListJsonConverter() Uint8List encryptedSecretKey, - FixedEncodedString86 superSignature, - FixedEncodedString86 signature}); -} - -/// @nodoc -class _$IdentityInstanceCopyWithImpl<$Res, $Val extends IdentityInstance> - implements $IdentityInstanceCopyWith<$Res> { - _$IdentityInstanceCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of IdentityInstance - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') + $IdentityInstanceCopyWith get copyWith => + _$IdentityInstanceCopyWithImpl( + this as IdentityInstance, _$identity); + + /// Serializes this IdentityInstance to a JSON map. + Map toJson(); + @override - $Res call({ - Object? recordKey = null, - Object? publicKey = null, - Object? encryptedSecretKey = null, - Object? superSignature = null, - Object? signature = null, - }) { - return _then(_value.copyWith( - recordKey: null == recordKey - ? _value.recordKey - : recordKey // ignore: cast_nullable_to_non_nullable - as Typed, - publicKey: null == publicKey - ? _value.publicKey - : publicKey // ignore: cast_nullable_to_non_nullable - as FixedEncodedString43, - encryptedSecretKey: null == encryptedSecretKey - ? _value.encryptedSecretKey - : encryptedSecretKey // ignore: cast_nullable_to_non_nullable - as Uint8List, - superSignature: null == superSignature - ? _value.superSignature - : superSignature // ignore: cast_nullable_to_non_nullable - as FixedEncodedString86, - signature: null == signature - ? _value.signature - : signature // ignore: cast_nullable_to_non_nullable - as FixedEncodedString86, - ) as $Val); + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is IdentityInstance && + (identical(other.recordKey, recordKey) || + other.recordKey == recordKey) && + (identical(other.publicKey, publicKey) || + other.publicKey == publicKey) && + const DeepCollectionEquality() + .equals(other.encryptedSecretKey, encryptedSecretKey) && + (identical(other.superSignature, superSignature) || + other.superSignature == superSignature) && + (identical(other.signature, signature) || + other.signature == signature)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + recordKey, + publicKey, + const DeepCollectionEquality().hash(encryptedSecretKey), + superSignature, + signature); + + @override + String toString() { + return 'IdentityInstance(recordKey: $recordKey, publicKey: $publicKey, encryptedSecretKey: $encryptedSecretKey, superSignature: $superSignature, signature: $signature)'; } } /// @nodoc -abstract class _$$IdentityInstanceImplCopyWith<$Res> - implements $IdentityInstanceCopyWith<$Res> { - factory _$$IdentityInstanceImplCopyWith(_$IdentityInstanceImpl value, - $Res Function(_$IdentityInstanceImpl) then) = - __$$IdentityInstanceImplCopyWithImpl<$Res>; - @override +abstract mixin class $IdentityInstanceCopyWith<$Res> { + factory $IdentityInstanceCopyWith( + IdentityInstance value, $Res Function(IdentityInstance) _then) = + _$IdentityInstanceCopyWithImpl; @useResult $Res call( {Typed recordKey, @@ -125,12 +90,12 @@ abstract class _$$IdentityInstanceImplCopyWith<$Res> } /// @nodoc -class __$$IdentityInstanceImplCopyWithImpl<$Res> - extends _$IdentityInstanceCopyWithImpl<$Res, _$IdentityInstanceImpl> - implements _$$IdentityInstanceImplCopyWith<$Res> { - __$$IdentityInstanceImplCopyWithImpl(_$IdentityInstanceImpl _value, - $Res Function(_$IdentityInstanceImpl) _then) - : super(_value, _then); +class _$IdentityInstanceCopyWithImpl<$Res> + implements $IdentityInstanceCopyWith<$Res> { + _$IdentityInstanceCopyWithImpl(this._self, this._then); + + final IdentityInstance _self; + final $Res Function(IdentityInstance) _then; /// Create a copy of IdentityInstance /// with the given fields replaced by the non-null parameter values. @@ -143,25 +108,25 @@ class __$$IdentityInstanceImplCopyWithImpl<$Res> Object? superSignature = null, Object? signature = null, }) { - return _then(_$IdentityInstanceImpl( + return _then(_self.copyWith( recordKey: null == recordKey - ? _value.recordKey + ? _self.recordKey! : recordKey // ignore: cast_nullable_to_non_nullable as Typed, publicKey: null == publicKey - ? _value.publicKey + ? _self.publicKey! : publicKey // ignore: cast_nullable_to_non_nullable as FixedEncodedString43, encryptedSecretKey: null == encryptedSecretKey - ? _value.encryptedSecretKey + ? _self.encryptedSecretKey : encryptedSecretKey // ignore: cast_nullable_to_non_nullable as Uint8List, superSignature: null == superSignature - ? _value.superSignature + ? _self.superSignature! : superSignature // ignore: cast_nullable_to_non_nullable as FixedEncodedString86, signature: null == signature - ? _value.signature + ? _self.signature! : signature // ignore: cast_nullable_to_non_nullable as FixedEncodedString86, )); @@ -170,17 +135,16 @@ class __$$IdentityInstanceImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() -class _$IdentityInstanceImpl extends _IdentityInstance { - const _$IdentityInstanceImpl( +class _IdentityInstance extends IdentityInstance { + const _IdentityInstance( {required this.recordKey, required this.publicKey, @Uint8ListJsonConverter() required this.encryptedSecretKey, required this.superSignature, required this.signature}) : super._(); - - factory _$IdentityInstanceImpl.fromJson(Map json) => - _$$IdentityInstanceImplFromJson(json); + factory _IdentityInstance.fromJson(Map json) => + _$IdentityInstanceFromJson(json); // Private DHT record storing identity account mapping @override @@ -205,16 +169,26 @@ class _$IdentityInstanceImpl extends _IdentityInstance { @override final FixedEncodedString86 signature; + /// Create a copy of IdentityInstance + /// with the given fields replaced by the non-null parameter values. @override - String toString() { - return 'IdentityInstance(recordKey: $recordKey, publicKey: $publicKey, encryptedSecretKey: $encryptedSecretKey, superSignature: $superSignature, signature: $signature)'; + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + _$IdentityInstanceCopyWith<_IdentityInstance> get copyWith => + __$IdentityInstanceCopyWithImpl<_IdentityInstance>(this, _$identity); + + @override + Map toJson() { + return _$IdentityInstanceToJson( + this, + ); } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$IdentityInstanceImpl && + other is _IdentityInstance && (identical(other.recordKey, recordKey) || other.recordKey == recordKey) && (identical(other.publicKey, publicKey) || @@ -237,60 +211,70 @@ class _$IdentityInstanceImpl extends _IdentityInstance { superSignature, signature); - /// Create a copy of IdentityInstance - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) @override - @pragma('vm:prefer-inline') - _$$IdentityInstanceImplCopyWith<_$IdentityInstanceImpl> get copyWith => - __$$IdentityInstanceImplCopyWithImpl<_$IdentityInstanceImpl>( - this, _$identity); - - @override - Map toJson() { - return _$$IdentityInstanceImplToJson( - this, - ); + String toString() { + return 'IdentityInstance(recordKey: $recordKey, publicKey: $publicKey, encryptedSecretKey: $encryptedSecretKey, superSignature: $superSignature, signature: $signature)'; } } -abstract class _IdentityInstance extends IdentityInstance { - const factory _IdentityInstance( - {required final Typed recordKey, - required final FixedEncodedString43 publicKey, - @Uint8ListJsonConverter() required final Uint8List encryptedSecretKey, - required final FixedEncodedString86 superSignature, - required final FixedEncodedString86 signature}) = _$IdentityInstanceImpl; - const _IdentityInstance._() : super._(); +/// @nodoc +abstract mixin class _$IdentityInstanceCopyWith<$Res> + implements $IdentityInstanceCopyWith<$Res> { + factory _$IdentityInstanceCopyWith( + _IdentityInstance value, $Res Function(_IdentityInstance) _then) = + __$IdentityInstanceCopyWithImpl; + @override + @useResult + $Res call( + {Typed recordKey, + FixedEncodedString43 publicKey, + @Uint8ListJsonConverter() Uint8List encryptedSecretKey, + FixedEncodedString86 superSignature, + FixedEncodedString86 signature}); +} - factory _IdentityInstance.fromJson(Map json) = - _$IdentityInstanceImpl.fromJson; +/// @nodoc +class __$IdentityInstanceCopyWithImpl<$Res> + implements _$IdentityInstanceCopyWith<$Res> { + __$IdentityInstanceCopyWithImpl(this._self, this._then); -// Private DHT record storing identity account mapping - @override - Typed get recordKey; // Public key of identity instance - @override - FixedEncodedString43 get publicKey; // Secret key of identity instance -// Encrypted with appended salt, key is DeriveSharedSecret( -// password = SuperIdentity.secret, -// salt = publicKey) -// Used to recover accounts without generating a new instance - @override - @Uint8ListJsonConverter() - Uint8List - get encryptedSecretKey; // Signature of SuperInstance recordKey and SuperInstance publicKey -// by publicKey - @override - FixedEncodedString86 - get superSignature; // Signature of recordKey, publicKey, encryptedSecretKey, and superSignature -// by SuperIdentity publicKey - @override - FixedEncodedString86 get signature; + final _IdentityInstance _self; + final $Res Function(_IdentityInstance) _then; /// Create a copy of IdentityInstance /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$IdentityInstanceImplCopyWith<_$IdentityInstanceImpl> get copyWith => - throw _privateConstructorUsedError; + @pragma('vm:prefer-inline') + $Res call({ + Object? recordKey = null, + Object? publicKey = null, + Object? encryptedSecretKey = null, + Object? superSignature = null, + Object? signature = null, + }) { + return _then(_IdentityInstance( + recordKey: null == recordKey + ? _self.recordKey + : recordKey // ignore: cast_nullable_to_non_nullable + as Typed, + publicKey: null == publicKey + ? _self.publicKey + : publicKey // ignore: cast_nullable_to_non_nullable + as FixedEncodedString43, + encryptedSecretKey: null == encryptedSecretKey + ? _self.encryptedSecretKey + : encryptedSecretKey // ignore: cast_nullable_to_non_nullable + as Uint8List, + superSignature: null == superSignature + ? _self.superSignature + : superSignature // ignore: cast_nullable_to_non_nullable + as FixedEncodedString86, + signature: null == signature + ? _self.signature + : signature // ignore: cast_nullable_to_non_nullable + as FixedEncodedString86, + )); + } } + +// dart format on diff --git a/packages/veilid_support/lib/identity_support/identity_instance.g.dart b/packages/veilid_support/lib/identity_support/identity_instance.g.dart index cb228e6..eddbcf6 100644 --- a/packages/veilid_support/lib/identity_support/identity_instance.g.dart +++ b/packages/veilid_support/lib/identity_support/identity_instance.g.dart @@ -6,9 +6,8 @@ part of 'identity_instance.dart'; // JsonSerializableGenerator // ************************************************************************** -_$IdentityInstanceImpl _$$IdentityInstanceImplFromJson( - Map json) => - _$IdentityInstanceImpl( +_IdentityInstance _$IdentityInstanceFromJson(Map json) => + _IdentityInstance( recordKey: Typed.fromJson(json['record_key']), publicKey: FixedEncodedString43.fromJson(json['public_key']), encryptedSecretKey: @@ -17,8 +16,7 @@ _$IdentityInstanceImpl _$$IdentityInstanceImplFromJson( signature: FixedEncodedString86.fromJson(json['signature']), ); -Map _$$IdentityInstanceImplToJson( - _$IdentityInstanceImpl instance) => +Map _$IdentityInstanceToJson(_IdentityInstance instance) => { 'record_key': instance.recordKey.toJson(), 'public_key': instance.publicKey.toJson(), diff --git a/packages/veilid_support/lib/identity_support/super_identity.dart b/packages/veilid_support/lib/identity_support/super_identity.dart index e4ec8fc..5ee8c43 100644 --- a/packages/veilid_support/lib/identity_support/super_identity.dart +++ b/packages/veilid_support/lib/identity_support/super_identity.dart @@ -22,7 +22,7 @@ part 'super_identity.g.dart'; /// DHT Owner Secret: SuperIdentity Secret Key (kept offline) /// Encryption: None @freezed -class SuperIdentity with _$SuperIdentity { +sealed class SuperIdentity with _$SuperIdentity { const factory SuperIdentity({ /// Public DHT record storing this structure for account recovery /// changing this can migrate/forward the SuperIdentity to a new DHT record diff --git a/packages/veilid_support/lib/identity_support/super_identity.freezed.dart b/packages/veilid_support/lib/identity_support/super_identity.freezed.dart index 9c5c6a7..b142373 100644 --- a/packages/veilid_support/lib/identity_support/super_identity.freezed.dart +++ b/packages/veilid_support/lib/identity_support/super_identity.freezed.dart @@ -1,3 +1,4 @@ +// dart format width=80 // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint @@ -9,65 +10,93 @@ part of 'super_identity.dart'; // FreezedGenerator // ************************************************************************** +// dart format off 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'); - -SuperIdentity _$SuperIdentityFromJson(Map json) { - return _SuperIdentity.fromJson(json); -} - /// @nodoc mixin _$SuperIdentity { /// Public DHT record storing this structure for account recovery /// changing this can migrate/forward the SuperIdentity to a new DHT record /// Instances should not hash this recordKey, rather the actual record /// key used to store the superIdentity, as this may change. - Typed get recordKey => - throw _privateConstructorUsedError; + TypedKey get recordKey; /// Public key of the SuperIdentity used to sign identity keys for recovery /// This must match the owner of the superRecord DHT record and can not be /// changed without changing the record - FixedEncodedString43 get publicKey => throw _privateConstructorUsedError; + PublicKey get publicKey; /// Current identity instance /// The most recently generated identity instance for this SuperIdentity - IdentityInstance get currentInstance => throw _privateConstructorUsedError; + IdentityInstance get currentInstance; /// Deprecated identity instances /// These may be compromised and should not be considered valid for /// new signatures, but may be used to validate old signatures - List get deprecatedInstances => - throw _privateConstructorUsedError; + List get deprecatedInstances; /// Deprecated superRecords /// These may be compromised and should not be considered valid for /// new signatures, but may be used to validate old signatures - List> get deprecatedSuperRecordKeys => - throw _privateConstructorUsedError; + List get deprecatedSuperRecordKeys; /// Signature of recordKey, currentInstance signature, /// signatures of deprecatedInstances, and deprecatedSuperRecordKeys /// by publicKey - FixedEncodedString86 get signature => throw _privateConstructorUsedError; - - /// Serializes this SuperIdentity to a JSON map. - Map toJson() => throw _privateConstructorUsedError; + Signature get signature; /// Create a copy of SuperIdentity /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') $SuperIdentityCopyWith get copyWith => - throw _privateConstructorUsedError; + _$SuperIdentityCopyWithImpl( + this as SuperIdentity, _$identity); + + /// Serializes this SuperIdentity to a JSON map. + Map toJson(); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is SuperIdentity && + (identical(other.recordKey, recordKey) || + other.recordKey == recordKey) && + (identical(other.publicKey, publicKey) || + other.publicKey == publicKey) && + (identical(other.currentInstance, currentInstance) || + other.currentInstance == currentInstance) && + const DeepCollectionEquality() + .equals(other.deprecatedInstances, deprecatedInstances) && + const DeepCollectionEquality().equals( + other.deprecatedSuperRecordKeys, deprecatedSuperRecordKeys) && + (identical(other.signature, signature) || + other.signature == signature)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + recordKey, + publicKey, + currentInstance, + const DeepCollectionEquality().hash(deprecatedInstances), + const DeepCollectionEquality().hash(deprecatedSuperRecordKeys), + signature); + + @override + String toString() { + return 'SuperIdentity(recordKey: $recordKey, publicKey: $publicKey, currentInstance: $currentInstance, deprecatedInstances: $deprecatedInstances, deprecatedSuperRecordKeys: $deprecatedSuperRecordKeys, signature: $signature)'; + } } /// @nodoc -abstract class $SuperIdentityCopyWith<$Res> { +abstract mixin class $SuperIdentityCopyWith<$Res> { factory $SuperIdentityCopyWith( - SuperIdentity value, $Res Function(SuperIdentity) then) = - _$SuperIdentityCopyWithImpl<$Res, SuperIdentity>; + SuperIdentity value, $Res Function(SuperIdentity) _then) = + _$SuperIdentityCopyWithImpl; @useResult $Res call( {Typed recordKey, @@ -81,14 +110,12 @@ abstract class $SuperIdentityCopyWith<$Res> { } /// @nodoc -class _$SuperIdentityCopyWithImpl<$Res, $Val extends SuperIdentity> +class _$SuperIdentityCopyWithImpl<$Res> implements $SuperIdentityCopyWith<$Res> { - _$SuperIdentityCopyWithImpl(this._value, this._then); + _$SuperIdentityCopyWithImpl(this._self, this._then); - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; + final SuperIdentity _self; + final $Res Function(SuperIdentity) _then; /// Create a copy of SuperIdentity /// with the given fields replaced by the non-null parameter values. @@ -102,32 +129,32 @@ class _$SuperIdentityCopyWithImpl<$Res, $Val extends SuperIdentity> Object? deprecatedSuperRecordKeys = null, Object? signature = null, }) { - return _then(_value.copyWith( + return _then(_self.copyWith( recordKey: null == recordKey - ? _value.recordKey + ? _self.recordKey! : recordKey // ignore: cast_nullable_to_non_nullable as Typed, publicKey: null == publicKey - ? _value.publicKey + ? _self.publicKey! : publicKey // ignore: cast_nullable_to_non_nullable as FixedEncodedString43, currentInstance: null == currentInstance - ? _value.currentInstance + ? _self.currentInstance : currentInstance // ignore: cast_nullable_to_non_nullable as IdentityInstance, deprecatedInstances: null == deprecatedInstances - ? _value.deprecatedInstances + ? _self.deprecatedInstances : deprecatedInstances // ignore: cast_nullable_to_non_nullable as List, deprecatedSuperRecordKeys: null == deprecatedSuperRecordKeys - ? _value.deprecatedSuperRecordKeys + ? _self.deprecatedSuperRecordKeys! : deprecatedSuperRecordKeys // ignore: cast_nullable_to_non_nullable as List>, signature: null == signature - ? _value.signature + ? _self.signature! : signature // ignore: cast_nullable_to_non_nullable as FixedEncodedString86, - ) as $Val); + )); } /// Create a copy of SuperIdentity @@ -135,85 +162,16 @@ class _$SuperIdentityCopyWithImpl<$Res, $Val extends SuperIdentity> @override @pragma('vm:prefer-inline') $IdentityInstanceCopyWith<$Res> get currentInstance { - return $IdentityInstanceCopyWith<$Res>(_value.currentInstance, (value) { - return _then(_value.copyWith(currentInstance: value) as $Val); + return $IdentityInstanceCopyWith<$Res>(_self.currentInstance, (value) { + return _then(_self.copyWith(currentInstance: value)); }); } } -/// @nodoc -abstract class _$$SuperIdentityImplCopyWith<$Res> - implements $SuperIdentityCopyWith<$Res> { - factory _$$SuperIdentityImplCopyWith( - _$SuperIdentityImpl value, $Res Function(_$SuperIdentityImpl) then) = - __$$SuperIdentityImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {Typed recordKey, - FixedEncodedString43 publicKey, - IdentityInstance currentInstance, - List deprecatedInstances, - List> deprecatedSuperRecordKeys, - FixedEncodedString86 signature}); - - @override - $IdentityInstanceCopyWith<$Res> get currentInstance; -} - -/// @nodoc -class __$$SuperIdentityImplCopyWithImpl<$Res> - extends _$SuperIdentityCopyWithImpl<$Res, _$SuperIdentityImpl> - implements _$$SuperIdentityImplCopyWith<$Res> { - __$$SuperIdentityImplCopyWithImpl( - _$SuperIdentityImpl _value, $Res Function(_$SuperIdentityImpl) _then) - : super(_value, _then); - - /// Create a copy of SuperIdentity - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? recordKey = null, - Object? publicKey = null, - Object? currentInstance = null, - Object? deprecatedInstances = null, - Object? deprecatedSuperRecordKeys = null, - Object? signature = null, - }) { - return _then(_$SuperIdentityImpl( - recordKey: null == recordKey - ? _value.recordKey - : recordKey // ignore: cast_nullable_to_non_nullable - as Typed, - publicKey: null == publicKey - ? _value.publicKey - : publicKey // ignore: cast_nullable_to_non_nullable - as FixedEncodedString43, - currentInstance: null == currentInstance - ? _value.currentInstance - : currentInstance // ignore: cast_nullable_to_non_nullable - as IdentityInstance, - deprecatedInstances: null == deprecatedInstances - ? _value._deprecatedInstances - : deprecatedInstances // ignore: cast_nullable_to_non_nullable - as List, - deprecatedSuperRecordKeys: null == deprecatedSuperRecordKeys - ? _value._deprecatedSuperRecordKeys - : deprecatedSuperRecordKeys // ignore: cast_nullable_to_non_nullable - as List>, - signature: null == signature - ? _value.signature - : signature // ignore: cast_nullable_to_non_nullable - as FixedEncodedString86, - )); - } -} - /// @nodoc @JsonSerializable() -class _$SuperIdentityImpl extends _SuperIdentity { - const _$SuperIdentityImpl( +class _SuperIdentity extends SuperIdentity { + const _SuperIdentity( {required this.recordKey, required this.publicKey, required this.currentInstance, @@ -224,9 +182,8 @@ class _$SuperIdentityImpl extends _SuperIdentity { : _deprecatedInstances = deprecatedInstances, _deprecatedSuperRecordKeys = deprecatedSuperRecordKeys, super._(); - - factory _$SuperIdentityImpl.fromJson(Map json) => - _$$SuperIdentityImplFromJson(json); + factory _SuperIdentity.fromJson(Map json) => + _$SuperIdentityFromJson(json); /// Public DHT record storing this structure for account recovery /// changing this can migrate/forward the SuperIdentity to a new DHT record @@ -284,16 +241,26 @@ class _$SuperIdentityImpl extends _SuperIdentity { @override final FixedEncodedString86 signature; + /// Create a copy of SuperIdentity + /// with the given fields replaced by the non-null parameter values. @override - String toString() { - return 'SuperIdentity(recordKey: $recordKey, publicKey: $publicKey, currentInstance: $currentInstance, deprecatedInstances: $deprecatedInstances, deprecatedSuperRecordKeys: $deprecatedSuperRecordKeys, signature: $signature)'; + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + _$SuperIdentityCopyWith<_SuperIdentity> get copyWith => + __$SuperIdentityCopyWithImpl<_SuperIdentity>(this, _$identity); + + @override + Map toJson() { + return _$SuperIdentityToJson( + this, + ); } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$SuperIdentityImpl && + other is _SuperIdentity && (identical(other.recordKey, recordKey) || other.recordKey == recordKey) && (identical(other.publicKey, publicKey) || @@ -319,76 +286,89 @@ class _$SuperIdentityImpl extends _SuperIdentity { const DeepCollectionEquality().hash(_deprecatedSuperRecordKeys), signature); - /// Create a copy of SuperIdentity - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) @override - @pragma('vm:prefer-inline') - _$$SuperIdentityImplCopyWith<_$SuperIdentityImpl> get copyWith => - __$$SuperIdentityImplCopyWithImpl<_$SuperIdentityImpl>(this, _$identity); - - @override - Map toJson() { - return _$$SuperIdentityImplToJson( - this, - ); + String toString() { + return 'SuperIdentity(recordKey: $recordKey, publicKey: $publicKey, currentInstance: $currentInstance, deprecatedInstances: $deprecatedInstances, deprecatedSuperRecordKeys: $deprecatedSuperRecordKeys, signature: $signature)'; } } -abstract class _SuperIdentity extends SuperIdentity { - const factory _SuperIdentity( - {required final Typed recordKey, - required final FixedEncodedString43 publicKey, - required final IdentityInstance currentInstance, - required final List deprecatedInstances, - required final List> - deprecatedSuperRecordKeys, - required final FixedEncodedString86 signature}) = _$SuperIdentityImpl; - const _SuperIdentity._() : super._(); - - factory _SuperIdentity.fromJson(Map json) = - _$SuperIdentityImpl.fromJson; - - /// Public DHT record storing this structure for account recovery - /// changing this can migrate/forward the SuperIdentity to a new DHT record - /// Instances should not hash this recordKey, rather the actual record - /// key used to store the superIdentity, as this may change. +/// @nodoc +abstract mixin class _$SuperIdentityCopyWith<$Res> + implements $SuperIdentityCopyWith<$Res> { + factory _$SuperIdentityCopyWith( + _SuperIdentity value, $Res Function(_SuperIdentity) _then) = + __$SuperIdentityCopyWithImpl; @override - Typed get recordKey; + @useResult + $Res call( + {Typed recordKey, + FixedEncodedString43 publicKey, + IdentityInstance currentInstance, + List deprecatedInstances, + List> deprecatedSuperRecordKeys, + FixedEncodedString86 signature}); - /// Public key of the SuperIdentity used to sign identity keys for recovery - /// This must match the owner of the superRecord DHT record and can not be - /// changed without changing the record @override - FixedEncodedString43 get publicKey; + $IdentityInstanceCopyWith<$Res> get currentInstance; +} - /// Current identity instance - /// The most recently generated identity instance for this SuperIdentity - @override - IdentityInstance get currentInstance; +/// @nodoc +class __$SuperIdentityCopyWithImpl<$Res> + implements _$SuperIdentityCopyWith<$Res> { + __$SuperIdentityCopyWithImpl(this._self, this._then); - /// Deprecated identity instances - /// These may be compromised and should not be considered valid for - /// new signatures, but may be used to validate old signatures - @override - List get deprecatedInstances; - - /// Deprecated superRecords - /// These may be compromised and should not be considered valid for - /// new signatures, but may be used to validate old signatures - @override - List> get deprecatedSuperRecordKeys; - - /// Signature of recordKey, currentInstance signature, - /// signatures of deprecatedInstances, and deprecatedSuperRecordKeys - /// by publicKey - @override - FixedEncodedString86 get signature; + final _SuperIdentity _self; + final $Res Function(_SuperIdentity) _then; /// Create a copy of SuperIdentity /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$SuperIdentityImplCopyWith<_$SuperIdentityImpl> get copyWith => - throw _privateConstructorUsedError; + @pragma('vm:prefer-inline') + $Res call({ + Object? recordKey = null, + Object? publicKey = null, + Object? currentInstance = null, + Object? deprecatedInstances = null, + Object? deprecatedSuperRecordKeys = null, + Object? signature = null, + }) { + return _then(_SuperIdentity( + recordKey: null == recordKey + ? _self.recordKey + : recordKey // ignore: cast_nullable_to_non_nullable + as Typed, + publicKey: null == publicKey + ? _self.publicKey + : publicKey // ignore: cast_nullable_to_non_nullable + as FixedEncodedString43, + currentInstance: null == currentInstance + ? _self.currentInstance + : currentInstance // ignore: cast_nullable_to_non_nullable + as IdentityInstance, + deprecatedInstances: null == deprecatedInstances + ? _self._deprecatedInstances + : deprecatedInstances // ignore: cast_nullable_to_non_nullable + as List, + deprecatedSuperRecordKeys: null == deprecatedSuperRecordKeys + ? _self._deprecatedSuperRecordKeys + : deprecatedSuperRecordKeys // ignore: cast_nullable_to_non_nullable + as List>, + signature: null == signature + ? _self.signature + : signature // ignore: cast_nullable_to_non_nullable + as FixedEncodedString86, + )); + } + + /// Create a copy of SuperIdentity + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $IdentityInstanceCopyWith<$Res> get currentInstance { + return $IdentityInstanceCopyWith<$Res>(_self.currentInstance, (value) { + return _then(_self.copyWith(currentInstance: value)); + }); + } } + +// dart format on diff --git a/packages/veilid_support/lib/identity_support/super_identity.g.dart b/packages/veilid_support/lib/identity_support/super_identity.g.dart index 4c4f4f3..1b52492 100644 --- a/packages/veilid_support/lib/identity_support/super_identity.g.dart +++ b/packages/veilid_support/lib/identity_support/super_identity.g.dart @@ -6,8 +6,8 @@ part of 'super_identity.dart'; // JsonSerializableGenerator // ************************************************************************** -_$SuperIdentityImpl _$$SuperIdentityImplFromJson(Map json) => - _$SuperIdentityImpl( +_SuperIdentity _$SuperIdentityFromJson(Map json) => + _SuperIdentity( recordKey: Typed.fromJson(json['record_key']), publicKey: FixedEncodedString43.fromJson(json['public_key']), currentInstance: IdentityInstance.fromJson(json['current_instance']), @@ -21,7 +21,7 @@ _$SuperIdentityImpl _$$SuperIdentityImplFromJson(Map json) => signature: FixedEncodedString86.fromJson(json['signature']), ); -Map _$$SuperIdentityImplToJson(_$SuperIdentityImpl instance) => +Map _$SuperIdentityToJson(_SuperIdentity instance) => { 'record_key': instance.recordKey.toJson(), 'public_key': instance.publicKey.toJson(), diff --git a/packages/veilid_support/lib/proto/proto.dart b/packages/veilid_support/lib/proto/proto.dart index a7a70bb..936bbdf 100644 --- a/packages/veilid_support/lib/proto/proto.dart +++ b/packages/veilid_support/lib/proto/proto.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; +import '../src/dynamic_debug.dart'; import '../veilid_support.dart' as veilid; import 'veilid.pb.dart' as proto; @@ -150,3 +151,26 @@ extension ProtoKeyPair on proto.KeyPair { veilid.KeyPair toVeilid() => veilid.KeyPair(key: key.toVeilid(), secret: secret.toVeilid()); } + +void registerVeilidProtoToDebug() { + dynamic toDebug(dynamic protoObj) { + if (protoObj is proto.CryptoKey) { + return protoObj.toVeilid(); + } + if (protoObj is proto.Signature) { + return protoObj.toVeilid(); + } + if (protoObj is proto.Nonce) { + return protoObj.toVeilid(); + } + if (protoObj is proto.TypedKey) { + return protoObj.toVeilid(); + } + if (protoObj is proto.KeyPair) { + return protoObj.toVeilid(); + } + return protoObj; + } + + DynamicDebug.registerToDebug(toDebug); +} diff --git a/packages/veilid_support/lib/src/dynamic_debug.dart b/packages/veilid_support/lib/src/dynamic_debug.dart new file mode 100644 index 0000000..1c38d96 --- /dev/null +++ b/packages/veilid_support/lib/src/dynamic_debug.dart @@ -0,0 +1,130 @@ +import 'package:async_tools/async_tools.dart'; +import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; +import 'package:convert/convert.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; + +import 'online_element_state.dart'; + +typedef ToDebugFunction = dynamic Function(dynamic protoObj); + +// This should be implemented to add toDebug capability +// ignore: one_member_abstracts +abstract class ToDebugMap { + Map toDebugMap(); +} + +// We explicitly want this class to avoid having a global function 'toDebug' +// ignore: avoid_classes_with_only_static_members +class DynamicDebug { + /// Add a 'toDebug' handler to the chain + static void registerToDebug(ToDebugFunction toDebugFunction) { + final _oldToDebug = _toDebug; + _toDebug = (obj) => _oldToDebug(toDebugFunction(obj)); + } + + /// Convert a type to a debug version of the same type that + /// has a better `toString` representation and possibly other extra debug + /// information + static dynamic toDebug(dynamic obj) { + try { + return _toDebug(obj); + // In this case we watch to catch everything + // because toDebug need to never fail + // ignore: avoid_catches_without_on_clauses + } catch (e) { + // Ensure this gets printed, but continue + // ignore: avoid_print + print('Exception in toDebug: $e'); + return obj.toString(); + } + } + + ////////////////////////////////////////////////////////////// + static dynamic _baseToDebug(dynamic obj) { + if (obj is AsyncValue) { + if (obj.isLoading) { + return {r'$runtimeType': obj.runtimeType, 'loading': null}; + } + if (obj.isError) { + return { + r'$runtimeType': obj.runtimeType, + 'error': toDebug(obj.asError!.error), + 'stackTrace': toDebug(obj.asError!.stackTrace), + }; + } + if (obj.isData) { + return { + r'$runtimeType': obj.runtimeType, + 'data': toDebug(obj.asData!.value), + }; + } + return obj.toString(); + } + if (obj is IMap) { + // Handled by Map + return _baseToDebug(obj.unlockView); + } + if (obj is IMapOfSets) { + // Handled by Map + return _baseToDebug(obj.unlock); + } + if (obj is ISet) { + // Handled by Iterable + return _baseToDebug(obj.unlockView); + } + if (obj is IList) { + return _baseToDebug(obj.unlockView); + } + if (obj is BlocBusyState) { + return { + r'$runtimeType': obj.runtimeType, + 'busy': obj.busy, + 'state': toDebug(obj.state), + }; + } + if (obj is OnlineElementState) { + return { + r'$runtimeType': obj.runtimeType, + 'isOffline': obj.isOffline, + 'value': toDebug(obj.value), + }; + } + if (obj is List) { + try { + // Do bytes as a hex string for brevity and clarity + return 'List: ${hex.encode(obj)}'; + // One has to be able to catch this + // ignore: avoid_catching_errors + } on RangeError { + // Otherwise directly convert as list of integers + return obj.toString(); + } + } + if (obj is Map) { + return obj.map((k, v) => MapEntry(toDebug(k), toDebug(v))); + } + if (obj is Iterable) { + return obj.map(toDebug).toList(); + } + if (obj is String || obj is bool || obj is num || obj == null) { + return obj; + } + if (obj is ToDebugMap) { + // Handled by Map + return _baseToDebug(obj.toDebugMap()); + } + + try { + // Let's try convering to a json object + // ignore: avoid_dynamic_calls + return obj.toJson(); + + // No matter how this fails, we shouldn't throw + // ignore: avoid_catches_without_on_clauses + } catch (_) {} + + return obj.toString(); + } + + static ToDebugFunction _toDebug = _baseToDebug; +} diff --git a/packages/veilid_support/lib/veilid_support.dart b/packages/veilid_support/lib/veilid_support.dart index f48376f..2f4da90 100644 --- a/packages/veilid_support/lib/veilid_support.dart +++ b/packages/veilid_support/lib/veilid_support.dart @@ -1,13 +1,14 @@ /// Dart Veilid Support Library /// Common functionality for interfacing with Veilid -library veilid_support; +library; export 'package:veilid/veilid.dart'; export 'dht_support/dht_support.dart'; export 'identity_support/identity_support.dart'; export 'src/config.dart'; +export 'src/dynamic_debug.dart'; export 'src/json_tools.dart'; export 'src/memory_tools.dart'; export 'src/online_element_state.dart'; diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index e3dfcdd..0c8ca3d 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -178,7 +178,7 @@ packages: source: hosted version: "1.19.1" convert: - dependency: transitive + dependency: "direct main" description: name: convert sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index 5aff89e..548c40e 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: bloc_advanced_tools: ^0.1.10 charcode: ^1.4.0 collection: ^1.19.1 + convert: ^3.1.2 equatable: ^2.0.7 fast_immutable_collections: ^11.0.3 freezed_annotation: ^3.0.0 From 6830b54ce8e1eecb097b56075deea4834c9d11ba Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 23 Mar 2025 10:41:19 -0400 Subject: [PATCH 220/270] move keyboard shortcuts into own file --- lib/app.dart | 68 +------------------- lib/layout/home/drawer_menu/drawer_menu.dart | 4 +- lib/router/cubits/router_cubit.dart | 13 ++++ lib/router/views/router_shell.dart | 4 +- 4 files changed, 19 insertions(+), 70 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index f8241da..802b0d7 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,15 +1,12 @@ import 'package:animated_theme_switcher/animated_theme_switcher.dart'; -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/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:provider/provider.dart'; -import 'package:veilid_support/veilid_support.dart'; import 'account_manager/account_manager.dart'; import 'init.dart'; @@ -19,17 +16,8 @@ import 'router/router.dart'; import 'settings/settings.dart'; import 'theme/theme.dart'; import 'tick.dart'; -import 'tools/loggy.dart'; import 'veilid_processor/veilid_processor.dart'; -class ReloadThemeIntent extends Intent { - const ReloadThemeIntent(); -} - -class AttachDetachIntent extends Intent { - const AttachDetachIntent(); -} - class ScrollBehaviorModified extends ScrollBehavior { const ScrollBehaviorModified(); @override @@ -47,59 +35,6 @@ class VeilidChatApp extends StatelessWidget { final ThemeData initialThemeData; - void reloadTheme(BuildContext context) { - singleFuture(this, () async { - log.info('Reloading theme'); - - await VeilidChatGlobalInit.loadAssetManifest(); - - final theme = - PreferencesRepository.instance.value.themePreference.themeData(); - if (context.mounted) { - ThemeSwitcher.of(context).changeTheme(theme: theme); - - // Hack to reload translations - final localizationDelegate = LocalizedApp.of(context).delegate; - await LocalizationDelegate.create( - fallbackLocale: localizationDelegate.fallbackLocale.toString(), - supportedLocales: localizationDelegate.supportedLocales - .map((x) => x.toString()) - .toList()); - } - }); - } - - void _attachDetach(BuildContext context) { - singleFuture(this, () async { - if (ProcessorRepository.instance.processorConnectionState.isAttached) { - log.info('Detaching'); - await Veilid.instance.detach(); - } else if (ProcessorRepository - .instance.processorConnectionState.isDetached) { - log.info('Attaching'); - await Veilid.instance.attach(); - } - }); - } - - Widget _buildShortcuts({required Widget Function(BuildContext) builder}) => - ThemeSwitcher( - builder: (context) => Shortcuts( - shortcuts: { - LogicalKeySet( - LogicalKeyboardKey.alt, LogicalKeyboardKey.keyR): - const ReloadThemeIntent(), - LogicalKeySet( - LogicalKeyboardKey.alt, LogicalKeyboardKey.keyD): - const AttachDetachIntent(), - }, - child: Actions(actions: >{ - ReloadThemeIntent: CallbackAction( - onInvoke: (intent) => reloadTheme(context)), - AttachDetachIntent: CallbackAction( - onInvoke: (intent) => _attachDetach(context)), - }, child: Focus(autofocus: true, child: builder(context))))); - Widget appBuilder( BuildContext context, LocalizationDelegate localizationDelegate) => ThemeProvider( @@ -138,8 +73,7 @@ class VeilidChatApp extends StatelessWidget { accountRepository: AccountRepository.instance, locator: context.read)), ], - child: - BackgroundTicker(child: _buildShortcuts(builder: (context) { + child: BackgroundTicker(child: Builder(builder: (context) { final scale = theme.extension()!; final scaleConfig = theme.extension()!; diff --git a/lib/layout/home/drawer_menu/drawer_menu.dart b/lib/layout/home/drawer_menu/drawer_menu.dart index 3cf1b79..0863b1f 100644 --- a/lib/layout/home/drawer_menu/drawer_menu.dart +++ b/lib/layout/home/drawer_menu/drawer_menu.dart @@ -9,7 +9,7 @@ import 'package:go_router/go_router.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../../account_manager/account_manager.dart'; -import '../../../app.dart'; +import '../../../keyboard_shortcuts.dart'; import '../../../theme/theme.dart'; import '../../../tools/tools.dart'; import '../../../veilid_processor/veilid_processor.dart'; @@ -366,7 +366,7 @@ class _DrawerMenuState extends State { GestureDetector( onLongPress: () async { context - .findAncestorWidgetOfExactType()! + .findAncestorWidgetOfExactType()! .reloadTheme(context); }, child: SvgPicture.asset( diff --git a/lib/router/cubits/router_cubit.dart b/lib/router/cubits/router_cubit.dart index 1492c51..19af5e0 100644 --- a/lib/router/cubits/router_cubit.dart +++ b/lib/router/cubits/router_cubit.dart @@ -16,6 +16,8 @@ import '../../tools/tools.dart'; import '../../veilid_processor/views/developer.dart'; import '../views/router_shell.dart'; +export 'package:go_router/go_router.dart'; + part 'router_cubit.freezed.dart'; part 'router_cubit.g.dart'; @@ -164,3 +166,14 @@ class RouterCubit extends Cubit { _accountRepositorySubscription; GoRouter? _router; } + +extension GoRouterExtension on GoRouter { + String location() { + final lastMatch = routerDelegate.currentConfiguration.last; + final matchList = lastMatch is ImperativeRouteMatch + ? lastMatch.matches + : routerDelegate.currentConfiguration; + final location = matchList.uri.toString(); + return location; + } +} diff --git a/lib/router/views/router_shell.dart b/lib/router/views/router_shell.dart index f2f035b..8a7130f 100644 --- a/lib/router/views/router_shell.dart +++ b/lib/router/views/router_shell.dart @@ -1,12 +1,14 @@ import 'package:flutter/widgets.dart'; +import '../../keyboard_shortcuts.dart'; import '../../notifications/notifications.dart'; class RouterShell extends StatelessWidget { const RouterShell({required Widget child, super.key}) : _child = child; @override - Widget build(BuildContext context) => NotificationsWidget(child: _child); + Widget build(BuildContext context) => + NotificationsWidget(child: KeyboardShortcuts(child: _child)); final Widget _child; } From 89c6bd5e43934942cfdb3a64a2fc33517524cee3 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 29 Mar 2025 18:06:08 -0400 Subject: [PATCH 221/270] changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7e369f..843cc65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## v0.4.6 ## +- Updated veilid-core to v0.4.4 + - See Veilid changelog for specifics +- UI improvements: Theme fixes, wallpaper option added +- Responsiveness improved +- Contacts workflow more consistent +- Safe-area fixes +- Make layout more mobile-friendly +- Improved contact invitation menus +- Deadlock fixes in veilid_support +- _pollWatch was degenerate and only watched first subkey + ## v0.4.5 ## - Updated veilid-core to v0.4.1 - See Veilid changelog for specifics From 57ac0e5c4d373ddd40b938bb99e9f003fc59ca7a Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 30 Mar 2025 11:07:36 -0400 Subject: [PATCH 222/270] oops --- lib/keyboard_shortcuts.dart | 98 +++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 lib/keyboard_shortcuts.dart diff --git a/lib/keyboard_shortcuts.dart b/lib/keyboard_shortcuts.dart new file mode 100644 index 0000000..0c531a9 --- /dev/null +++ b/lib/keyboard_shortcuts.dart @@ -0,0 +1,98 @@ +import 'package:animated_theme_switcher/animated_theme_switcher.dart'; +import 'package:async_tools/async_tools.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_translate/flutter_translate.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import 'init.dart'; +import 'router/router.dart'; +import 'settings/settings.dart'; +import 'theme/theme.dart'; +import 'tools/tools.dart'; +import 'veilid_processor/veilid_processor.dart'; + +class ReloadThemeIntent extends Intent { + const ReloadThemeIntent(); +} + +class AttachDetachIntent extends Intent { + const AttachDetachIntent(); +} + +class DeveloperPageIntent extends Intent { + const DeveloperPageIntent(); +} + +class KeyboardShortcuts extends StatelessWidget { + const KeyboardShortcuts({required this.child, super.key}); + + void reloadTheme(BuildContext context) { + singleFuture(this, () async { + log.info('Reloading theme'); + + await VeilidChatGlobalInit.loadAssetManifest(); + + final theme = + PreferencesRepository.instance.value.themePreference.themeData(); + if (context.mounted) { + ThemeSwitcher.of(context).changeTheme(theme: theme); + + // Hack to reload translations + final localizationDelegate = LocalizedApp.of(context).delegate; + await LocalizationDelegate.create( + fallbackLocale: localizationDelegate.fallbackLocale.toString(), + supportedLocales: localizationDelegate.supportedLocales + .map((x) => x.toString()) + .toList()); + } + }); + } + + void _attachDetach(BuildContext context) { + singleFuture(this, () async { + if (ProcessorRepository.instance.processorConnectionState.isAttached) { + log.info('Detaching'); + await Veilid.instance.detach(); + } else if (ProcessorRepository + .instance.processorConnectionState.isDetached) { + log.info('Attaching'); + await Veilid.instance.attach(); + } + }); + } + + void _developerPage(BuildContext context) { + singleFuture(this, () async { + final path = GoRouter.of(context).location(); + if (path != '/developer') { + await GoRouterHelper(context).push('/developer'); + } + }); + } + + @override + Widget build(BuildContext context) => ThemeSwitcher( + builder: (context) => Shortcuts( + shortcuts: { + LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.keyR): + const ReloadThemeIntent(), + LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.keyD): + const AttachDetachIntent(), + LogicalKeySet( + LogicalKeyboardKey.alt, LogicalKeyboardKey.backquote): + const DeveloperPageIntent(), + }, + child: Actions(actions: >{ + ReloadThemeIntent: CallbackAction( + onInvoke: (intent) => reloadTheme(context)), + AttachDetachIntent: CallbackAction( + onInvoke: (intent) => _attachDetach(context)), + DeveloperPageIntent: CallbackAction( + onInvoke: (intent) => _developerPage(context)), + }, child: Focus(autofocus: true, child: child)))); + + ///////////////////////////////////////////////////////// + + final Widget child; +} From 741d1878175e1eeb358cc5455cb1b5912d1d05db Mon Sep 17 00:00:00 2001 From: TC Johnson Date: Sun, 30 Mar 2025 14:48:58 -0500 Subject: [PATCH 223/270] =?UTF-8?q?Version=20update:=20v0.4.5=20=E2=86=92?= =?UTF-8?q?=20v0.4.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- ios/Podfile.lock | 30 +++++++++---------- ios/Runner.xcodeproj/project.pbxproj | 7 +++++ .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- pubspec.lock | 2 +- pubspec.yaml | 2 +- 6 files changed, 26 insertions(+), 19 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index f122962..874bed6 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.4.5+0 +current_version = 0.4.6+0 commit = False tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)\+(?P\d+) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 2528d2d..9f7e7e6 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -143,10 +143,10 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/veilid/ios" SPEC CHECKSUMS: - camera_avfoundation: 04b44aeb14070126c6529e5ab82cc7c9fca107cf - file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6 + camera_avfoundation: dd002b0330f4981e1bbcb46ae9b62829237459a4 + file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf + flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318 GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 @@ -156,20 +156,20 @@ SPEC CHECKSUMS: MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2 MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e - mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036 + mobile_scanner: fd0054c52ede661e80bf5a4dea477a2467356bee nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 - pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 - printing: 54ff03f28fe9ba3aa93358afb80a8595a071dd07 + package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 + pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + printing: 233e1b73bd1f4a05615548e9b5a324c98588640b PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 - share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - system_info_plus: 555ce7047fbbf29154726db942ae785c29211740 - url_launcher_ios: 694010445543906933d732453a59da0a173ae33d - veilid: 3ce560a4f2b568a77a9fd5e23090f2fa97581019 + share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + system_info_plus: 5393c8da281d899950d751713575fbf91c7709aa + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + veilid: 51243c25047dbc1ebbfd87d713560260d802b845 PODFILE CHECKSUM: c8bf5b16c34712d5790b0b8d2472cc66ac0a8487 -COCOAPODS: 1.16.2 +COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 06556a5..5b0968b 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -156,6 +156,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { @@ -342,6 +343,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -352,6 +354,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -416,6 +419,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -426,6 +430,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -471,6 +476,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -481,6 +487,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 3e31b44..1eb7fee 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ =3.2.0 <4.0.0' From f4407e5284d7093b5eb1c358fb92f21a0af8e30e Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 30 Mar 2025 16:23:53 -0400 Subject: [PATCH 224/270] build update --- ios/Podfile.lock | 30 +++++++++---------- ios/Runner.xcodeproj/project.pbxproj | 9 ++++-- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 9f7e7e6..2528d2d 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -143,10 +143,10 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/veilid/ios" SPEC CHECKSUMS: - camera_avfoundation: dd002b0330f4981e1bbcb46ae9b62829237459a4 - file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 + camera_avfoundation: 04b44aeb14070126c6529e5ab82cc7c9fca107cf + file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29 + flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318 GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 @@ -156,20 +156,20 @@ SPEC CHECKSUMS: MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2 MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e - mobile_scanner: fd0054c52ede661e80bf5a4dea477a2467356bee + mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 - pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - printing: 233e1b73bd1f4a05615548e9b5a324c98588640b + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + printing: 54ff03f28fe9ba3aa93358afb80a8595a071dd07 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 - share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d - system_info_plus: 5393c8da281d899950d751713575fbf91c7709aa - url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe - veilid: 51243c25047dbc1ebbfd87d713560260d802b845 + share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + system_info_plus: 555ce7047fbbf29154726db942ae785c29211740 + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + veilid: 3ce560a4f2b568a77a9fd5e23090f2fa97581019 PODFILE CHECKSUM: c8bf5b16c34712d5790b0b8d2472cc66ac0a8487 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 5b0968b..e612191 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -354,7 +354,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -380,6 +380,7 @@ CLANG_ENABLE_MODULES = YES; DEVELOPMENT_TEAM = XP5LBLT7M7; ENABLE_BITCODE = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = VeilidChat; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; @@ -430,7 +431,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -487,7 +488,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -515,6 +516,7 @@ CLANG_ENABLE_MODULES = YES; DEVELOPMENT_TEAM = XP5LBLT7M7; ENABLE_BITCODE = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = VeilidChat; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; @@ -540,6 +542,7 @@ CLANG_ENABLE_MODULES = YES; DEVELOPMENT_TEAM = XP5LBLT7M7; ENABLE_BITCODE = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = VeilidChat; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 1eb7fee..3e31b44 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Date: Thu, 3 Apr 2025 13:51:14 -0400 Subject: [PATCH 225/270] Fix getting stuck on splash screen when veilid is already started --- .../repository/processor_repository.dart | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/veilid_processor/repository/processor_repository.dart b/lib/veilid_processor/repository/processor_repository.dart index 7ff2482..a92347c 100644 --- a/lib/veilid_processor/repository/processor_repository.dart +++ b/lib/veilid_processor/repository/processor_repository.dart @@ -44,8 +44,21 @@ class ProcessorRepository { log.info('Veilid version: $veilidVersion'); - final updateStream = await Veilid.instance - .startupVeilidCore(await getVeilidConfig(kIsWeb, VeilidChatApp.name)); + Stream updateStream; + + try { + log.debug('Starting VeilidCore'); + updateStream = await Veilid.instance + .startupVeilidCore(await getVeilidConfig(kIsWeb, VeilidChatApp.name)); + } on VeilidAPIExceptionAlreadyInitialized catch (_) { + log.debug( + 'VeilidCore is already started, shutting down and restarting...'); + startedUp = true; + await shutdown(); + updateStream = await Veilid.instance + .startupVeilidCore(await getVeilidConfig(kIsWeb, VeilidChatApp.name)); + } + _updateSubscription = updateStream.listen((update) { if (update is VeilidLog) { processLog(update); From e1f081105ab32efd6773c8d883196815a762cc9c Mon Sep 17 00:00:00 2001 From: Brandon Vandegrift <798832-bmv437@users.noreply.gitlab.com> Date: Thu, 3 Apr 2025 14:27:56 -0400 Subject: [PATCH 226/270] Fix routing to home after initial account creation --- lib/account_manager/views/new_account_page.dart | 13 ++++++++++--- .../views/show_recovery_key_page.dart | 15 +++++++++++---- lib/router/cubits/router_cubit.dart | 15 ++++++--------- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/lib/account_manager/views/new_account_page.dart b/lib/account_manager/views/new_account_page.dart index a739094..07034df 100644 --- a/lib/account_manager/views/new_account_page.dart +++ b/lib/account_manager/views/new_account_page.dart @@ -63,10 +63,13 @@ class _NewAccountPageState extends WindowSetupState { return false; } + final isFirstAccount = + AccountRepository.instance.getLocalAccounts().isEmpty; + final writableSuperIdentity = await AccountRepository.instance .createWithNewSuperIdentity(accountSpec); GoRouterHelper(context).pushReplacement('/new_account/recovery_key', - extra: [writableSuperIdentity, accountSpec.name]); + extra: [writableSuperIdentity, accountSpec.name, isFirstAccount]); return true; } finally { @@ -92,11 +95,15 @@ class _NewAccountPageState extends WindowSetupState { return StyledScaffold( appBar: DefaultAppBar( title: Text(translate('new_account_page.titlebar')), - leading: Navigator.canPop(context) + leading: GoRouterHelper(context).canPop() ? IconButton( icon: const Icon(Icons.arrow_back), onPressed: () { - Navigator.pop(context); + if (GoRouterHelper(context).canPop()) { + GoRouterHelper(context).pop(); + } else { + GoRouterHelper(context).go('/'); + } }, ) : null, diff --git a/lib/account_manager/views/show_recovery_key_page.dart b/lib/account_manager/views/show_recovery_key_page.dart index acbb3f3..bf44dd7 100644 --- a/lib/account_manager/views/show_recovery_key_page.dart +++ b/lib/account_manager/views/show_recovery_key_page.dart @@ -25,15 +25,18 @@ class ShowRecoveryKeyPage extends StatefulWidget { const ShowRecoveryKeyPage( {required WritableSuperIdentity writableSuperIdentity, required String name, + required bool isFirstAccount, super.key}) : _writableSuperIdentity = writableSuperIdentity, - _name = name; + _name = name, + _isFirstAccount = isFirstAccount; @override State createState() => _ShowRecoveryKeyPageState(); final WritableSuperIdentity _writableSuperIdentity; final String _name; + final bool _isFirstAccount; } class _ShowRecoveryKeyPageState extends WindowSetupState { @@ -248,9 +251,13 @@ class _ShowRecoveryKeyPageState extends WindowSetupState { child: ElevatedButton( onPressed: () { if (context.mounted) { - Navigator.canPop(context) - ? GoRouterHelper(context).pop() - : GoRouterHelper(context).go('/'); + if (widget._isFirstAccount) { + GoRouterHelper(context).go('/'); + } else { + GoRouterHelper(context).canPop() + ? GoRouterHelper(context).pop() + : GoRouterHelper(context).go('/'); + } } }, child: Text(translate('button.finish')).paddingAll(8)) diff --git a/lib/router/cubits/router_cubit.dart b/lib/router/cubits/router_cubit.dart index 19af5e0..a15eb50 100644 --- a/lib/router/cubits/router_cubit.dart +++ b/lib/router/cubits/router_cubit.dart @@ -96,7 +96,8 @@ class RouterCubit extends Cubit { if (extra == null || extra is! List || extra[0] is! WritableSuperIdentity || - extra[1] is! String) { + extra[1] is! String || + extra[2] is! bool) { return '/'; } return null; @@ -107,7 +108,8 @@ class RouterCubit extends Cubit { return ShowRecoveryKeyPage( writableSuperIdentity: extra[0] as WritableSuperIdentity, - name: extra[1] as String); + name: extra[1] as String, + isFirstAccount: extra[2] as bool); }), ]), GoRoute( @@ -123,14 +125,8 @@ class RouterCubit extends Cubit { /// Redirects when our state changes String? redirect(BuildContext context, GoRouterState goRouterState) { - // No matter where we are, if there's not - switch (goRouterState.matchedLocation) { - case '/': - if (!state.hasAnyAccount) { - return '/new_account'; - } - return null; + // We can go to any of these routes without an account. case '/new_account': return null; case '/new_account/recovery_key': @@ -139,6 +135,7 @@ class RouterCubit extends Cubit { return null; case '/developer': return null; + // Otherwise, if there's no account, we need to go to the new account page. default: return state.hasAnyAccount ? null : '/new_account'; } From a3aa7569ab50fd746829597f3454b82ac3b2b48a Mon Sep 17 00:00:00 2001 From: Brandon Vandegrift <798832-bmv437@users.noreply.gitlab.com> Date: Thu, 3 Apr 2025 14:52:09 -0400 Subject: [PATCH 227/270] edit_account_form visual improvements This changes the icon for the "Waiting for network" button to an hourglass instead of a checkmark, and adds more vertical spacing between the field so that the labels and validation messages don't collide. --- lib/account_manager/views/edit_profile_form.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/account_manager/views/edit_profile_form.dart b/lib/account_manager/views/edit_profile_form.dart index 80bd2b3..4977dc7 100644 --- a/lib/account_manager/views/edit_profile_form.dart +++ b/lib/account_manager/views/edit_profile_form.dart @@ -192,6 +192,8 @@ class _EditProfileFormState extends State { onChanged: _onChanged, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + spacing: 8, children: [ Row(children: [ const Spacer(), @@ -289,7 +291,7 @@ class _EditProfileFormState extends State { _currentValueAutoAway = v ?? false; }); }, - ), + ).paddingLTRB(0, 0, 0, 16), FormBuilderTextField( name: EditProfileForm.formFieldAutoAwayTimeout, enabled: _currentValueAutoAway, @@ -319,7 +321,9 @@ class _EditProfileFormState extends State { return ElevatedButton( onPressed: (networkReady && _isModified) ? _doSubmit : null, child: Row(mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0), + Icon(networkReady ? Icons.check : Icons.hourglass_empty, + size: 16) + .paddingLTRB(0, 0, 4, 0), Text(networkReady ? widget.submitText : widget.submitDisabledText) From 02b03c8cabfacb5ed74e649c8062c0c02a6a3e07 Mon Sep 17 00:00:00 2001 From: Paul Sajna Date: Fri, 4 Apr 2025 15:54:30 +0000 Subject: [PATCH 228/270] Flatpak CI Update --- .gitlab-ci.yml | 74 ++++++++++++++++--------------- flatpak/com.veilid.veilidchat.yml | 2 +- 2 files changed, 40 insertions(+), 36 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4476fbd..72e4576 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,76 +3,80 @@ stages: - build - - build_flatpak -# - test -.macos_saas_runners: - tags: - - saas-macos-medium-m1 - image: macos-12-xcode-14 - before_script: - - echo "started by ${GITLAB_USER_NAME}" +#.macos_saas_runners: +# tags: +# - saas-macos-medium-m1 +# image: macos-12-xcode-14 +# before_script: +# - echo "started by ${GITLAB_USER_NAME}" -build_macos: - extends: - - .macos_saas_runners - stage: build - script: - - echo "place holder for build" - - sudo softwareupdate --install-rosetta --agree-to-license - - git clone https://gitlab.com/veilid/veilid.git ../veilid +#build_macos: +# extends: +# - .macos_saas_runners +# stage: build +# script: +# - echo "place holder for build" +# - sudo softwareupdate --install-rosetta --agree-to-license +# - git clone https://gitlab.com/veilid/veilid.git ../veilid #- curl –proto ‘=https’ –tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y #- source "$HOME/.cargo/env" #- brew install capnp cmake wabt llvm protobuf openjdk@17 jq cocoapods #- cargo install wasm-bindgen-cli wasm-pack cargo-edit - - wget https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_3.13.5-stable.zip - - unzip flutter_macos_arm64_3.13.5-stable.zip && export PATH="$PATH:`pwd`/flutter/bin" - - flutter upgrade - - yes | flutter doctor --android-licenses - - flutter config --enable-macos-desktop --enable-ios - - flutter config --no-analytics - - dart --disable-analytics - - flutter doctor -v +# - wget https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_3.13.5-stable.zip +# - unzip flutter_macos_arm64_3.13.5-stable.zip && export PATH="$PATH:`pwd`/flutter/bin" +# - flutter upgrade +# - yes | flutter doctor --android-licenses +# - flutter config --enable-macos-desktop --enable-ios +# - flutter config --no-analytics +# - dart --disable-analytics +# - flutter doctor -v #- flutter build ipa #- flutter build appbundle - when: manual +# only: +# - schedules build_linux_amd64_bundle: + stage: build tags: - saas-linux-medium-amd64 - image: ghcr.io/cirruslabs/flutter:3.19.4 - stage: build + image: ghcr.io/cirruslabs/flutter:3.29.2 script: - apt-get update - - apt-get install -y --no-install-recommends cmake ninja-build clang build-essential pkg-config libgtk-3-dev liblzma-dev lcov rustc cargo + - apt-get install -y --no-install-recommends cmake ninja-build clang build-essential pkg-config libgtk-3-dev liblzma-dev lcov rustup + - rustup toolchain install 1.81 --profile minimal --no-self-update - flutter config --enable-linux-desktop - git clone https://gitlab.com/veilid/veilid.git ../veilid - flutter build linux artifacts: paths: - build/linux/x64/release/bundle/ - when: manual + only: + - schedules build_linux_amd64_flatpak: tags: - saas-linux-small-amd64 - image: ubuntu:23.04 - stage: build_flatpak + image: ubuntu:24.04 + stage: build dependencies: [build_linux_amd64_bundle] + needs: + - job: build_linux_amd64_bundle + artifacts: true script: - apt-get update - apt-get install -y --no-install-recommends flatpak flatpak-builder gnupg2 elfutils ca-certificates - flatpak remote-add --no-gpg-verify --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo - - flatpak install -y --noninteractive org.gnome.Sdk/x86_64/45 org.gnome.Platform/x86_64/45 app/org.flathub.flatpak-external-data-checker/x86_64/stable org.freedesktop.appstream-glib + - flatpak install -y --noninteractive org.gnome.Sdk/x86_64/46 org.gnome.Platform/x86_64/46 app/org.flathub.flatpak-external-data-checker/x86_64/stable org.freedesktop.appstream-glib - pushd flatpak/ - flatpak-builder --force-clean build-dir com.veilid.veilidchat.yml --repo=repo - flatpak build-bundle repo com.veilid.veilidchat.flatpak com.veilid.veilidchat - popd artifacts: - paths: + paths: - flatpak/com.veilid.veilidchat.flatpak - when: manual - + only: + - schedules #test: # extends: # - .macos_saas_runners diff --git a/flatpak/com.veilid.veilidchat.yml b/flatpak/com.veilid.veilidchat.yml index af45e4b..3db5a02 100644 --- a/flatpak/com.veilid.veilidchat.yml +++ b/flatpak/com.veilid.veilidchat.yml @@ -3,7 +3,7 @@ --- app-id: com.veilid.veilidchat runtime: org.gnome.Platform -runtime-version: "45" +runtime-version: "46" sdk: org.gnome.Sdk command: veilidchat separate-locales: false From 0eb181bb0e3cf08840c69ae47d64aa04c956a927 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 4 Apr 2025 13:16:58 -0400 Subject: [PATCH 229/270] Revert "Merge branch 'fix-linux-desktop' into 'main'" This reverts merge request !32 --- linux/CMakeLists.txt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index e71febc..b669b11 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -122,12 +122,6 @@ foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) COMPONENT Runtime) endforeach(bundled_library) -# Copy the native assets provided by the build.dart from all packages. -set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") -install(DIRECTORY "${NATIVE_ASSETS_DIR}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") From 5e4ffafacda7574be791cc84161812cd8c1450d8 Mon Sep 17 00:00:00 2001 From: Paul Sajna Date: Fri, 4 Apr 2025 17:17:46 +0000 Subject: [PATCH 230/270] Build Arm64 flatpaks --- .gitlab-ci.yml | 43 +++++++++++++++++++++++++ flatpak/com.veilid.veilidchat.arm64.yml | 35 ++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 flatpak/com.veilid.veilidchat.arm64.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 72e4576..78a5f55 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -77,6 +77,49 @@ build_linux_amd64_flatpak: - flatpak/com.veilid.veilidchat.flatpak only: - schedules + +build_linux_arm64_bundle: + stage: build + tags: + - saas-linux-small-arm64 + image: ghcr.io/cirruslabs/flutter:3.29.2 + script: + - apt-get update + - apt-get install -y --no-install-recommends cmake ninja-build clang build-essential pkg-config libgtk-3-dev liblzma-dev lcov rustup + - rustup toolchain install 1.81 --profile minimal --no-self-update + - flutter config --enable-linux-desktop + - git clone https://gitlab.com/veilid/veilid.git ../veilid + - flutter build linux + artifacts: + paths: + - build/linux/arm64/release/bundle/ + only: + - schedules + +build_linux_arm64_flatpak: + tags: + - saas-linux-small-arm64 + image: ubuntu:24.04 + stage: build + dependencies: [build_linux_arm64_bundle] + needs: + - job: build_linux_arm64_bundle + artifacts: true + script: + - apt-get update + - apt-get install -y --no-install-recommends flatpak flatpak-builder gnupg2 elfutils ca-certificates + - flatpak remote-add --no-gpg-verify --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + - flatpak install -y --noninteractive org.gnome.Sdk/aarch64/46 org.gnome.Platform/aarch64/46 app/org.flathub.flatpak-external-data-checker/aarch64/stable org.freedesktop.appstream-glib + - pushd flatpak/ + - flatpak-builder --force-clean build-dir com.veilid.veilidchat.arm64.yml --repo=repo + - flatpak build-bundle repo com.veilid.veilidchat.flatpak com.veilid.veilidchat + - popd + artifacts: + paths: + - flatpak/com.veilid.veilidchat.flatpak + only: + - schedules + #test: # extends: # - .macos_saas_runners diff --git a/flatpak/com.veilid.veilidchat.arm64.yml b/flatpak/com.veilid.veilidchat.arm64.yml new file mode 100644 index 0000000..f1c3d59 --- /dev/null +++ b/flatpak/com.veilid.veilidchat.arm64.yml @@ -0,0 +1,35 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/flatpak/flatpak-builder/main/data/flatpak-manifest.schema.json + +--- +app-id: com.veilid.veilidchat +runtime: org.gnome.Platform +runtime-version: "46" +sdk: org.gnome.Sdk +command: veilidchat +separate-locales: false +finish-args: + - --share=ipc + - --socket=fallback-x11 + - --socket=wayland + - --device=dri + - --socket=pulseaudio + - --share=network + - --talk-name=org.freedesktop.secrets +modules: + - name: VeilidChat + buildsystem: simple + only-arches: + - aarch64 + build-commands: + - "./build-flatpak.sh" + sources: + - type: dir + path: ../build/linux/arm64/release/ + - type: file + path: build-flatpak.sh + - type: file + path: com.veilid.veilidchat.png + - type: file + path: com.veilid.veilidchat.desktop + - type: file + path: com.veilid.veilidchat.metainfo.xml From 91883d59e5727712cc1d66c34cebdaae67b2e401 Mon Sep 17 00:00:00 2001 From: Brandon Vandegrift <798832-bmv437@users.noreply.gitlab.com> Date: Sun, 6 Apr 2025 13:45:34 -0400 Subject: [PATCH 231/270] Remove duplicate html inside of index.html --- web/index.html | 136 +------------------------------------------------ 1 file changed, 1 insertion(+), 135 deletions(-) diff --git a/web/index.html b/web/index.html index 54ea8ab..99eac34 100644 --- a/web/index.html +++ b/web/index.html @@ -19,7 +19,7 @@ - + @@ -66,137 +66,3 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - VeilidChat - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From bd00636780b3543e26723a8db163c38bb29befdd Mon Sep 17 00:00:00 2001 From: TC Johnson Date: Sun, 6 Apr 2025 17:30:30 -0500 Subject: [PATCH 232/270] Update changelog for v0.4.7 --- CHANGELOG.md | 9 +++++++++ ios/Podfile.lock | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 843cc65..5cd3873 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## v0.4.7 ## +- *Community Contributions* + - Fix getting stuck on splash screen when veilid is already started @bmv437 / @bgrift + - Fix routing to home after initial account creation @bmv437 / @bgrift + - edit_account_form visual improvements @bmv437 / @bgrift + - Flatpak CI Update @sajattack + - Build Arm64 flatpaks @sajattack +- Dependency updates + ## v0.4.6 ## - Updated veilid-core to v0.4.4 - See Veilid changelog for specifics diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 2528d2d..d943756 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -168,7 +168,7 @@ SPEC CHECKSUMS: sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 system_info_plus: 555ce7047fbbf29154726db942ae785c29211740 url_launcher_ios: 694010445543906933d732453a59da0a173ae33d - veilid: 3ce560a4f2b568a77a9fd5e23090f2fa97581019 + veilid: b3b9418ae6b083e662396bfa2c635fb115c8510e PODFILE CHECKSUM: c8bf5b16c34712d5790b0b8d2472cc66ac0a8487 From 1ce01d17b01bc83ab6e8d0332eafb919e893ffae Mon Sep 17 00:00:00 2001 From: TC Johnson Date: Sun, 6 Apr 2025 17:33:17 -0500 Subject: [PATCH 233/270] =?UTF-8?q?Version=20update:=20v0.4.6=20=E2=86=92?= =?UTF-8?q?=20v0.4.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 874bed6..a5b4502 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.4.6+0 +current_version = 0.4.7+0 commit = False tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)\+(?P\d+) diff --git a/pubspec.yaml b/pubspec.yaml index 48244dd..ac985bf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: veilidchat description: VeilidChat publish_to: 'none' -version: 0.4.6+19 +version: 0.4.7+20 environment: sdk: '>=3.2.0 <4.0.0' From b34503bbf2e97682e881136322abc04be8b03df9 Mon Sep 17 00:00:00 2001 From: TC Date: Mon, 7 Apr 2025 14:49:00 +0000 Subject: [PATCH 234/270] Update .gitlab-ci.yml file to run builds when a new version tag is published. --- .gitlab-ci.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 78a5f55..a29048e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -51,8 +51,8 @@ build_linux_amd64_bundle: artifacts: paths: - build/linux/x64/release/bundle/ - only: - - schedules + rules: + - if: '$CI_COMMIT_TAG =~ /v\d.+/' build_linux_amd64_flatpak: tags: @@ -75,8 +75,8 @@ build_linux_amd64_flatpak: artifacts: paths: - flatpak/com.veilid.veilidchat.flatpak - only: - - schedules + rules: + - if: '$CI_COMMIT_TAG =~ /v\d.+/' build_linux_arm64_bundle: stage: build @@ -93,8 +93,8 @@ build_linux_arm64_bundle: artifacts: paths: - build/linux/arm64/release/bundle/ - only: - - schedules + rules: + - if: '$CI_COMMIT_TAG =~ /v\d.+/' build_linux_arm64_flatpak: tags: @@ -117,8 +117,8 @@ build_linux_arm64_flatpak: artifacts: paths: - flatpak/com.veilid.veilidchat.flatpak - only: - - schedules + rules: + - if: '$CI_COMMIT_TAG =~ /v\d.+/' #test: # extends: From f8977147f6084aa08a4e0fc66a0fe170cf74e8e6 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 9 Apr 2025 15:38:30 -0400 Subject: [PATCH 235/270] remove things that fail to deserialize --- .../models/local_account/local_account.dart | 11 ++++++++-- .../models/user_login/user_login.dart | 11 ++++++++-- packages/veilid_support/lib/src/table_db.dart | 22 ++++++++++++++----- .../veilid_support/lib/src/veilid_log.dart | 1 + 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/lib/account_manager/models/local_account/local_account.dart b/lib/account_manager/models/local_account/local_account.dart index 1ec6d22..49506d0 100644 --- a/lib/account_manager/models/local_account/local_account.dart +++ b/lib/account_manager/models/local_account/local_account.dart @@ -40,6 +40,13 @@ sealed class LocalAccount with _$LocalAccount { required String name, }) = _LocalAccount; - factory LocalAccount.fromJson(dynamic json) => - _$LocalAccountFromJson(json as Map); + factory LocalAccount.fromJson(dynamic json) { + try { + return _$LocalAccountFromJson(json as Map); + // Need to catch any errors here too + // ignore: avoid_catches_without_on_clauses + } catch (e, st) { + throw Exception('invalid local account: $e\n$st'); + } + } } diff --git a/lib/account_manager/models/user_login/user_login.dart b/lib/account_manager/models/user_login/user_login.dart index d43fdfd..4d96d38 100644 --- a/lib/account_manager/models/user_login/user_login.dart +++ b/lib/account_manager/models/user_login/user_login.dart @@ -23,6 +23,13 @@ sealed class UserLogin with _$UserLogin { required Timestamp lastActive, }) = _UserLogin; - factory UserLogin.fromJson(dynamic json) => - _$UserLoginFromJson(json as Map); + factory UserLogin.fromJson(dynamic json) { + try { + return _$UserLoginFromJson(json as Map); + // Need to catch any errors here too + // ignore: avoid_catches_without_on_clauses + } catch (e, st) { + throw Exception('invalid user login: $e\n$st'); + } + } } diff --git a/packages/veilid_support/lib/src/table_db.dart b/packages/veilid_support/lib/src/table_db.dart index 522a837..eb7fe99 100644 --- a/packages/veilid_support/lib/src/table_db.dart +++ b/packages/veilid_support/lib/src/table_db.dart @@ -6,6 +6,9 @@ import 'package:async_tools/async_tools.dart'; import 'package:meta/meta.dart'; import 'package:veilid/veilid.dart'; +import '../veilid_support.dart'; +import 'veilid_log.dart'; + Future tableScope( String name, Future Function(VeilidTableDB tdb) callback, {int columnCount = 1}) async { @@ -48,11 +51,20 @@ abstract mixin class TableDBBackedJson { /// Load things from storage @protected Future load() async { - final obj = await tableScope(tableName(), (tdb) async { - final objJson = await tdb.loadStringJson(0, tableKeyName()); - return valueFromJson(objJson); - }); - return obj; + try { + final obj = await tableScope(tableName(), (tdb) async { + final objJson = await tdb.loadStringJson(0, tableKeyName()); + return valueFromJson(objJson); + }); + return obj; + } on Exception catch (e, st) { + veilidLoggy.debug( + 'Unable to load data from table store: ' + '${tableName()}:${tableKeyName()}', + e, + st); + return null; + } } /// Store things to storage diff --git a/packages/veilid_support/lib/src/veilid_log.dart b/packages/veilid_support/lib/src/veilid_log.dart index 3b35b5d..4c9ad0b 100644 --- a/packages/veilid_support/lib/src/veilid_log.dart +++ b/packages/veilid_support/lib/src/veilid_log.dart @@ -69,6 +69,7 @@ void processLog(VeilidLog log) { } void initVeilidLog(bool debugMode) { + // Always allow LOG_TRACE option // ignore: do_not_use_environment const isTrace = String.fromEnvironment('LOG_TRACE') != ''; LogLevel logLevel; From 2313247407bd270266b0f7e8cfc3bf4edd07f5b9 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 9 Apr 2025 18:24:46 -0400 Subject: [PATCH 236/270] fix popcontrol for android --- lib/router/views/router_shell.dart | 6 ++++-- lib/theme/views/pop_control.dart | 9 +++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/router/views/router_shell.dart b/lib/router/views/router_shell.dart index 8a7130f..164c452 100644 --- a/lib/router/views/router_shell.dart +++ b/lib/router/views/router_shell.dart @@ -2,13 +2,15 @@ import 'package:flutter/widgets.dart'; import '../../keyboard_shortcuts.dart'; import '../../notifications/notifications.dart'; +import '../../theme/theme.dart'; class RouterShell extends StatelessWidget { const RouterShell({required Widget child, super.key}) : _child = child; @override - Widget build(BuildContext context) => - NotificationsWidget(child: KeyboardShortcuts(child: _child)); + Widget build(BuildContext context) => PopControl( + dismissible: false, + child: NotificationsWidget(child: KeyboardShortcuts(child: _child))); final Widget _child; } diff --git a/lib/theme/views/pop_control.dart b/lib/theme/views/pop_control.dart index deaf785..d2e98f9 100644 --- a/lib/theme/views/pop_control.dart +++ b/lib/theme/views/pop_control.dart @@ -8,18 +8,15 @@ class PopControl extends StatelessWidget { super.key, }); - void _doDismiss(NavigatorState navigator) { + void _doDismiss(BuildContext context) { if (!dismissible) { return; } - navigator.pop(); + Navigator.of(context).pop(); } @override - // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { - final navigator = Navigator.of(context); - final route = ModalRoute.of(context); if (route != null && route is PopControlDialogRoute) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -33,7 +30,7 @@ class PopControl extends StatelessWidget { if (didPop) { return; } - _doDismiss(navigator); + _doDismiss(context); return; }, child: child); From d0fe5c5519e18be5a595207a5e0d4d182779b5ac Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 9 Apr 2025 18:36:46 -0400 Subject: [PATCH 237/270] additional popcontrol for child route --- lib/router/cubits/router_cubit.dart | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/router/cubits/router_cubit.dart b/lib/router/cubits/router_cubit.dart index a15eb50..e5a2024 100644 --- a/lib/router/cubits/router_cubit.dart +++ b/lib/router/cubits/router_cubit.dart @@ -12,6 +12,7 @@ import 'package:veilid_support/veilid_support.dart'; import '../../../account_manager/account_manager.dart'; import '../../layout/layout.dart'; import '../../settings/settings.dart'; +import '../../theme/theme.dart'; import '../../tools/tools.dart'; import '../../veilid_processor/views/developer.dart'; import '../views/router_shell.dart'; @@ -105,11 +106,13 @@ class RouterCubit extends Cubit { builder: (context, state) { final extra = state.extra! as List; - return ShowRecoveryKeyPage( - writableSuperIdentity: - extra[0] as WritableSuperIdentity, - name: extra[1] as String, - isFirstAccount: extra[2] as bool); + return PopControl( + dismissible: false, + child: ShowRecoveryKeyPage( + writableSuperIdentity: + extra[0] as WritableSuperIdentity, + name: extra[1] as String, + isFirstAccount: extra[2] as bool)); }), ]), GoRoute( From bf38c2c44df0d88d6e80fc7002dd022f8822ea65 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 17 Apr 2025 18:55:43 -0400 Subject: [PATCH 238/270] clean up a bunch of exceptions --- devtools_options.yaml | 3 +- ios/Podfile.lock | 2 +- .../message_reconciliation.dart | 28 +++-- .../cubits/single_contact_messages_cubit.dart | 56 +++++---- lib/router/cubits/router_cubit.dart | 3 +- lib/veilid_processor/views/developer.dart | 16 ++- packages/veilid_support/example/pubspec.lock | 2 +- .../lib/dht_support/src/dht_log/dht_log.dart | 1 + .../dht_support/src/dht_log/dht_log_read.dart | 12 +- .../src/dht_log/dht_log_spine.dart | 7 +- .../src/dht_log/dht_log_write.dart | 37 ++++-- .../src/dht_record/dht_record_pool.dart | 117 +++++------------- .../dht_record/dht_record_pool_private.dart | 20 +-- .../src/dht_short_array/dht_short_array.dart | 1 + .../dht_short_array/dht_short_array_head.dart | 18 +++ .../dht_short_array/dht_short_array_read.dart | 12 +- .../dht_short_array_write.dart | 20 ++- .../src/interfaces/exceptions.dart | 14 ++- .../lib/src/persistent_queue.dart | 27 +++- .../lib/src/table_db_array.dart | 12 +- packages/veilid_support/pubspec.lock | 2 +- 21 files changed, 244 insertions(+), 166 deletions(-) diff --git a/devtools_options.yaml b/devtools_options.yaml index 5c27c3e..7093540 100644 --- a/devtools_options.yaml +++ b/devtools_options.yaml @@ -1,2 +1,3 @@ extensions: - - provider: true \ No newline at end of file + - provider: true + - shared_preferences: true \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d943756..2528d2d 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -168,7 +168,7 @@ SPEC CHECKSUMS: sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 system_info_plus: 555ce7047fbbf29154726db942ae785c29211740 url_launcher_ios: 694010445543906933d732453a59da0a173ae33d - veilid: b3b9418ae6b083e662396bfa2c635fb115c8510e + veilid: 3ce560a4f2b568a77a9fd5e23090f2fa97581019 PODFILE CHECKSUM: c8bf5b16c34712d5790b0b8d2472cc66ac0a8487 diff --git a/lib/chat/cubits/reconciliation/message_reconciliation.dart b/lib/chat/cubits/reconciliation/message_reconciliation.dart index f0b8c4c..9b183b5 100644 --- a/lib/chat/cubits/reconciliation/message_reconciliation.dart +++ b/lib/chat/cubits/reconciliation/message_reconciliation.dart @@ -6,6 +6,7 @@ import 'package:sorted_list/sorted_list.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../../proto/proto.dart' as proto; +import '../../../tools/tools.dart'; import 'author_input_queue.dart'; import 'author_input_source.dart'; import 'output_position.dart'; @@ -62,17 +63,24 @@ class MessageReconciliation { Future _enqueueAuthorInput( {required TypedKey author, required AuthorInputSource inputSource}) async { - // Get the position of our most recent reconciled message from this author - final outputPosition = await _findLastOutputPosition(author: author); + try { + // Get the position of our most recent reconciled message from this author + final outputPosition = await _findLastOutputPosition(author: author); - // Find oldest message we have not yet reconciled - final inputQueue = await AuthorInputQueue.create( - author: author, - inputSource: inputSource, - outputPosition: outputPosition, - onError: _onError, - ); - return inputQueue; + // Find oldest message we have not yet reconciled + final inputQueue = await AuthorInputQueue.create( + author: author, + inputSource: inputSource, + outputPosition: outputPosition, + onError: _onError, + ); + return inputQueue; + // Catch everything so we can avoid ParallelWaitError + // ignore: avoid_catches_without_on_clauses + } catch (e, st) { + log.error('Exception enqueing author input: $e:\n$st\n'); + return null; + } } // Get the position of our most recent reconciled message from this author diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index b4e77db..559eae2 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -100,8 +100,8 @@ class SingleContactMessagesCubit extends Cubit { key: _remoteConversationRecordKey.toString(), fromBuffer: proto.Message.fromBuffer, closure: _processUnsentMessages, - onError: (e, sp) { - log.error('Exception while processing unsent messages: $e\n$sp\n'); + onError: (e, st) { + log.error('Exception while processing unsent messages: $e\n$st\n'); }); // Make crypto @@ -310,14 +310,11 @@ class SingleContactMessagesCubit extends Cubit { Future _processMessageToSend( proto.Message message, proto.Message? previousMessage) async { - // Get the previous message if we don't have one - previousMessage ??= await _sentMessagesCubit!.operate((r) async => - r.length == 0 - ? null - : await r.getProtobuf(proto.Message.fromBuffer, r.length - 1)); - - message.id = - await _senderMessageIntegrity.generateMessageId(previousMessage); + // It's possible we had a signature from a previous + // operateAppendEventual attempt, so clear it and make a new message id too + message + ..clearSignature() + ..id = await _senderMessageIntegrity.generateMessageId(previousMessage); // Now sign it await _senderMessageIntegrity.signMessage( @@ -326,26 +323,33 @@ class SingleContactMessagesCubit extends Cubit { // Async process to send messages in the background Future _processUnsentMessages(IList messages) async { - // Go through and assign ids to all the messages in order - proto.Message? previousMessage; - final processedMessages = messages.toList(); - for (final message in processedMessages) { - try { - await _processMessageToSend(message, previousMessage); - previousMessage = message; - } on Exception catch (e) { - log.error('Exception processing unsent message: $e'); - } - } - // _sendingMessages = messages; // _renderState(); try { - await _sentMessagesCubit!.operateAppendEventual((writer) => - writer.addAll(messages.map((m) => m.writeToBuffer()).toList())); - } on Exception catch (e) { - log.error('Exception appending unsent messages: $e'); + await _sentMessagesCubit!.operateAppendEventual((writer) async { + // Get the previous message if we have one + var previousMessage = writer.length == 0 + ? null + : await writer.getProtobuf( + proto.Message.fromBuffer, writer.length - 1); + + // Sign all messages + final processedMessages = messages.toList(); + for (final message in processedMessages) { + try { + await _processMessageToSend(message, previousMessage); + previousMessage = message; + } on Exception catch (e, st) { + log.error('Exception processing unsent message: $e:\n$st\n'); + } + } + final byteMessages = messages.map((m) => m.writeToBuffer()).toList(); + + return writer.addAll(byteMessages); + }); + } on Exception catch (e, st) { + log.error('Exception appending unsent messages: $e:\n$st\n'); } // _sendingMessages = const IList.empty(); diff --git a/lib/router/cubits/router_cubit.dart b/lib/router/cubits/router_cubit.dart index e5a2024..d651611 100644 --- a/lib/router/cubits/router_cubit.dart +++ b/lib/router/cubits/router_cubit.dart @@ -138,7 +138,8 @@ class RouterCubit extends Cubit { return null; case '/developer': return null; - // Otherwise, if there's no account, we need to go to the new account page. + // Otherwise, if there's no account, + // we need to go to the new account page. default: return state.hasAnyAccount ? null : '/new_account'; } diff --git a/lib/veilid_processor/views/developer.dart b/lib/veilid_processor/views/developer.dart index cb64d3d..08463fb 100644 --- a/lib/veilid_processor/views/developer.dart +++ b/lib/veilid_processor/views/developer.dart @@ -124,7 +124,21 @@ class _DeveloperPageState extends State { _debugOut('DEBUG >>>\n$debugCommand\n'); try { - final out = await Veilid.instance.debug(debugCommand); + var out = await Veilid.instance.debug(debugCommand); + + if (debugCommand == 'help') { + out = 'VeilidChat Commands:\n' + ' pool allocations - List DHTRecordPool allocations\n' + ' pool opened - List opened DHTRecord instances' + ' from the pool\n' + ' change_log_ignore change the log' + ' target ignore list for a tracing layer\n' + ' targets to add to the ignore list can be separated by' + ' a comma.\n' + ' to remove a target from the ignore list, prepend it' + ' with a minus.\n\n$out'; + } + _debugOut('<<< DEBUG\n$out\n'); } on Exception catch (e, st) { _debugOut('<<< ERROR\n$e\n<<< STACK\n$st'); diff --git a/packages/veilid_support/example/pubspec.lock b/packages/veilid_support/example/pubspec.lock index 6ef291b..2e87d8c 100644 --- a/packages/veilid_support/example/pubspec.lock +++ b/packages/veilid_support/example/pubspec.lock @@ -650,7 +650,7 @@ packages: path: "../../../../veilid/veilid-flutter" relative: true source: path - version: "0.4.3" + version: "0.4.4" veilid_support: dependency: "direct main" description: diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart index 8f88ce1..1d3fb89 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart @@ -7,6 +7,7 @@ import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; +import '../../../src/veilid_log.dart'; import '../../../veilid_support.dart'; import '../../proto/proto.dart' as proto; diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart index 6281d6e..d8634c6 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart @@ -47,8 +47,16 @@ class _DHTLogRead implements DHTLogReadOperations { final chunks = Iterable.generate(length) .slices(kMaxDHTConcurrency) - .map((chunk) => chunk - .map((pos) async => get(pos + start, forceRefresh: forceRefresh))); + .map((chunk) => chunk.map((pos) async { + try { + return get(pos + start, forceRefresh: forceRefresh); + // Need some way to debug ParallelWaitError + // ignore: avoid_catches_without_on_clauses + } catch (e, st) { + veilidLoggy.error('$e\n$st\n'); + rethrow; + } + })); for (final chunk in chunks) { final elems = await chunk.wait; diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index bb27e04..d9f5df2 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -248,7 +248,12 @@ class _DHTLogSpine { final headDelta = _ringDistance(newHead, oldHead); final tailDelta = _ringDistance(newTail, oldTail); if (headDelta > _positionLimit ~/ 2 || tailDelta > _positionLimit ~/ 2) { - throw const DHTExceptionInvalidData(); + throw DHTExceptionInvalidData('_DHTLogSpine::_updateHead ' + '_head=$_head _tail=$_tail ' + 'oldHead=$oldHead oldTail=$oldTail ' + 'newHead=$newHead newTail=$newTail ' + 'headDelta=$headDelta tailDelta=$tailDelta ' + '_positionLimit=$_positionLimit'); } } diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart index 1b5c09f..8d34280 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart @@ -17,7 +17,8 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { } final lookup = await _spine.lookupPosition(pos); if (lookup == null) { - throw const DHTExceptionInvalidData(); + throw DHTExceptionInvalidData( + '_DHTLogRead::tryWriteItem pos=$pos _spine.length=${_spine.length}'); } // Write item to the segment @@ -45,12 +46,14 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { } final aLookup = await _spine.lookupPosition(aPos); if (aLookup == null) { - throw const DHTExceptionInvalidData(); + throw DHTExceptionInvalidData('_DHTLogWrite::swap aPos=$aPos bPos=$bPos ' + '_spine.length=${_spine.length}'); } final bLookup = await _spine.lookupPosition(bPos); if (bLookup == null) { await aLookup.close(); - throw const DHTExceptionInvalidData(); + throw DHTExceptionInvalidData('_DHTLogWrite::swap aPos=$aPos bPos=$bPos ' + '_spine.length=${_spine.length}'); } // Swap items in the segments @@ -65,7 +68,10 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { if (bItem.value == null) { final aItem = await aWrite.get(aLookup.pos); if (aItem == null) { - throw const DHTExceptionInvalidData(); + throw DHTExceptionInvalidData( + '_DHTLogWrite::swap aPos=$aPos bPos=$bPos ' + 'aLookup.pos=${aLookup.pos} bLookup.pos=${bLookup.pos} ' + '_spine.length=${_spine.length}'); } await sb.operateWriteEventual((bWrite) async { final success = await bWrite @@ -101,7 +107,9 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { await write.clear(); } else if (lookup.pos != write.length) { // We should always be appending at the length - throw const DHTExceptionInvalidData(); + throw DHTExceptionInvalidData( + '_DHTLogWrite::add lookup.pos=${lookup.pos} ' + 'write.length=${write.length}'); } return write.add(value); })); @@ -117,12 +125,16 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { final dws = DelayedWaitSet(); var success = true; - for (var valueIdx = 0; valueIdx < values.length;) { + for (var valueIdxIter = 0; valueIdxIter < values.length;) { + final valueIdx = valueIdxIter; final remaining = values.length - valueIdx; final lookup = await _spine.lookupPosition(insertPos + valueIdx); if (lookup == null) { - throw const DHTExceptionInvalidData(); + throw DHTExceptionInvalidData('_DHTLogWrite::addAll ' + '_spine.length=${_spine.length}' + 'insertPos=$insertPos valueIdx=$valueIdx ' + 'values.length=${values.length} '); } final sacount = min(remaining, DHTShortArray.maxElements - lookup.pos); @@ -137,16 +149,21 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { await write.clear(); } else if (lookup.pos != write.length) { // We should always be appending at the length - throw const DHTExceptionInvalidData(); + await write.truncate(lookup.pos); } - return write.addAll(sublistValues); + await write.addAll(sublistValues); + success = true; })); } on DHTExceptionOutdated { success = false; + // Need some way to debug ParallelWaitError + // ignore: avoid_catches_without_on_clauses + } catch (e, st) { + veilidLoggy.error('$e\n$st\n'); } }); - valueIdx += sacount; + valueIdxIter += sacount; } await dws(); 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 9027799..e3c9abe 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 @@ -246,11 +246,13 @@ class DHTRecordPool with TableDBBackedJson { /// Print children String debugChildren(TypedKey recordKey, {List? allDeps}) { allDeps ??= _collectChildrenInner(recordKey); + // Debugging // ignore: avoid_print var out = 'Parent: $recordKey (${_state.debugNames[recordKey.toString()]})\n'; for (final dep in allDeps) { if (dep != recordKey) { + // Debugging // ignore: avoid_print out += ' Child: $dep (${_state.debugNames[dep.toString()]})\n'; } @@ -270,32 +272,25 @@ class DHTRecordPool with TableDBBackedJson { break; } } - } else { - final now = Veilid.instance.now().value; - // Expired, process renewal if desired - for (final entry in _opened.entries) { - final openedKey = entry.key; - final openedRecordInfo = entry.value; - - if (openedKey == updateValueChange.key) { - // Renew watch state for each opened record - 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; - } - } } + // else { + + // XXX: should no longer be necessary + // // Remove watch state + // + // for (final entry in _opened.entries) { + // final openedKey = entry.key; + // final openedRecordInfo = entry.value; + + // if (openedKey == updateValueChange.key) { + // for (final rec in openedRecordInfo.records) { + // rec._watchState = null; + // } + // openedRecordInfo.shared.needsWatchStateUpdate = true; + // break; + // } + // } + //} } /// Log the current record allocations @@ -735,7 +730,6 @@ class DHTRecordPool with TableDBBackedJson { int? totalCount; Timestamp? maxExpiration; List? allSubkeys; - Timestamp? earliestRenewalTime; var noExpiration = false; var everySubkey = false; @@ -768,15 +762,6 @@ class DHTRecordPool with TableDBBackedJson { } else { everySubkey = true; } - final wsRenewalTime = ws.renewalTime; - if (wsRenewalTime != null) { - earliestRenewalTime = earliestRenewalTime == null - ? wsRenewalTime - : Timestamp( - value: (wsRenewalTime.value < earliestRenewalTime.value - ? wsRenewalTime.value - : earliestRenewalTime.value)); - } } } if (noExpiration) { @@ -790,25 +775,10 @@ class DHTRecordPool with TableDBBackedJson { } return _WatchState( - subkeys: allSubkeys, - expiration: maxExpiration, - count: totalCount, - renewalTime: earliestRenewalTime); - } - - static void _updateWatchRealExpirations(Iterable records, - Timestamp realExpiration, Timestamp renewalTime) { - 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, - renewalTime: renewalTime); - } - } + subkeys: allSubkeys, + expiration: maxExpiration, + count: totalCount, + ); } Future _watchStateChange( @@ -833,9 +803,9 @@ class DHTRecordPool with TableDBBackedJson { // Only try this once, if it doesn't succeed then it can just expire // on its own. try { - final cancelled = await dhtctx.cancelDHTWatch(openedRecordKey); + final stillActive = await dhtctx.cancelDHTWatch(openedRecordKey); - log('cancelDHTWatch: key=$openedRecordKey, cancelled=$cancelled, ' + log('cancelDHTWatch: key=$openedRecordKey, stillActive=$stillActive, ' 'debugNames=${openedRecordInfo.debugNames}'); openedRecordInfo.shared.unionWatchState = null; @@ -858,34 +828,20 @@ class DHTRecordPool with TableDBBackedJson { final subkeys = unionWatchState.subkeys?.toList(); final count = unionWatchState.count; final expiration = unionWatchState.expiration; - final now = veilid.now(); - final realExpiration = await dhtctx.watchDHTValues(openedRecordKey, + final active = await dhtctx.watchDHTValues(openedRecordKey, subkeys: unionWatchState.subkeys?.toList(), count: unionWatchState.count, - expiration: unionWatchState.expiration ?? - (_defaultWatchDurationSecs == null - ? null - : veilid.now().offset(TimestampDuration.fromMillis( - _defaultWatchDurationSecs! * 1000)))); + expiration: unionWatchState.expiration); - final expirationDuration = realExpiration.diff(now); - final renewalTime = now.offset(TimestampDuration( - value: expirationDuration.value * - BigInt.from(_watchRenewalNumerator) ~/ - BigInt.from(_watchRenewalDenominator))); - - log('watchDHTValues: key=$openedRecordKey, subkeys=$subkeys, ' + log('watchDHTValues(active=$active): ' + 'key=$openedRecordKey, subkeys=$subkeys, ' 'count=$count, expiration=$expiration, ' - 'realExpiration=$realExpiration, ' - 'renewalTime=$renewalTime, ' 'debugNames=${openedRecordInfo.debugNames}'); // Update watch states with real expiration - if (realExpiration.value != BigInt.zero) { + if (active) { openedRecordInfo.shared.unionWatchState = unionWatchState; - _updateWatchRealExpirations( - openedRecordInfo.records, realExpiration, renewalTime); openedRecordInfo.shared.needsWatchStateUpdate = false; } } on VeilidAPIExceptionTimeout { @@ -944,22 +900,13 @@ class DHTRecordPool with TableDBBackedJson { /// Ticker to check watch state change requests Future tick() async => _mutex.protect(() async { // See if any opened records need watch state changes - final now = veilid.now(); for (final kv in _opened.entries) { final openedRecordKey = kv.key; final openedRecordInfo = kv.value; - var wantsWatchStateUpdate = + final wantsWatchStateUpdate = openedRecordInfo.shared.needsWatchStateUpdate; - // Check if we have reached renewal time for the watch - if (openedRecordInfo.shared.unionWatchState != null && - openedRecordInfo.shared.unionWatchState!.renewalTime != null && - now.value > - openedRecordInfo.shared.unionWatchState!.renewalTime!.value) { - wantsWatchStateUpdate = true; - } - if (wantsWatchStateUpdate) { // Update union watch state final unionWatchState = diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool_private.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool_private.dart index d1fb5d1..05b93b0 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool_private.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool_private.dart @@ -1,9 +1,5 @@ part of 'dht_record_pool.dart'; -const int? _defaultWatchDurationSecs = null; // 600 -const int _watchRenewalNumerator = 4; -const int _watchRenewalDenominator = 5; - // DHT crypto domain const String _cryptoDomainDHT = 'dht'; @@ -14,21 +10,17 @@ const _sfListen = 'listen'; /// Watch state @immutable class _WatchState extends Equatable { - const _WatchState( - {required this.subkeys, - required this.expiration, - required this.count, - this.realExpiration, - this.renewalTime}); + const _WatchState({ + required this.subkeys, + required this.expiration, + required this.count, + }); final List? subkeys; final Timestamp? expiration; final int? count; - final Timestamp? realExpiration; - final Timestamp? renewalTime; @override - List get props => - [subkeys, expiration, count, realExpiration, renewalTime]; + List get props => [subkeys, expiration, count]; } /// Data shared amongst all DHTRecord instances 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 8101a7a..ccf7d18 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 @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:async_tools/async_tools.dart'; import 'package:collection/collection.dart'; +import '../../../src/veilid_log.dart'; import '../../../veilid_support.dart'; import '../../proto/proto.dart' as proto; 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 49659cd..b0cc41b 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 @@ -383,6 +383,24 @@ class _DHTShortArrayHead { // xxx: free list optimization here? } + /// Truncate index to a particular length + void truncate(int newLength) { + if (newLength >= _index.length) { + return; + } else if (newLength == 0) { + clearIndex(); + return; + } else if (newLength < 0) { + throw StateError('can not truncate to negative length'); + } + + final newIndex = _index.sublist(0, newLength); + final freed = _index.sublist(newLength); + + _index = newIndex; + _free.addAll(freed); + } + /// Validate the head from the DHT is properly formatted /// and calculate the free list from it while we're here List _makeFreeList( 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 index ddfdedc..747a892 100644 --- 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 @@ -60,8 +60,16 @@ class _DHTShortArrayRead implements DHTShortArrayReadOperations { final chunks = Iterable.generate(length) .slices(kMaxDHTConcurrency) - .map((chunk) => chunk - .map((pos) async => get(pos + start, forceRefresh: forceRefresh))); + .map((chunk) => chunk.map((pos) async { + try { + return get(pos + start, forceRefresh: forceRefresh); + // Need some way to debug ParallelWaitError + // ignore: avoid_catches_without_on_clauses + } catch (e, st) { + veilidLoggy.error('$e\n$st\n'); + rethrow; + } + })); for (final chunk in chunks) { final elems = await chunk.wait; 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 index 51950f6..1705bc0 100644 --- 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 @@ -9,6 +9,7 @@ abstract class DHTShortArrayWriteOperations DHTRandomWrite, DHTInsertRemove, DHTAdd, + DHTTruncate, DHTClear {} class _DHTShortArrayWrite extends _DHTShortArrayRead @@ -72,10 +73,16 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead final value = values[i]; final outSeqNum = outSeqNums[i]; dws.add((_) async { - final outValue = await lookup.record.tryWriteBytes(value, - subkey: lookup.recordSubkey, outSeqNum: outSeqNum); - if (outValue != null) { - success = false; + try { + final outValue = await lookup.record.tryWriteBytes(value, + subkey: lookup.recordSubkey, outSeqNum: outSeqNum); + if (outValue != null) { + success = false; + } + // Need some way to debug ParallelWaitError + // ignore: avoid_catches_without_on_clauses + } catch (e, st) { + veilidLoggy.error('$e\n$st\n'); } }); } @@ -142,6 +149,11 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead _head.clearIndex(); } + @override + Future truncate(int newLength) async { + _head.truncate(newLength); + } + @override Future tryWriteItem(int pos, Uint8List newValue, {Output? output}) async { diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/exceptions.dart b/packages/veilid_support/lib/dht_support/src/interfaces/exceptions.dart index b17dbee..134f5fa 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/exceptions.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/exceptions.dart @@ -2,20 +2,32 @@ class DHTExceptionOutdated implements Exception { const DHTExceptionOutdated( [this.cause = 'operation failed due to newer dht value']); final String cause; + + @override + String toString() => 'DHTExceptionOutdated: $cause'; } class DHTExceptionInvalidData implements Exception { - const DHTExceptionInvalidData([this.cause = 'dht data structure is corrupt']); + const DHTExceptionInvalidData(this.cause); final String cause; + + @override + String toString() => 'DHTExceptionInvalidData: $cause'; } class DHTExceptionCancelled implements Exception { const DHTExceptionCancelled([this.cause = 'operation was cancelled']); final String cause; + + @override + String toString() => 'DHTExceptionCancelled: $cause'; } class DHTExceptionNotAvailable implements Exception { const DHTExceptionNotAvailable( [this.cause = 'request could not be completed at this time']); final String cause; + + @override + String toString() => 'DHTExceptionNotAvailable: $cause'; } diff --git a/packages/veilid_support/lib/src/persistent_queue.dart b/packages/veilid_support/lib/src/persistent_queue.dart index 750c48e..939a5b3 100644 --- a/packages/veilid_support/lib/src/persistent_queue.dart +++ b/packages/veilid_support/lib/src/persistent_queue.dart @@ -7,6 +7,7 @@ import 'package:protobuf/protobuf.dart'; import 'config.dart'; import 'table_db.dart'; +import 'veilid_log.dart'; class PersistentQueue with TableDBBackedFromBuffer> { @@ -46,7 +47,7 @@ class PersistentQueue } } - Future _init(_) async { + Future _init(Completer _) async { // Start the processor unawaited(Future.delayed(Duration.zero, () async { await _initWait(); @@ -182,10 +183,28 @@ class PersistentQueue @override IList valueFromBuffer(Uint8List bytes) { - final reader = CodedBufferReader(bytes); var out = IList(); - while (!reader.isAtEnd()) { - out = out.add(_fromBuffer(reader.readBytesAsView())); + try { + final reader = CodedBufferReader(bytes); + while (!reader.isAtEnd()) { + final bytes = reader.readBytesAsView(); + try { + final item = _fromBuffer(bytes); + out = out.add(item); + } on Exception catch (e, st) { + veilidLoggy.debug( + 'Dropping invalid item from persistent queue: $bytes\n' + 'tableName=${tableName()}:tableKeyName=${tableKeyName()}\n', + e, + st); + } + } + } on Exception catch (e, st) { + veilidLoggy.debug( + 'Dropping remainder of invalid persistent queue\n' + 'tableName=${tableName()}:tableKeyName=${tableKeyName()}\n', + e, + st); } return out; } diff --git a/packages/veilid_support/lib/src/table_db_array.dart b/packages/veilid_support/lib/src/table_db_array.dart index c1d54cf..8b59336 100644 --- a/packages/veilid_support/lib/src/table_db_array.dart +++ b/packages/veilid_support/lib/src/table_db_array.dart @@ -9,6 +9,7 @@ import 'package:meta/meta.dart'; import 'package:protobuf/protobuf.dart'; import '../veilid_support.dart'; +import 'veilid_log.dart'; @immutable class TableDBArrayUpdate extends Equatable { @@ -262,7 +263,16 @@ class _TableDBArrayBase { final dws = DelayedWaitSet(); while (batchLen > 0) { final entry = await _getIndexEntry(pos); - dws.add((_) async => (await _loadEntry(entry))!); + dws.add((_) async { + try { + return (await _loadEntry(entry))!; + // Need some way to debug ParallelWaitError + // ignore: avoid_catches_without_on_clauses + } catch (e, st) { + veilidLoggy.error('$e\n$st\n'); + rethrow; + } + }); pos++; batchLen--; } diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index 0c8ca3d..11c2db6 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -726,7 +726,7 @@ packages: path: "../../../veilid/veilid-flutter" relative: true source: path - version: "0.4.3" + version: "0.4.4" vm_service: dependency: transitive description: From 4797184a1a8d8a44a0016b2ca0ec108f4157e152 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 19 Apr 2025 22:21:40 -0400 Subject: [PATCH 239/270] simplify reconciliation --- lib/chat/cubits/chat_component_cubit.dart | 13 +- .../reconciliation/author_input_queue.dart | 282 +++++++++++------- .../reconciliation/author_input_source.dart | 94 +++--- .../message_reconciliation.dart | 234 ++++++++++----- .../cubits/single_contact_messages_cubit.dart | 125 ++++---- .../lib/dht_support/src/dht_log/dht_log.dart | 8 +- .../src/dht_log/dht_log_cubit.dart | 26 +- .../dht_support/src/dht_log/dht_log_read.dart | 42 +-- .../src/dht_log/dht_log_write.dart | 4 +- .../dht_short_array_cubit.dart | 3 - .../dht_short_array/dht_short_array_read.dart | 17 +- .../src/interfaces/dht_random_read.dart | 9 +- 12 files changed, 512 insertions(+), 345 deletions(-) diff --git a/lib/chat/cubits/chat_component_cubit.dart b/lib/chat/cubits/chat_component_cubit.dart index 7ea9e95..6112384 100644 --- a/lib/chat/cubits/chat_component_cubit.dart +++ b/lib/chat/cubits/chat_component_cubit.dart @@ -15,6 +15,7 @@ import '../../account_manager/account_manager.dart'; import '../../contacts/contacts.dart'; import '../../conversation/conversation.dart'; import '../../proto/proto.dart' as proto; +import '../../tools/tools.dart'; import '../models/chat_component_state.dart'; import '../models/message_state.dart'; import '../models/window_state.dart'; @@ -383,13 +384,13 @@ class ChatComponentCubit extends Cubit { if (chatMessage == null) { continue; } - chatMessages.insert(0, chatMessage); if (!tsSet.add(chatMessage.id)) { - // ignore: avoid_print - print('duplicate id found: ${chatMessage.id}:\n' - 'Messages:\n${messagesState.window}\n' - 'ChatMessages:\n$chatMessages'); - assert(false, 'should not have duplicate id'); + log.error('duplicate id found: ${chatMessage.id}' + // '\nMessages:\n${messagesState.window}' + // '\nChatMessages:\n$chatMessages' + ); + } else { + chatMessages.insert(0, chatMessage); } } return currentState.copyWith( diff --git a/lib/chat/cubits/reconciliation/author_input_queue.dart b/lib/chat/cubits/reconciliation/author_input_queue.dart index b15d92c..73dddc6 100644 --- a/lib/chat/cubits/reconciliation/author_input_queue.dart +++ b/lib/chat/cubits/reconciliation/author_input_queue.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math'; import 'package:veilid_support/veilid_support.dart'; import '../../../proto/proto.dart' as proto; @@ -6,106 +7,127 @@ import '../../../proto/proto.dart' as proto; import '../../../tools/tools.dart'; import 'author_input_source.dart'; import 'message_integrity.dart'; -import 'output_position.dart'; class AuthorInputQueue { AuthorInputQueue._({ required TypedKey author, required AuthorInputSource inputSource, - required OutputPosition? outputPosition, + required int inputPosition, + required proto.Message? previousMessage, required void Function(Object, StackTrace?) onError, required MessageIntegrity messageIntegrity, }) : _author = author, _onError = onError, _inputSource = inputSource, - _outputPosition = outputPosition, - _lastMessage = outputPosition?.message.content, + _previousMessage = previousMessage, _messageIntegrity = messageIntegrity, - _currentPosition = inputSource.currentWindow.last; + _inputPosition = inputPosition; static Future create({ required TypedKey author, required AuthorInputSource inputSource, - required OutputPosition? outputPosition, + required proto.Message? previousMessage, required void Function(Object, StackTrace?) onError, }) async { + // Get ending input position + final inputPosition = await inputSource.getTailPosition() - 1; + + // Create an input queue for the input source final queue = AuthorInputQueue._( author: author, inputSource: inputSource, - outputPosition: outputPosition, + inputPosition: inputPosition, + previousMessage: previousMessage, onError: onError, messageIntegrity: await MessageIntegrity.create(author: author)); - if (!await queue._findStartOfWork()) { + + // Rewind the queue's 'inputPosition' to the first unreconciled message + if (!await queue._rewindInputToAfterLastMessage()) { return null; } + return queue; } //////////////////////////////////////////////////////////////////////////// // Public interface - // Check if there are no messages left in this queue to reconcile - bool get isDone => _isDone; + /// Get the input source for this queue + AuthorInputSource get inputSource => _inputSource; - // Get the current message that needs reconciliation - proto.Message? get current => _currentMessage; - - // Get the earliest output position to start inserting - OutputPosition? get outputPosition => _outputPosition; - - // Get the author of this queue + /// Get the author of this queue TypedKey get author => _author; - // Remove a reconciled message and move to the next message - // Returns true if there is more work to do - Future consume() async { - if (_isDone) { + /// Get the current message that needs reconciliation + Future getCurrentMessage() async { + try { + // if we have a current message already, return it + if (_currentMessage != null) { + return _currentMessage; + } + + // Get the window + final currentWindow = await _updateWindow(clampInputPosition: false); + if (currentWindow == null) { + return null; + } + final currentElement = + currentWindow.elements[_inputPosition - currentWindow.firstPosition]; + return _currentMessage = currentElement.value; + // Catch everything so we can avoid ParallelWaitError + // ignore: avoid_catches_without_on_clauses + } catch (e, st) { + log.error('Exception getting current message: $e:\n$st\n'); + _currentMessage = null; + return null; + } + } + + /// Remove a reconciled message and move to the next message + /// Returns true if there is more work to do + Future advance() async { + final currentMessage = await getCurrentMessage(); + if (currentMessage == null) { return false; } - while (true) { - _lastMessage = _currentMessage; - _currentPosition++; + // Move current message to previous + _previousMessage = _currentMessage; + _currentMessage = null; + + while (true) { + // Advance to next position + _inputPosition++; // Get more window if we need to - if (!await _updateWindow()) { - // Window is not available so this queue can't work right now - _isDone = true; + final currentMessage = await getCurrentMessage(); + if (currentMessage == null) { return false; } - final nextMessage = _inputSource.currentWindow - .elements[_currentPosition - _inputSource.currentWindow.first]; - // Drop the 'offline' elements because we don't reconcile - // anything until it has been confirmed to be committed to the DHT - // if (nextMessage.isOffline) { - // continue; - // } - - if (_lastMessage != null) { + if (_previousMessage != null) { // Ensure the timestamp is not moving backward - if (nextMessage.value.timestamp < _lastMessage!.timestamp) { - log.warning('timestamp backward: ${nextMessage.value.timestamp}' - ' < ${_lastMessage!.timestamp}'); + if (currentMessage.timestamp < _previousMessage!.timestamp) { + log.warning('timestamp backward: ${currentMessage.timestamp}' + ' < ${_previousMessage!.timestamp}'); continue; } } // Verify the id chain for the message - final matchId = await _messageIntegrity.generateMessageId(_lastMessage); - if (matchId.compare(nextMessage.value.idBytes) != 0) { - log.warning( - 'id chain invalid: $matchId != ${nextMessage.value.idBytes}'); + final matchId = + await _messageIntegrity.generateMessageId(_previousMessage); + if (matchId.compare(currentMessage.idBytes) != 0) { + log.warning('id chain invalid: $matchId != ${currentMessage.idBytes}'); continue; } // Verify the signature for the message - if (!await _messageIntegrity.verifyMessage(nextMessage.value)) { - log.warning('invalid message signature: ${nextMessage.value}'); + if (!await _messageIntegrity.verifyMessage(currentMessage)) { + log.warning('invalid message signature: $currentMessage'); continue; } - _currentMessage = nextMessage.value; break; } return true; @@ -114,106 +136,166 @@ class AuthorInputQueue { //////////////////////////////////////////////////////////////////////////// // Internal implementation - // Walk backward from the tail of the input queue to find the first - // message newer than our last reconciled message from this author - // Returns false if no work is needed - Future _findStartOfWork() async { + /// Walk backward from the tail of the input queue to find the first + /// message newer than our last reconciled message from this author + /// Returns false if no work is needed + Future _rewindInputToAfterLastMessage() async { // Iterate windows over the inputSource + InputWindow? currentWindow; outer: while (true) { + // Get more window if we need to + currentWindow = await _updateWindow(clampInputPosition: true); + if (currentWindow == null) { + // Window is not available or things are empty so this + // queue can't work right now + return false; + } + // Iterate through current window backward - for (var i = _inputSource.currentWindow.elements.length - 1; - i >= 0 && _currentPosition >= 0; - i--, _currentPosition--) { - final elem = _inputSource.currentWindow.elements[i]; + for (var i = currentWindow.elements.length - 1; + i >= 0 && _inputPosition >= 0; + i--, _inputPosition--) { + final elem = currentWindow.elements[i]; // If we've found an input element that is older or same time as our // last reconciled message for this author, or we find the message // itself then we stop - if (_lastMessage != null) { + if (_previousMessage != null) { if (elem.value.authorUniqueIdBytes - .compare(_lastMessage!.authorUniqueIdBytes) == + .compare(_previousMessage!.authorUniqueIdBytes) == 0 || - elem.value.timestamp <= _lastMessage!.timestamp) { + elem.value.timestamp <= _previousMessage!.timestamp) { break outer; } } } // If we're at the beginning of the inputSource then we stop - if (_currentPosition < 0) { + if (_inputPosition < 0) { break; } - - // Get more window if we need to - if (!await _updateWindow()) { - // Window is not available or things are empty so this - // queue can't work right now - _isDone = true; - return false; - } } - // _currentPosition points to either before the input source starts + // _inputPosition points to either before the input source starts // or the position of the previous element. We still need to set the // _currentMessage to the previous element so consume() can compare // against it if we can. - if (_currentPosition >= 0) { - _currentMessage = _inputSource.currentWindow - .elements[_currentPosition - _inputSource.currentWindow.first].value; + if (_inputPosition >= 0) { + _currentMessage = currentWindow + .elements[_inputPosition - currentWindow.firstPosition].value; } - // After this consume(), the currentPosition and _currentMessage should + // After this advance(), the _inputPosition and _currentMessage should // be equal to the first message to process and the current window to - // process should not be empty - return consume(); + // process should not be empty if there is work to do + return advance(); } - // Slide the window toward the current position and load the batch around it - Future _updateWindow() async { + /// Slide the window toward the current position and load the batch around it + Future _updateWindow({required bool clampInputPosition}) async { + final inputTailPosition = await _inputSource.getTailPosition(); + if (inputTailPosition == 0) { + return null; + } + + // Handle out-of-range input position + if (clampInputPosition) { + _inputPosition = min(max(_inputPosition, 0), inputTailPosition - 1); + } else if (_inputPosition < 0 || _inputPosition >= inputTailPosition) { + return null; + } + // Check if we are still in the window - if (_currentPosition >= _inputSource.currentWindow.first && - _currentPosition <= _inputSource.currentWindow.last) { - return true; + final currentWindow = _currentWindow; + + int firstPosition; + int lastPosition; + if (currentWindow != null) { + firstPosition = currentWindow.firstPosition; + lastPosition = currentWindow.lastPosition; + + // Slide the window if we need to + if (_inputPosition >= firstPosition && _inputPosition <= lastPosition) { + return currentWindow; + } else if (_inputPosition < firstPosition) { + // Slide it backward, current position is now last + firstPosition = max((_inputPosition - _maxWindowLength) + 1, 0); + lastPosition = _inputPosition; + } else if (_inputPosition > lastPosition) { + // Slide it forward, current position is now first + firstPosition = _inputPosition; + lastPosition = + min((_inputPosition + _maxWindowLength) - 1, inputTailPosition - 1); + } + } else { + // need a new window, start with the input position at the end + lastPosition = _inputPosition; + firstPosition = max((_inputPosition - _maxWindowLength) + 1, 0); } // Get another input batch futher back - final avOk = - await _inputSource.updateWindow(_currentPosition, _maxWindowLength); + final avCurrentWindow = await _inputSource.getWindow( + firstPosition, lastPosition - firstPosition + 1); - final asErr = avOk.asError; + final asErr = avCurrentWindow.asError; if (asErr != null) { _onError(asErr.error, asErr.stackTrace); - return false; + _currentWindow = null; + return null; } - final asLoading = avOk.asLoading; + final asLoading = avCurrentWindow.asLoading; if (asLoading != null) { - // xxx: no need to block the cubit here for this - // xxx: might want to switch to a 'busy' state though - // xxx: to let the messages view show a spinner at the bottom - // xxx: while we reconcile... - // emit(const AsyncValue.loading()); - return false; + _currentWindow = null; + return null; } - return avOk.asData!.value; + + final nextWindow = avCurrentWindow.asData!.value; + if (nextWindow == null || nextWindow.length == 0) { + _currentWindow = null; + return null; + } + + // Handle out-of-range input position + // Doing this again because getWindow is allowed to return a smaller + // window than the one requested, possibly due to DHT consistency + // fluctuations and race conditions + if (clampInputPosition) { + _inputPosition = min(max(_inputPosition, nextWindow.firstPosition), + nextWindow.lastPosition); + } else if (_inputPosition < nextWindow.firstPosition || + _inputPosition > nextWindow.lastPosition) { + return null; + } + + return _currentWindow = nextWindow; } //////////////////////////////////////////////////////////////////////////// + /// The author of this messages in the input source final TypedKey _author; + + /// The input source we're pulling messages from final AuthorInputSource _inputSource; - final OutputPosition? _outputPosition; + + /// What to call if an error happens final void Function(Object, StackTrace?) _onError; + + /// The message integrity validator final MessageIntegrity _messageIntegrity; - // The last message we've consumed - proto.Message? _lastMessage; - // The current position in the input log that we are looking at - int _currentPosition; - // The current message we're looking at - proto.Message? _currentMessage; - // If we have reached the end - bool _isDone = false; + /// The last message we reconciled/output + proto.Message? _previousMessage; - // Desired maximum window length + /// The current message we're looking at + proto.Message? _currentMessage; + + /// The current position in the input source that we are looking at + int _inputPosition; + + /// The current input window from the InputSource; + InputWindow? _currentWindow; + + /// Desired maximum window length static const int _maxWindowLength = 256; } diff --git a/lib/chat/cubits/reconciliation/author_input_source.dart b/lib/chat/cubits/reconciliation/author_input_source.dart index 0bd1afb..f974dae 100644 --- a/lib/chat/cubits/reconciliation/author_input_source.dart +++ b/lib/chat/cubits/reconciliation/author_input_source.dart @@ -9,64 +9,68 @@ import '../../../proto/proto.dart' as proto; @immutable class InputWindow { - const InputWindow( - {required this.elements, required this.first, required this.last}); + const InputWindow({required this.elements, required this.firstPosition}) + : lastPosition = firstPosition + elements.length - 1, + isEmpty = elements.length == 0, + length = elements.length; + final IList> elements; - final int first; - final int last; + final int firstPosition; + final int lastPosition; + final bool isEmpty; + final int length; } class AuthorInputSource { - AuthorInputSource.fromCubit( - {required DHTLogStateData cubitState, - required this.cubit}) { - _currentWindow = InputWindow( - elements: cubitState.window, - first: (cubitState.windowTail - cubitState.window.length) % - cubitState.length, - last: (cubitState.windowTail - 1) % cubitState.length); - } + AuthorInputSource.fromDHTLog(DHTLog dhtLog) : _dhtLog = dhtLog; //////////////////////////////////////////////////////////////////////////// - InputWindow get currentWindow => _currentWindow; + Future getTailPosition() async => + _dhtLog.operate((reader) async => reader.length); - Future> updateWindow( - int currentPosition, int windowLength) async => - cubit.operate((reader) async { - // See if we're beyond the input source - if (currentPosition < 0 || currentPosition >= reader.length) { - return const AsyncValue.data(false); - } - - // Slide the window if we need to - var first = _currentWindow.first; - var last = _currentWindow.last; - if (currentPosition < first) { - // Slide it backward, current position is now last - first = max((currentPosition - windowLength) + 1, 0); - last = currentPosition; - } else if (currentPosition > last) { - // Slide it forward, current position is now first - first = currentPosition; - last = min((currentPosition + windowLength) - 1, reader.length - 1); - } else { - return const AsyncValue.data(true); + Future> getWindow( + int startPosition, int windowLength) async => + _dhtLog.operate((reader) async { + // Don't allow negative length + if (windowLength <= 0) { + return const AsyncValue.data(null); } + // Trim if we're beyond input source + var endPosition = startPosition + windowLength - 1; + startPosition = max(startPosition, 0); + endPosition = max(endPosition, 0); // Get another input batch futher back - final nextWindow = await cubit.loadElementsFromReader( - reader, last + 1, (last + 1) - first); - if (nextWindow == null) { - return const AsyncValue.loading(); + try { + Set? offlinePositions; + if (_dhtLog.writer != null) { + offlinePositions = await reader.getOfflinePositions(); + } + + final messages = await reader.getRangeProtobuf( + proto.Message.fromBuffer, startPosition, + length: endPosition - startPosition + 1); + if (messages == null) { + return const AsyncValue.loading(); + } + + final elements = messages.indexed + .map((x) => OnlineElementState( + value: x.$2, + isOffline: offlinePositions?.contains(x.$1 + startPosition) ?? + false)) + .toIList(); + + final window = + InputWindow(elements: elements, firstPosition: startPosition); + + return AsyncValue.data(window); + } on Exception catch (e, st) { + return AsyncValue.error(e, st); } - _currentWindow = - InputWindow(elements: nextWindow, first: first, last: last); - return const AsyncValue.data(true); }); //////////////////////////////////////////////////////////////////////////// - final DHTLogCubit cubit; - - late InputWindow _currentWindow; + final DHTLog _dhtLog; } diff --git a/lib/chat/cubits/reconciliation/message_reconciliation.dart b/lib/chat/cubits/reconciliation/message_reconciliation.dart index 9b183b5..683b46d 100644 --- a/lib/chat/cubits/reconciliation/message_reconciliation.dart +++ b/lib/chat/cubits/reconciliation/message_reconciliation.dart @@ -20,66 +20,113 @@ class MessageReconciliation { //////////////////////////////////////////////////////////////////////////// - void reconcileMessages( - TypedKey author, - DHTLogStateData inputMessagesCubitState, - DHTLogCubit inputMessagesCubit) { - if (inputMessagesCubitState.window.isEmpty) { - return; - } + void addInputSourceFromDHTLog(TypedKey author, DHTLog inputMessagesDHTLog) { + _inputSources[author] = AuthorInputSource.fromDHTLog(inputMessagesDHTLog); + } - _inputSources[author] = AuthorInputSource.fromCubit( - cubitState: inputMessagesCubitState, cubit: inputMessagesCubit); + void reconcileMessages(TypedKey? author) { + // xxx: can we use 'author' here to optimize _updateAuthorInputQueues? singleFuture(this, onError: _onError, () async { - // Take entire list of input sources we have currently and process them - final inputSources = _inputSources; - _inputSources = {}; - - final inputFuts = >[]; - for (final kv in inputSources.entries) { - final author = kv.key; - final inputSource = kv.value; - inputFuts - .add(_enqueueAuthorInput(author: author, inputSource: inputSource)); - } - final inputQueues = await inputFuts.wait; - - // Make this safe to cast by removing inputs that were rejected or empty - inputQueues.removeNulls(); + // Update queues + final activeInputQueues = await _updateAuthorInputQueues(); // Process all input queues together await _outputCubit .operate((reconciledArray) async => _reconcileInputQueues( reconciledArray: reconciledArray, - inputQueues: inputQueues.cast(), + activeInputQueues: activeInputQueues, )); }); } //////////////////////////////////////////////////////////////////////////// - // Set up a single author's message reconciliation - Future _enqueueAuthorInput( - {required TypedKey author, - required AuthorInputSource inputSource}) async { - try { - // Get the position of our most recent reconciled message from this author - final outputPosition = await _findLastOutputPosition(author: author); + // Prepare author input queues by removing dead ones and adding new ones + // Queues that are empty are not added until they have something in them + // Active input queues with a current message are returned in a list + Future> _updateAuthorInputQueues() async { + // Remove any dead input queues + final deadQueues = []; + for (final author in _inputQueues.keys) { + if (!_inputSources.containsKey(author)) { + deadQueues.add(author); + } + } + for (final author in deadQueues) { + _inputQueues.remove(author); + _outputPositions.remove(author); + } - // Find oldest message we have not yet reconciled - final inputQueue = await AuthorInputQueue.create( - author: author, - inputSource: inputSource, - outputPosition: outputPosition, - onError: _onError, - ); - return inputQueue; - // Catch everything so we can avoid ParallelWaitError - // ignore: avoid_catches_without_on_clauses - } catch (e, st) { - log.error('Exception enqueing author input: $e:\n$st\n'); - return null; + await _outputCubit.operate((outputArray) async { + final dws = DelayedWaitSet(); + + for (final kv in _inputSources.entries) { + final author = kv.key; + final inputSource = kv.value; + + final iqExisting = _inputQueues[author]; + if (iqExisting == null || iqExisting.inputSource != inputSource) { + dws.add((_) async { + try { + await _enqueueAuthorInput( + author: author, + inputSource: inputSource, + outputArray: outputArray); + // Catch everything so we can avoid ParallelWaitError + // ignore: avoid_catches_without_on_clauses + } catch (e, st) { + log.error('Exception updating author input queue: $e:\n$st\n'); + _inputQueues.remove(author); + _outputPositions.remove(author); + } + }); + } + } + + await dws(); + }); + + // Get the active input queues + final activeInputQueues = await _inputQueues.entries + .map((entry) async { + if (await entry.value.getCurrentMessage() != null) { + return entry.value; + } else { + return null; + } + }) + .toList() + .wait + ..removeNulls(); + + return activeInputQueues.cast(); + } + + // Set up a single author's message reconciliation + Future _enqueueAuthorInput( + {required TypedKey author, + required AuthorInputSource inputSource, + required TableDBArrayProtobuf + outputArray}) async { + // Get the position of our most recent reconciled message from this author + final outputPosition = + await _findLastOutputPosition(author: author, outputArray: outputArray); + + // Find oldest message we have not yet reconciled + final inputQueue = await AuthorInputQueue.create( + author: author, + inputSource: inputSource, + previousMessage: outputPosition?.message.content, + onError: _onError, + ); + + if (inputQueue != null) { + _inputQueues[author] = inputQueue; + _outputPositions[author] = outputPosition; + } else { + _inputQueues.remove(author); + _outputPositions.remove(author); } } @@ -87,36 +134,38 @@ class MessageReconciliation { // XXX: For a group chat, this should find when the author // was added to the membership so we don't just go back in time forever Future _findLastOutputPosition( - {required TypedKey author}) async => - _outputCubit.operate((arr) async { - var pos = arr.length - 1; - while (pos >= 0) { - final message = await arr.get(pos); - if (message.content.author.toVeilid() == author) { - return OutputPosition(message, pos); - } - pos--; - } - return null; - }); + {required TypedKey author, + required TableDBArrayProtobuf + outputArray}) async { + var pos = outputArray.length - 1; + while (pos >= 0) { + final message = await outputArray.get(pos); + if (message.content.author.toVeilid() == author) { + return OutputPosition(message, pos); + } + pos--; + } + return null; + } // Process a list of author input queues and insert their messages // into the output array, performing validation steps along the way Future _reconcileInputQueues({ required TableDBArrayProtobuf reconciledArray, - required List inputQueues, + required List activeInputQueues, }) async { - // Ensure queues all have something to do - inputQueues.removeWhere((q) => q.isDone); - if (inputQueues.isEmpty) { + // Ensure we have active queues to process + if (activeInputQueues.isEmpty) { return; } // Sort queues from earliest to latest and then by author // to ensure a deterministic insert order - inputQueues.sort((a, b) { - final acmp = a.outputPosition?.pos ?? -1; - final bcmp = b.outputPosition?.pos ?? -1; + activeInputQueues.sort((a, b) { + final aout = _outputPositions[a.author]; + final bout = _outputPositions[b.author]; + final acmp = aout?.pos ?? -1; + final bcmp = bout?.pos ?? -1; if (acmp == bcmp) { return a.author.toString().compareTo(b.author.toString()); } @@ -124,21 +173,28 @@ class MessageReconciliation { }); // Start at the earliest position we know about in all the queues - var currentOutputPosition = inputQueues.first.outputPosition; + var currentOutputPosition = + _outputPositions[activeInputQueues.first.author]; final toInsert = SortedList(proto.MessageExt.compareTimestamp); - while (inputQueues.isNotEmpty) { + while (activeInputQueues.isNotEmpty) { // Get up to '_maxReconcileChunk' of the items from the queues // that we can insert at this location bool added; do { added = false; - var someQueueEmpty = false; - for (final inputQueue in inputQueues) { - final inputCurrent = inputQueue.current!; + + final emptyQueues = {}; + for (final inputQueue in activeInputQueues) { + final inputCurrent = await inputQueue.getCurrentMessage(); + if (inputCurrent == null) { + log.error('Active input queue did not have a current message: ' + '${inputQueue.author}'); + continue; + } if (currentOutputPosition == null || inputCurrent.timestamp < currentOutputPosition.message.content.timestamp) { @@ -146,16 +202,14 @@ class MessageReconciliation { added = true; // Advance this queue - if (!await inputQueue.consume()) { - // Queue is empty now, run a queue purge - someQueueEmpty = true; + if (!await inputQueue.advance()) { + // Mark queue as empty for removal + emptyQueues.add(inputQueue); } } } - // Remove empty queues now that we're done iterating - if (someQueueEmpty) { - inputQueues.removeWhere((q) => q.isDone); - } + // Remove finished queues now that we're done iterating + activeInputQueues.removeWhere(emptyQueues.contains); if (toInsert.length >= _maxReconcileChunk) { break; @@ -173,9 +227,27 @@ class MessageReconciliation { ..content = message) .toList(); - await reconciledArray.insertAll( - currentOutputPosition?.pos ?? reconciledArray.length, - reconciledInserts); + // Figure out where to insert the reconciled messages + final insertPos = currentOutputPosition?.pos ?? reconciledArray.length; + + // Insert them all at once + await reconciledArray.insertAll(insertPos, reconciledInserts); + + // Update output positions for input queues + final updatePositions = _outputPositions.keys.toSet(); + var outputPos = insertPos + reconciledInserts.length; + for (final inserted in reconciledInserts.reversed) { + if (updatePositions.isEmpty) { + // Last seen positions already recorded for each active author + break; + } + outputPos--; + final author = inserted.content.author.toVeilid(); + if (updatePositions.contains(author)) { + _outputPositions[author] = OutputPosition(inserted, outputPos); + updatePositions.remove(author); + } + } toInsert.clear(); } else { @@ -195,7 +267,9 @@ class MessageReconciliation { //////////////////////////////////////////////////////////////////////////// - Map _inputSources = {}; + final Map _inputSources = {}; + final Map _inputQueues = {}; + final Map _outputPositions = {}; final TableDBArrayProtobufCubit _outputCubit; final void Function(Object, StackTrace?) _onError; diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 559eae2..66032f6 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -77,8 +77,8 @@ class SingleContactMessagesCubit extends Cubit { await _sentSubscription?.cancel(); await _rcvdSubscription?.cancel(); await _reconciledSubscription?.cancel(); - await _sentMessagesCubit?.close(); - await _rcvdMessagesCubit?.close(); + await _sentMessagesDHTLog?.close(); + await _rcvdMessagesDHTLog?.close(); await _reconciledMessagesCubit?.close(); // If the local conversation record is gone, then delete the reconciled @@ -111,10 +111,10 @@ class SingleContactMessagesCubit extends Cubit { await _initReconciledMessagesCubit(); // Local messages key - await _initSentMessagesCubit(); + await _initSentMessagesDHTLog(); // Remote messages key - await _initRcvdMessagesCubit(); + await _initRcvdMessagesDHTLog(); // Command execution background process _commandRunnerFut = Future.delayed(Duration.zero, _commandRunner); @@ -129,39 +129,40 @@ class SingleContactMessagesCubit extends Cubit { } // Open local messages key - Future _initSentMessagesCubit() async { + Future _initSentMessagesDHTLog() async { final writer = _accountInfo.identityWriter; - _sentMessagesCubit = DHTLogCubit( - open: () async => DHTLog.openWrite(_localMessagesRecordKey, writer, + final sentMessagesDHTLog = + await DHTLog.openWrite(_localMessagesRecordKey, writer, debugName: 'SingleContactMessagesCubit::_initSentMessagesCubit::' 'SentMessages', parent: _localConversationRecordKey, - crypto: _conversationCrypto), - decodeElement: proto.Message.fromBuffer); - _sentSubscription = - _sentMessagesCubit!.stream.listen(_updateSentMessagesState); - _updateSentMessagesState(_sentMessagesCubit!.state); + crypto: _conversationCrypto); + _sentSubscription = await sentMessagesDHTLog.listen(_updateSentMessages); + + _sentMessagesDHTLog = sentMessagesDHTLog; + _reconciliation.addInputSourceFromDHTLog( + _accountInfo.identityTypedPublicKey, sentMessagesDHTLog); } // Open remote messages key - Future _initRcvdMessagesCubit() async { + Future _initRcvdMessagesDHTLog() async { // Don't bother if we don't have a remote messages record key yet if (_remoteMessagesRecordKey == null) { return; } // Open new cubit if one is desired - _rcvdMessagesCubit = DHTLogCubit( - open: () async => DHTLog.openRead(_remoteMessagesRecordKey!, - debugName: 'SingleContactMessagesCubit::_initRcvdMessagesCubit::' - 'RcvdMessages', - parent: _remoteConversationRecordKey, - crypto: _conversationCrypto), - decodeElement: proto.Message.fromBuffer); - _rcvdSubscription = - _rcvdMessagesCubit!.stream.listen(_updateRcvdMessagesState); - _updateRcvdMessagesState(_rcvdMessagesCubit!.state); + final rcvdMessagesDHTLog = await DHTLog.openRead(_remoteMessagesRecordKey!, + debugName: 'SingleContactMessagesCubit::_initRcvdMessagesCubit::' + 'RcvdMessages', + parent: _remoteConversationRecordKey, + crypto: _conversationCrypto); + _rcvdSubscription = await rcvdMessagesDHTLog.listen(_updateRcvdMessages); + + _rcvdMessagesDHTLog = rcvdMessagesDHTLog; + _reconciliation.addInputSourceFromDHTLog( + _remoteIdentityPublicKey, rcvdMessagesDHTLog); } Future updateRemoteMessagesRecordKey( @@ -175,17 +176,17 @@ class SingleContactMessagesCubit extends Cubit { return; } - // Close existing cubit if we have one - final rcvdMessagesCubit = _rcvdMessagesCubit; - _rcvdMessagesCubit = null; + // Close existing DHTLog if we have one + final rcvdMessagesDHTLog = _rcvdMessagesDHTLog; + _rcvdMessagesDHTLog = null; _remoteMessagesRecordKey = null; await _rcvdSubscription?.cancel(); _rcvdSubscription = null; - await rcvdMessagesCubit?.close(); + await rcvdMessagesDHTLog?.close(); - // Init the new cubit if we should + // Init the new DHTLog if we should _remoteMessagesRecordKey = remoteMessagesRecordKey; - await _initRcvdMessagesCubit(); + await _initRcvdMessagesDHTLog(); }); } @@ -275,30 +276,15 @@ class SingleContactMessagesCubit extends Cubit { //////////////////////////////////////////////////////////////////////////// // Internal implementation - // Called when the sent messages cubit gets a change + // Called when the sent messages DHTLog gets a change // This will re-render when messages are sent from another machine - void _updateSentMessagesState(DHTLogBusyState avmessages) { - final sentMessages = avmessages.state.asData?.value; - if (sentMessages == null) { - return; - } - - _reconciliation.reconcileMessages( - _accountInfo.identityTypedPublicKey, sentMessages, _sentMessagesCubit!); - - // Update the view - _renderState(); + void _updateSentMessages(DHTLogUpdate upd) { + _reconciliation.reconcileMessages(_accountInfo.identityTypedPublicKey); } - // Called when the received messages cubit gets a change - void _updateRcvdMessagesState(DHTLogBusyState avmessages) { - final rcvdMessages = avmessages.state.asData?.value; - if (rcvdMessages == null) { - return; - } - - _reconciliation.reconcileMessages( - _remoteIdentityPublicKey, rcvdMessages, _rcvdMessagesCubit!); + // Called when the received messages DHTLog gets a change + void _updateRcvdMessages(DHTLogUpdate upd) { + _reconciliation.reconcileMessages(_remoteIdentityPublicKey); } // Called when the reconciled messages window gets a change @@ -327,7 +313,7 @@ class SingleContactMessagesCubit extends Cubit { // _renderState(); try { - await _sentMessagesCubit!.operateAppendEventual((writer) async { + await _sentMessagesDHTLog!.operateAppendEventual((writer) async { // Get the previous message if we have one var previousMessage = writer.length == 0 ? null @@ -357,16 +343,17 @@ class SingleContactMessagesCubit extends Cubit { // Produce a state for this cubit from the input cubits and queues void _renderState() { - // Get all reconciled messages + // Get all reconciled messages in the cubit window final reconciledMessages = _reconciledMessagesCubit?.state.state.asData?.value; - // Get all sent messages - final sentMessages = _sentMessagesCubit?.state.state.asData?.value; + + // Get all sent messages that are still offline + //final sentMessages = _sentMessagesDHTLog. //Get all items in the unsent queue //final unsentMessages = _unsentMessagesQueue.queue; // If we aren't ready to render a state, say we're loading - if (reconciledMessages == null || sentMessages == null) { + if (reconciledMessages == null) { emit(const AsyncLoading()); return; } @@ -377,11 +364,11 @@ class SingleContactMessagesCubit extends Cubit { // keyMapper: (x) => x.content.authorUniqueIdString, // values: reconciledMessages.windowElements, // ); - final sentMessagesMap = - IMap>.fromValues( - keyMapper: (x) => x.value.authorUniqueIdString, - values: sentMessages.window, - ); + // final sentMessagesMap = + // IMap>.fromValues( + // keyMapper: (x) => x.value.authorUniqueIdString, + // values: sentMessages.window, + // ); // final unsentMessagesMap = IMap.fromValues( // keyMapper: (x) => x.authorUniqueIdString, // values: unsentMessages, @@ -393,10 +380,12 @@ class SingleContactMessagesCubit extends Cubit { final isLocal = m.content.author.toVeilid() == _accountInfo.identityTypedPublicKey; final reconciledTimestamp = Timestamp.fromInt64(m.reconciledTime); - final sm = - isLocal ? sentMessagesMap[m.content.authorUniqueIdString] : null; - final sent = isLocal && sm != null; - final sentOffline = isLocal && sm != null && sm.isOffline; + //final sm = + //isLocal ? sentMessagesMap[m.content.authorUniqueIdString] : null; + //final sent = isLocal && sm != null; + //final sentOffline = isLocal && sm != null && sm.isOffline; + final sent = isLocal; + final sentOffline = false; // renderedElements.add(RenderStateElement( message: m.content, @@ -491,16 +480,16 @@ class SingleContactMessagesCubit extends Cubit { late final VeilidCrypto _conversationCrypto; late final MessageIntegrity _senderMessageIntegrity; - DHTLogCubit? _sentMessagesCubit; - DHTLogCubit? _rcvdMessagesCubit; + DHTLog? _sentMessagesDHTLog; + DHTLog? _rcvdMessagesDHTLog; TableDBArrayProtobufCubit? _reconciledMessagesCubit; late final MessageReconciliation _reconciliation; late final PersistentQueue _unsentMessagesQueue; // IList _sendingMessages = const IList.empty(); - StreamSubscription>? _sentSubscription; - StreamSubscription>? _rcvdSubscription; + StreamSubscription? _sentSubscription; + StreamSubscription? _rcvdSubscription; StreamSubscription>? _reconciledSubscription; final StreamController Function()> _commandController; diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart index 1d3fb89..2687bdc 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart @@ -213,7 +213,7 @@ class DHTLog implements DHTDeleteable { /// Runs a closure allowing read-only access to the log Future operate(Future Function(DHTLogReadOperations) closure) async { if (!isOpen) { - throw StateError('log is not open"'); + throw StateError('log is not open'); } return _spine.operate((spine) async { @@ -230,7 +230,7 @@ class DHTLog implements DHTDeleteable { Future operateAppend( Future Function(DHTLogWriteOperations) closure) async { if (!isOpen) { - throw StateError('log is not open"'); + throw StateError('log is not open'); } return _spine.operateAppend((spine) async { @@ -249,7 +249,7 @@ class DHTLog implements DHTDeleteable { Future Function(DHTLogWriteOperations) closure, {Duration? timeout}) async { if (!isOpen) { - throw StateError('log is not open"'); + throw StateError('log is not open'); } return _spine.operateAppendEventual((spine) async { @@ -264,7 +264,7 @@ class DHTLog implements DHTDeleteable { void Function(DHTLogUpdate) onChanged, ) { if (!isOpen) { - throw StateError('log is not open"'); + throw StateError('log is not open'); } return _listenMutex.protect(() async { diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart index 492312f..a7884f9 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart @@ -112,33 +112,34 @@ class DHTLogCubit extends Cubit> Future _refreshInner(void Function(AsyncValue>) emit, {bool forceRefresh = false}) async { late final int length; - final window = await _log.operate((reader) async { + final windowElements = await _log.operate((reader) async { length = reader.length; - return loadElementsFromReader(reader, _windowTail, _windowSize); + return _loadElementsFromReader(reader, _windowTail, _windowSize); }); - if (window == null) { + if (windowElements == null) { setWantsRefresh(); return; } + emit(AsyncValue.data(DHTLogStateData( length: length, - window: window, - windowTail: _windowTail, - windowSize: _windowSize, + window: windowElements.$2, + windowTail: windowElements.$1 + windowElements.$2.length, + windowSize: windowElements.$2.length, follow: _follow))); setRefreshed(); } // Tail is one past the last element to load - Future>?> loadElementsFromReader( + Future<(int, IList>)?> _loadElementsFromReader( DHTLogReadOperations reader, int tail, int count, {bool forceRefresh = false}) async { final length = reader.length; - if (length == 0) { - return const IList.empty(); - } final end = ((tail - 1) % length) + 1; final start = (count < end) ? end - count : 0; + if (length == 0) { + return (start, IList>.empty()); + } // If this is writeable get the offline positions Set? offlinePositions; @@ -154,8 +155,11 @@ class DHTLogCubit extends Cubit> value: _decodeElement(x.$2), isOffline: offlinePositions?.contains(x.$1) ?? false)) .toIList(); + if (allItems == null) { + return null; + } - return allItems; + return (start, allItems); } void _update(DHTLogUpdate upd) { diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart index d8634c6..3ebb2b8 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_read.dart @@ -21,8 +21,14 @@ class _DHTLogRead implements DHTLogReadOperations { return null; } - return lookup.scope((sa) => - sa.operate((read) => read.get(lookup.pos, forceRefresh: forceRefresh))); + return lookup.scope((sa) => sa.operate((read) async { + if (lookup.pos >= read.length) { + veilidLoggy.error('DHTLog shortarray read @ ${lookup.pos}' + ' >= length ${read.length}'); + return null; + } + return read.get(lookup.pos, forceRefresh: forceRefresh); + })); } (int, int) _clampStartLen(int start, int? len) { @@ -49,7 +55,7 @@ class _DHTLogRead implements DHTLogReadOperations { .slices(kMaxDHTConcurrency) .map((chunk) => chunk.map((pos) async { try { - return get(pos + start, forceRefresh: forceRefresh); + return await get(pos + start, forceRefresh: forceRefresh); // Need some way to debug ParallelWaitError // ignore: avoid_catches_without_on_clauses } catch (e, st) { @@ -59,36 +65,42 @@ class _DHTLogRead implements DHTLogReadOperations { })); for (final chunk in chunks) { - final elems = await chunk.wait; + var elems = await chunk.wait; - // If any element was unavailable, return null - if (elems.contains(null)) { - return null; + // Return only the first contiguous range, anything else is garbage + // due to a representational error in the head or shortarray legnth + final nullPos = elems.indexOf(null); + if (nullPos != -1) { + elems = elems.sublist(0, nullPos); } + out.addAll(elems.cast()); + + if (nullPos != -1) { + break; + } } return out; } @override - Future?> getOfflinePositions() async { + Future> getOfflinePositions() async { final positionOffline = {}; // Iterate positions backward from most recent for (var pos = _spine.length - 1; pos >= 0; pos--) { + // Get position final lookup = await _spine.lookupPosition(pos); + // If position doesn't exist then it definitely wasn't written to offline if (lookup == null) { - return null; + continue; } // Check each segment for offline positions var foundOffline = false; - final success = await lookup.scope((sa) => sa.operate((read) async { + await lookup.scope((sa) => sa.operate((read) async { final segmentOffline = await read.getOfflinePositions(); - if (segmentOffline == null) { - return false; - } // For each shortarray segment go through their segment positions // in reverse order and see if they are offline @@ -102,11 +114,7 @@ class _DHTLogRead implements DHTLogReadOperations { foundOffline = true; } } - return true; })); - if (!success) { - return null; - } // If we found nothing offline in this segment then we can stop if (!foundOffline) { break; diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart index 8d34280..397a1dc 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart @@ -107,9 +107,7 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { await write.clear(); } else if (lookup.pos != write.length) { // We should always be appending at the length - throw DHTExceptionInvalidData( - '_DHTLogWrite::add lookup.pos=${lookup.pos} ' - 'write.length=${write.length}'); + await write.truncate(lookup.pos); } return write.add(value); })); 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 6ff6d95..246a990 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 @@ -62,9 +62,6 @@ class DHTShortArrayCubit extends Cubit> Set? offlinePositions; if (_shortArray.writer != null) { offlinePositions = await reader.getOfflinePositions(); - if (offlinePositions == null) { - return null; - } } // Get the items 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 index 747a892..eeb9648 100644 --- 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 @@ -62,7 +62,7 @@ class _DHTShortArrayRead implements DHTShortArrayReadOperations { .slices(kMaxDHTConcurrency) .map((chunk) => chunk.map((pos) async { try { - return get(pos + start, forceRefresh: forceRefresh); + return await get(pos + start, forceRefresh: forceRefresh); // Need some way to debug ParallelWaitError // ignore: avoid_catches_without_on_clauses } catch (e, st) { @@ -72,13 +72,20 @@ class _DHTShortArrayRead implements DHTShortArrayReadOperations { })); for (final chunk in chunks) { - final elems = await chunk.wait; + var elems = await chunk.wait; - // If any element was unavailable, return null - if (elems.contains(null)) { - return null; + // Return only the first contiguous range, anything else is garbage + // due to a representational error in the head or shortarray legnth + final nullPos = elems.indexOf(null); + if (nullPos != -1) { + elems = elems.sublist(0, nullPos); } + out.addAll(elems.cast()); + + if (nullPos != -1) { + break; + } } return out; diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart index 0547332..d361757 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_read.dart @@ -14,19 +14,22 @@ abstract class DHTRandomRead { /// is specified, the network will always be checked for newer values /// rather than returning the existing locally stored copy of the elements. /// Throws an IndexError if the 'pos' is not within the length - /// of the container. + /// of the container. May return null if the item is not available at this + /// time. Future get(int pos, {bool forceRefresh = false}); /// Return a list of a range of items in the DHTArray. If 'forceRefresh' /// is specified, the network will always be checked for newer values /// rather than returning the existing locally stored copy of the elements. /// Throws an IndexError if either 'start' or '(start+length)' is not within - /// the length of the container. + /// the length of the container. May return fewer items than the length + /// expected if the requested items are not available, but will always + /// return a contiguous range starting at 'start'. Future?> getRange(int start, {int? length, bool forceRefresh = false}); /// Get a list of the positions that were written offline and not flushed yet - Future?> getOfflinePositions(); + Future> getOfflinePositions(); } extension DHTRandomReadExt on DHTRandomRead { From 3fabf602cd93d4adc59d01d7b2586916ff9a5dc0 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 21 Apr 2025 13:39:57 -0400 Subject: [PATCH 240/270] fix developer pane crash --- lib/veilid_processor/views/developer.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/veilid_processor/views/developer.dart b/lib/veilid_processor/views/developer.dart index 08463fb..0bfcc0a 100644 --- a/lib/veilid_processor/views/developer.dart +++ b/lib/veilid_processor/views/developer.dart @@ -65,8 +65,9 @@ class _DeveloperPageState extends State { } void _debugOut(String out) { + final sanitizedOut = out.replaceAll('\uFFFD', ''); final pen = AnsiPen()..cyan(bold: true); - final colorOut = pen(out); + final colorOut = pen(sanitizedOut); debugPrint(colorOut); globalDebugTerminal.write(colorOut.replaceAll('\n', '\r\n')); } From 933a22122acda8aa233063850bd2f118864cce0d Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 21 Apr 2025 15:23:36 -0400 Subject: [PATCH 241/270] attempt to deal with focus problem in developer page --- lib/veilid_processor/views/developer.dart | 29 +++++++++++++---------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/lib/veilid_processor/views/developer.dart b/lib/veilid_processor/views/developer.dart index 0bfcc0a..e329d47 100644 --- a/lib/veilid_processor/views/developer.dart +++ b/lib/veilid_processor/views/developer.dart @@ -21,7 +21,7 @@ import '../../tools/tools.dart'; import 'history_text_editing_controller.dart'; final globalDebugTerminal = Terminal( - maxLines: 50000, + maxLines: 10000, ); const kDefaultTerminalStyle = TerminalStyle( @@ -187,6 +187,15 @@ class _DeveloperPageState extends State { } } + Future _onSubmitCommand(String debugCommand) async { + final ok = await _sendDebugCommand(debugCommand); + if (ok) { + setState(() { + _historyController.submit(debugCommand); + }); + } + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -282,16 +291,17 @@ class _DeveloperPageState extends State { textStyle: kDefaultTerminalStyle, controller: _terminalController, keyboardType: TextInputType.none, - //autofocus: true, backgroundOpacity: _showEllet ? 0.75 : 1.0, onSecondaryTapDown: (details, offset) async { await copySelection(context); }) ]).expanded(), - TextField( + TextFormField( enabled: !_busy, + autofocus: true, controller: _historyController.controller, focusNode: _historyController.focusNode, + textInputAction: TextInputAction.send, onTapOutside: (event) { FocusManager.instance.primaryFocus?.unfocus(); }, @@ -323,7 +333,7 @@ class _DeveloperPageState extends State { final debugCommand = _historyController.controller.text; _historyController.controller.clear(); - await _sendDebugCommand(debugCommand); + await _onSubmitCommand(debugCommand); }, )), onChanged: (_) { @@ -334,17 +344,12 @@ class _DeveloperPageState extends State { _historyController.controller.clearComposing(); // don't give up focus though }, - onSubmitted: (debugCommand) async { + onFieldSubmitted: (debugCommand) async { if (debugCommand.isEmpty) { return; } - - final ok = await _sendDebugCommand(debugCommand); - if (ok) { - setState(() { - _historyController.submit(debugCommand); - }); - } + await _onSubmitCommand(debugCommand); + _historyController.focusNode.requestFocus(); }, ).paddingAll(4) ]))); From 8194a79ce4e2431968c13c21beb78ce9de23c548 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 25 Apr 2025 17:13:58 -0400 Subject: [PATCH 242/270] update handling of nulls in inspect results --- .../src/dht_record/dht_record.dart | 4 +-- .../src/dht_record/dht_record_pool.dart | 4 +-- .../src/dht_record/extensions.dart | 20 ++++++++----- .../dht_short_array/dht_short_array_head.dart | 30 +++++++++---------- .../dht_short_array_write.dart | 4 +-- 5 files changed, 31 insertions(+), 31 deletions(-) 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 4e632fc..d632b58 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 @@ -511,7 +511,7 @@ class DHTRecord implements DHTDeleteable { key, subkeys: [ValueSubkeyRange.single(subkey)], ); - return rr.localSeqs.firstOrNull ?? emptySeq; + return rr.localSeqs.firstOrNull; } void _addValueChange( @@ -566,6 +566,4 @@ class DHTRecord implements DHTDeleteable { int _openCount; StreamController? _watchController; _WatchState? _watchState; - - static const int emptySeq = 0xFFFFFFFF; } 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 e3c9abe..3ee9adc 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 @@ -875,7 +875,7 @@ class DHTRecordPool with TableDBBackedJson { if (fsc == null) { return null; } - final newerSubkeys = currentReport.newerSubkeys; + final newerSubkeys = currentReport.newerOnlineSubkeys; final valueData = await dhtctx.getDHTValue(openedRecordKey, fsc.subkey, forceRefresh: true); @@ -887,7 +887,7 @@ class DHTRecordPool with TableDBBackedJson { log('inspect returned a newer seq than get: ${valueData.seq} < $fsc'); } - if (valueData.seq > fsc.oldSeq && valueData.seq != DHTRecord.emptySeq) { + if (fsc.oldSeq == null || valueData.seq > fsc.oldSeq!) { processRemoteValueChange(VeilidUpdateValueChange( key: openedRecordKey, subkeys: newerSubkeys, diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/extensions.dart b/packages/veilid_support/lib/dht_support/src/dht_record/extensions.dart index e62403e..b0da9e3 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/extensions.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/extensions.dart @@ -1,15 +1,14 @@ import 'package:veilid/veilid.dart'; -import 'dht_record_pool.dart'; class DHTSeqChange { const DHTSeqChange(this.subkey, this.oldSeq, this.newSeq); final int subkey; - final int oldSeq; + final int? oldSeq; final int newSeq; } extension DHTReportReportExt on DHTRecordReport { - List get newerSubkeys { + List get newerOnlineSubkeys { if (networkSeqs.isEmpty || localSeqs.isEmpty || subkeys.isEmpty) { return []; } @@ -19,8 +18,10 @@ extension DHTReportReportExt on DHTRecordReport { var i = 0; for (final skr in subkeys) { for (var sk = skr.low; sk <= skr.high; sk++) { - if (networkSeqs[i] > localSeqs[i] && - networkSeqs[i] != DHTRecord.emptySeq) { + final nseq = networkSeqs[i]; + final lseq = localSeqs[i]; + + if (nseq != null && (lseq == null || nseq > lseq)) { if (currentSubkeys.isNotEmpty && currentSubkeys.last.high == (sk - 1)) { currentSubkeys.add(ValueSubkeyRange( @@ -29,6 +30,7 @@ extension DHTReportReportExt on DHTRecordReport { currentSubkeys.add(ValueSubkeyRange.single(sk)); } } + i++; } } @@ -44,9 +46,11 @@ extension DHTReportReportExt on DHTRecordReport { var i = 0; for (final skr in subkeys) { for (var sk = skr.low; sk <= skr.high; sk++) { - if (networkSeqs[i] > localSeqs[i] && - networkSeqs[i] != DHTRecord.emptySeq) { - return DHTSeqChange(sk, localSeqs[i], networkSeqs[i]); + final nseq = networkSeqs[i]; + final lseq = localSeqs[i]; + + if (nseq != null && (lseq == null || nseq > lseq)) { + return DHTSeqChange(sk, lseq, nseq); } i++; } 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 b0cc41b..66b5baa 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 @@ -5,7 +5,7 @@ class DHTShortArrayHeadLookup { {required this.record, required this.recordSubkey, required this.seq}); final DHTRecord record; final int recordSubkey; - final int seq; + final int? seq; } class _DHTShortArrayHead { @@ -41,7 +41,7 @@ class _DHTShortArrayHead { final head = proto.DHTShortArray(); head.keys.addAll(_linkedRecords.map((lr) => lr.key.toProto())); head.index = List.of(_index); - head.seqs.addAll(_seqs); + head.seqs.addAll(_seqs.map((x) => x ?? 0xFFFFFFFF)); // Do not serialize free list, it gets recreated // Do not serialize local seqs, they are only locally relevant return head; @@ -70,10 +70,7 @@ class _DHTShortArrayHead { Future delete() => _headMutex.protect(_headRecord.delete); Future operate(Future Function(_DHTShortArrayHead) closure) async => - // ignore: prefer_expression_function_bodies - _headMutex.protect(() async { - return closure(this); - }); + _headMutex.protect(() async => closure(this)); Future operateWrite( Future Function(_DHTShortArrayHead) closure) async => @@ -115,7 +112,7 @@ class _DHTShortArrayHead { late List oldLinkedRecords; late List oldIndex; late List oldFree; - late List oldSeqs; + late List oldSeqs; late T out; try { @@ -197,7 +194,8 @@ class _DHTShortArrayHead { // 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 updatedSeqs = + List.of(head.seqs.map((x) => x == 0xFFFFFFFF ? null : x)); final updatedFree = _makeFreeList(updatedLinkedKeys, updatedIndex); // See which records are actually new @@ -333,7 +331,7 @@ class _DHTShortArrayHead { } Future lookupIndex(int idx, bool allowCreate) async { - final seq = idx < _seqs.length ? _seqs[idx] : DHTRecord.emptySeq; + final seq = idx < _seqs.length ? _seqs[idx] : null; final recordNumber = idx ~/ _stride; final record = await _getOrCreateLinkedRecord(recordNumber, allowCreate); final recordSubkey = (idx % _stride) + ((recordNumber == 0) ? 1 : 0); @@ -445,18 +443,18 @@ class _DHTShortArrayHead { // 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 < idx || _localSeqs[idx] == DHTRecord.emptySeq) { + if (_localSeqs.length < idx || _localSeqs[idx] == null) { 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 < idx || _seqs[idx] == DHTRecord.emptySeq) { + if (_seqs.length < idx || _seqs[idx] == null) { return false; } - return _localSeqs[idx] < _seqs[idx]; + return _localSeqs[idx]! < _seqs[idx]!; } /// Update the sequence number for a particular index in @@ -466,12 +464,12 @@ class _DHTShortArrayHead { final idx = _index[pos]; while (_localSeqs.length <= idx) { - _localSeqs.add(DHTRecord.emptySeq); + _localSeqs.add(null); } _localSeqs[idx] = newSeq; if (write) { while (_seqs.length <= idx) { - _seqs.add(DHTRecord.emptySeq); + _seqs.add(null); } _seqs[idx] = newSeq; } @@ -555,7 +553,7 @@ class _DHTShortArrayHead { // 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/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 index 1705bc0..fa3b1c6 100644 --- 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 @@ -129,7 +129,7 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead final outSeqNum = Output(); - final result = lookup.seq == DHTRecord.emptySeq + final result = lookup.seq == null ? null : await lookup.record.get(subkey: lookup.recordSubkey); @@ -163,7 +163,7 @@ class _DHTShortArrayWrite extends _DHTShortArrayRead final lookup = await _head.lookupPosition(pos, true); final outSeqNumRead = Output(); - final oldValue = lookup.seq == DHTRecord.emptySeq + final oldValue = lookup.seq == null ? null : await lookup.record .get(subkey: lookup.recordSubkey, outSeqNum: outSeqNumRead); From ae154f1bed097515023e0886c0c07cf6a835e017 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 12 May 2025 10:15:53 -0400 Subject: [PATCH 243/270] Dht perf --- CHANGELOG.md | 8 + .../reconciliation/author_input_queue.dart | 13 +- lib/veilid_processor/views/developer.dart | 17 +- packages/veilid_support/example/pubspec.lock | 8 + .../dht_support/src/dht_record/barrel.dart | 1 + .../src/dht_record/dht_record.dart | 301 +++++++++--------- .../src/dht_record/dht_record_pool.dart | 264 ++++++++------- packages/veilid_support/pubspec.lock | 8 + packages/veilid_support/pubspec.yaml | 1 + pubspec.lock | 8 + 10 files changed, 340 insertions(+), 289 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cd3873..9182300 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## UNRELEASED ## + +- Fix reconciliation `advance()` +- Add `pool stats` command +- Fixed issue with Android 'back' button exiting the app (#331) +- Deprecated accounts no longer crash application at startup +- Simplify SingleContactMessagesCubit and MessageReconciliation + ## v0.4.7 ## - *Community Contributions* - Fix getting stuck on splash screen when veilid is already started @bmv437 / @bgrift diff --git a/lib/chat/cubits/reconciliation/author_input_queue.dart b/lib/chat/cubits/reconciliation/author_input_queue.dart index 73dddc6..9a65e82 100644 --- a/lib/chat/cubits/reconciliation/author_input_queue.dart +++ b/lib/chat/cubits/reconciliation/author_input_queue.dart @@ -83,16 +83,13 @@ class AuthorInputQueue { } } - /// Remove a reconciled message and move to the next message + /// Move the reconciliation cursor (_inputPosition) forward on the input + /// queue and tees up the next message for processing /// Returns true if there is more work to do + /// Returns false if there are no more messages to reconcile in this queue Future advance() async { - final currentMessage = await getCurrentMessage(); - if (currentMessage == null) { - return false; - } - // Move current message to previous - _previousMessage = _currentMessage; + _previousMessage = await getCurrentMessage(); _currentMessage = null; while (true) { @@ -178,7 +175,7 @@ class AuthorInputQueue { // _inputPosition points to either before the input source starts // or the position of the previous element. We still need to set the - // _currentMessage to the previous element so consume() can compare + // _currentMessage to the previous element so advance() can compare // against it if we can. if (_inputPosition >= 0) { _currentMessage = currentWindow diff --git a/lib/veilid_processor/views/developer.dart b/lib/veilid_processor/views/developer.dart index e329d47..d749a9c 100644 --- a/lib/veilid_processor/views/developer.dart +++ b/lib/veilid_processor/views/developer.dart @@ -98,6 +98,16 @@ class _DeveloperPageState extends State { return true; } + if (debugCommand == 'pool stats') { + try { + DHTRecordPool.instance.debugPrintStats(); + } on Exception catch (e, st) { + _debugOut('<<< ERROR\n$e\n<<< STACK\n$st'); + return false; + } + return true; + } + if (debugCommand.startsWith('change_log_ignore ')) { final args = debugCommand.split(' '); if (args.length < 3) { @@ -129,9 +139,10 @@ class _DeveloperPageState extends State { if (debugCommand == 'help') { out = 'VeilidChat Commands:\n' - ' pool allocations - List DHTRecordPool allocations\n' - ' pool opened - List opened DHTRecord instances' - ' from the pool\n' + ' pool \n' + ' allocations - List DHTRecordPool allocations\n' + ' opened - List opened DHTRecord instances\n' + ' stats - Dump DHTRecordPool statistics\n' ' change_log_ignore change the log' ' target ignore list for a tracing layer\n' ' targets to add to the ignore list can be separated by' diff --git a/packages/veilid_support/example/pubspec.lock b/packages/veilid_support/example/pubspec.lock index 2e87d8c..c40540e 100644 --- a/packages/veilid_support/example/pubspec.lock +++ b/packages/veilid_support/example/pubspec.lock @@ -258,6 +258,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + indent: + dependency: transitive + description: + name: indent + sha256: "819319a5c185f7fe412750c798953378b37a0d0d32564ce33e7c5acfd1372d2a" + url: "https://pub.dev" + source: hosted + version: "2.0.0" integration_test: dependency: "direct dev" description: flutter 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 index 06933be..2d7e677 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/barrel.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/barrel.dart @@ -1,3 +1,4 @@ export 'default_dht_record_cubit.dart'; export 'dht_record_cubit.dart'; export 'dht_record_pool.dart'; +export 'stats.dart'; 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 d632b58..b9218e4 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 @@ -119,55 +119,56 @@ class DHTRecord implements DHTDeleteable { /// * 'outSeqNum' optionally returns the sequence number of the value being /// returned if one was returned. Future get( - {int subkey = -1, - VeilidCrypto? crypto, - DHTRecordRefreshMode refreshMode = DHTRecordRefreshMode.cached, - Output? outSeqNum}) async { - subkey = subkeyOrDefault(subkey); + {int subkey = -1, + VeilidCrypto? crypto, + DHTRecordRefreshMode refreshMode = DHTRecordRefreshMode.cached, + Output? outSeqNum}) async => + _wrapStats('get', () async { + subkey = subkeyOrDefault(subkey); - // Get the last sequence number if we need it - final lastSeq = - refreshMode._inspectLocal ? await _localSubkeySeq(subkey) : null; + // Get the last sequence number if we need it + final lastSeq = + refreshMode._inspectLocal ? await _localSubkeySeq(subkey) : null; - // See if we only ever want the locally stored value - if (refreshMode == DHTRecordRefreshMode.local && lastSeq == null) { - // If it's not available locally already just return null now - return null; - } - - var retry = kDHTTryAgainTries; - ValueData? valueData; - while (true) { - try { - valueData = await _routingContext.getDHTValue(key, subkey, - forceRefresh: refreshMode._forceRefresh); - break; - } on VeilidAPIExceptionTryAgain { - retry--; - if (retry == 0) { - throw const DHTExceptionNotAvailable(); + // See if we only ever want the locally stored value + if (refreshMode == DHTRecordRefreshMode.local && lastSeq == null) { + // If it's not available locally already just return null now + return null; } - await asyncSleep(); - } - } - if (valueData == null) { - return null; - } - // See if this get resulted in a newer sequence number - if (refreshMode == DHTRecordRefreshMode.update && - lastSeq != null && - valueData.seq <= lastSeq) { - // If we're only returning updates then punt now - return null; - } - // If we're returning a value, decrypt it - final out = (crypto ?? _crypto).decrypt(valueData.data); - if (outSeqNum != null) { - outSeqNum.save(valueData.seq); - } - return out; - } + var retry = kDHTTryAgainTries; + ValueData? valueData; + while (true) { + try { + valueData = await _routingContext.getDHTValue(key, subkey, + forceRefresh: refreshMode._forceRefresh); + break; + } on VeilidAPIExceptionTryAgain { + retry--; + if (retry == 0) { + throw const DHTExceptionNotAvailable(); + } + await asyncSleep(); + } + } + if (valueData == null) { + return null; + } + + // See if this get resulted in a newer sequence number + if (refreshMode == DHTRecordRefreshMode.update && + lastSeq != null && + valueData.seq <= lastSeq) { + // If we're only returning updates then punt now + return null; + } + // If we're returning a value, decrypt it + final out = (crypto ?? _crypto).decrypt(valueData.data); + if (outSeqNum != null) { + outSeqNum.save(valueData.seq); + } + return out; + }); /// Get a subkey value from this record. /// Process the record returned with a JSON unmarshal function 'fromJson'. @@ -223,97 +224,102 @@ class DHTRecord implements DHTDeleteable { /// If a newer value was found on the network, it is returned /// If the value was succesfully written, null is returned Future tryWriteBytes(Uint8List newValue, - {int subkey = -1, - VeilidCrypto? crypto, - KeyPair? writer, - Output? outSeqNum}) async { - subkey = subkeyOrDefault(subkey); - final lastSeq = await _localSubkeySeq(subkey); - final encryptedNewValue = await (crypto ?? _crypto).encrypt(newValue); + {int subkey = -1, + VeilidCrypto? crypto, + KeyPair? writer, + Output? outSeqNum}) async => + _wrapStats('tryWriteBytes', () async { + subkey = subkeyOrDefault(subkey); + final lastSeq = await _localSubkeySeq(subkey); + final encryptedNewValue = await (crypto ?? _crypto).encrypt(newValue); - // Set the new data if possible - var newValueData = await _routingContext - .setDHTValue(key, subkey, encryptedNewValue, writer: 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 - newValueData = await _routingContext.getDHTValue(key, subkey); - if (newValueData == null) { - assert(newValueData != null, "can't get value that was just set"); - return null; - } - } + // Set the new data if possible + var newValueData = await _routingContext.setDHTValue( + key, subkey, encryptedNewValue, + writer: 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 + newValueData = await _routingContext.getDHTValue(key, subkey); + 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; - if (isUpdated && outSeqNum != null) { - outSeqNum.save(newValueData.seq); - } + // Record new sequence number + final isUpdated = newValueData.seq != lastSeq; + if (isUpdated && outSeqNum != null) { + outSeqNum.save(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) { - DHTRecordPool.instance._processLocalValueChange(key, newValue, subkey); - } - return null; - } + // 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) { + DHTRecordPool.instance + ._processLocalValueChange(key, newValue, subkey); + } + return null; + } - // Decrypt value to return it - final decryptedNewValue = - await (crypto ?? _crypto).decrypt(newValueData.data); - if (isUpdated) { - DHTRecordPool.instance - ._processLocalValueChange(key, decryptedNewValue, subkey); - } - return decryptedNewValue; - } + // Decrypt value to return it + final decryptedNewValue = + await (crypto ?? _crypto).decrypt(newValueData.data); + if (isUpdated) { + DHTRecordPool.instance + ._processLocalValueChange(key, decryptedNewValue, subkey); + } + return decryptedNewValue; + }); /// Attempt to write a byte buffer to a DHTRecord subkey /// If a newer value was found on the network, another attempt /// will be made to write the subkey until this succeeds Future eventualWriteBytes(Uint8List newValue, - {int subkey = -1, - VeilidCrypto? crypto, - KeyPair? writer, - Output? outSeqNum}) async { - subkey = subkeyOrDefault(subkey); - final lastSeq = await _localSubkeySeq(subkey); - final encryptedNewValue = await (crypto ?? _crypto).encrypt(newValue); + {int subkey = -1, + VeilidCrypto? crypto, + KeyPair? writer, + Output? outSeqNum}) async => + _wrapStats('eventualWriteBytes', () async { + subkey = subkeyOrDefault(subkey); + final lastSeq = await _localSubkeySeq(subkey); + final encryptedNewValue = await (crypto ?? _crypto).encrypt(newValue); - ValueData? newValueData; - do { - do { - // Set the new data - newValueData = await _routingContext.setDHTValue( - key, subkey, encryptedNewValue, - writer: writer ?? _writer); + ValueData? newValueData; + do { + do { + // Set the new data + newValueData = await _routingContext.setDHTValue( + key, subkey, encryptedNewValue, + writer: writer ?? _writer); - // Repeat if newer data on the network was found - } while (newValueData != null); + // Repeat if newer data on the network was found + } while (newValueData != null); - // Get the data to check its sequence number - newValueData = await _routingContext.getDHTValue(key, subkey); - if (newValueData == null) { - assert(newValueData != null, "can't get value that was just set"); - return; - } + // Get the data to check its sequence number + 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 - if (outSeqNum != null) { - outSeqNum.save(newValueData.seq); - } + // Record new sequence number + if (outSeqNum != null) { + outSeqNum.save(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.equals(encryptedNewValue)); + // 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.equals(encryptedNewValue)); - final isUpdated = newValueData.seq != lastSeq; - if (isUpdated) { - DHTRecordPool.instance._processLocalValueChange(key, newValue, subkey); - } - } + final isUpdated = newValueData.seq != lastSeq; + if (isUpdated) { + DHTRecordPool.instance + ._processLocalValueChange(key, newValue, subkey); + } + }); /// Attempt to write a byte buffer to a DHTRecord subkey /// If a newer value was found on the network, another attempt @@ -321,32 +327,36 @@ class DHTRecord implements DHTDeleteable { /// Each attempt to write the value calls an update function with the /// old value to determine what new value should be attempted for that write. Future eventualUpdateBytes( - Future Function(Uint8List? oldValue) update, - {int subkey = -1, - VeilidCrypto? crypto, - KeyPair? writer, - Output? outSeqNum}) async { - subkey = subkeyOrDefault(subkey); + Future Function(Uint8List? oldValue) update, + {int subkey = -1, + VeilidCrypto? crypto, + KeyPair? writer, + Output? outSeqNum}) async => + _wrapStats('eventualUpdateBytes', () async { + subkey = subkeyOrDefault(subkey); - // 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, crypto: crypto, outSeqNum: outSeqNum); + // 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, crypto: crypto, outSeqNum: outSeqNum); - do { - // Update the data - final updatedValue = await update(oldValue); - if (updatedValue == null) { - // If null is returned from the update, stop trying to do the update - break; - } - // Try to write it back to the network - oldValue = await tryWriteBytes(updatedValue, - subkey: subkey, crypto: crypto, writer: writer, outSeqNum: outSeqNum); + do { + // Update the data + final updatedValue = await update(oldValue); + if (updatedValue == null) { + // If null is returned from the update, stop trying to do the update + break; + } + // Try to write it back to the network + oldValue = await tryWriteBytes(updatedValue, + subkey: subkey, + crypto: crypto, + writer: writer, + outSeqNum: outSeqNum); - // Repeat update if newer data on the network was found - } while (oldValue != null); - } + // Repeat update if newer data on the network was found + } while (oldValue != null); + }); /// Like 'tryWriteBytes' but with JSON marshal/unmarshal of the value Future tryWriteJson(T Function(dynamic) fromJson, T newValue, @@ -555,6 +565,9 @@ class DHTRecord implements DHTDeleteable { local: false, data: update.value?.data, subkeys: update.subkeys); } + Future _wrapStats(String func, Future Function() closure) => + DHTRecordPool.instance._stats.measure(key, debugName, func, closure); + ////////////////////////////////////////////////////////////// final _SharedDHTRecordData _sharedDHTRecordData; 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 3ee9adc..6dc0634 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 @@ -273,24 +273,6 @@ class DHTRecordPool with TableDBBackedJson { } } } - // else { - - // XXX: should no longer be necessary - // // Remove watch state - // - // for (final entry in _opened.entries) { - // final openedKey = entry.key; - // final openedRecordInfo = entry.value; - - // if (openedKey == updateValueChange.key) { - // for (final rec in openedRecordInfo.records) { - // rec._watchState = null; - // } - // openedRecordInfo.shared.needsWatchStateUpdate = true; - // break; - // } - // } - //} } /// Log the current record allocations @@ -320,6 +302,11 @@ class DHTRecordPool with TableDBBackedJson { } } + /// Log the performance stats + void debugPrintStats() { + log('DHTRecordPool Stats:\n${_stats.debugString()}'); + } + /// Public interface to DHTRecordPool logger void log(String message) { _logger?.call(message); @@ -369,109 +356,110 @@ class DHTRecordPool with TableDBBackedJson { } Future _recordOpenCommon( - {required String debugName, - required VeilidRoutingContext dhtctx, - required TypedKey recordKey, - required VeilidCrypto crypto, - required KeyPair? writer, - required TypedKey? parent, - required int defaultSubkey}) async { - log('openDHTRecord: debugName=$debugName key=$recordKey'); + {required String debugName, + required VeilidRoutingContext dhtctx, + required TypedKey recordKey, + required VeilidCrypto crypto, + required KeyPair? writer, + required TypedKey? parent, + required int defaultSubkey}) async => + _stats.measure(recordKey, debugName, '_recordOpenCommon', () async { + log('openDHTRecord: debugName=$debugName key=$recordKey'); - // See if this has been opened yet - final openedRecordInfo = await _mutex.protect(() async { - // If we are opening a key that already exists - // make sure we are using the same parent if one was specified - _validateParentInner(parent, recordKey); + // See if this has been opened yet + final openedRecordInfo = await _mutex.protect(() async { + // If we are opening a key that already exists + // make sure we are using the same parent if one was specified + _validateParentInner(parent, recordKey); - return _opened[recordKey]; - }); + return _opened[recordKey]; + }); - if (openedRecordInfo == null) { - // Fresh open, just open the record - var retry = kDHTKeyNotFoundTries; - late final DHTRecordDescriptor recordDescriptor; - while (true) { - try { - recordDescriptor = - await dhtctx.openDHTRecord(recordKey, writer: writer); - break; - } on VeilidAPIExceptionTryAgain { - throw const DHTExceptionNotAvailable(); - } on VeilidAPIExceptionKeyNotFound { - await asyncSleep(); - retry--; - if (retry == 0) { - throw const DHTExceptionNotAvailable(); + if (openedRecordInfo == null) { + // Fresh open, just open the record + var retry = kDHTKeyNotFoundTries; + late final DHTRecordDescriptor recordDescriptor; + while (true) { + try { + recordDescriptor = + await dhtctx.openDHTRecord(recordKey, writer: writer); + break; + } on VeilidAPIExceptionTryAgain { + throw const DHTExceptionNotAvailable(); + } on VeilidAPIExceptionKeyNotFound { + await asyncSleep(); + retry--; + if (retry == 0) { + throw const DHTExceptionNotAvailable(); + } + } } + + final newOpenedRecordInfo = _OpenedRecordInfo( + recordDescriptor: recordDescriptor, + defaultWriter: writer, + defaultRoutingContext: dhtctx); + + final rec = DHTRecord._( + debugName: debugName, + routingContext: dhtctx, + defaultSubkey: defaultSubkey, + sharedDHTRecordData: newOpenedRecordInfo.shared, + writer: writer, + crypto: crypto); + + await _mutex.protect(() async { + // Register the opened record + _opened[recordDescriptor.key] = newOpenedRecordInfo; + + // Register the dependency + await _addDependencyInner( + parent, + recordKey, + debugName: debugName, + ); + + // Register the newly opened record + newOpenedRecordInfo.records.add(rec); + }); + + return rec; } - } - final newOpenedRecordInfo = _OpenedRecordInfo( - recordDescriptor: recordDescriptor, - defaultWriter: writer, - defaultRoutingContext: dhtctx); + // Already opened - final rec = DHTRecord._( - debugName: debugName, - routingContext: dhtctx, - defaultSubkey: defaultSubkey, - sharedDHTRecordData: newOpenedRecordInfo.shared, - writer: writer, - crypto: crypto); + // 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) { + await dhtctx.openDHTRecord(recordKey, writer: writer); + // New writer if we didn't specify one before + openedRecordInfo.shared.defaultWriter = writer; + // New default routing context if we opened it again + openedRecordInfo.shared.defaultRoutingContext = dhtctx; + } - await _mutex.protect(() async { - // Register the opened record - _opened[recordDescriptor.key] = newOpenedRecordInfo; + final rec = DHTRecord._( + debugName: debugName, + routingContext: dhtctx, + defaultSubkey: defaultSubkey, + sharedDHTRecordData: openedRecordInfo.shared, + writer: writer, + crypto: crypto); - // Register the dependency - await _addDependencyInner( - parent, - recordKey, - debugName: debugName, - ); + await _mutex.protect(() async { + // Register the dependency + await _addDependencyInner( + parent, + recordKey, + debugName: debugName, + ); - // Register the newly opened record - newOpenedRecordInfo.records.add(rec); + openedRecordInfo.records.add(rec); + }); + + return rec; }); - return rec; - } - - // 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) { - await dhtctx.openDHTRecord(recordKey, writer: writer); - // New writer if we didn't specify one before - openedRecordInfo.shared.defaultWriter = writer; - // New default routing context if we opened it again - openedRecordInfo.shared.defaultRoutingContext = dhtctx; - } - - final rec = DHTRecord._( - debugName: debugName, - routingContext: dhtctx, - defaultSubkey: defaultSubkey, - sharedDHTRecordData: openedRecordInfo.shared, - writer: writer, - crypto: crypto); - - await _mutex.protect(() async { - // Register the dependency - await _addDependencyInner( - parent, - recordKey, - debugName: debugName, - ); - - openedRecordInfo.records.add(rec); - }); - - return rec; - } - // Called when a DHTRecord is closed // Cleans up the opened record housekeeping and processes any late deletions Future _recordClosed(DHTRecord record) async { @@ -866,34 +854,37 @@ class DHTRecordPool with TableDBBackedJson { void _pollWatch(TypedKey openedRecordKey, _OpenedRecordInfo openedRecordInfo, _WatchState unionWatchState) { singleFuture((this, _sfPollWatch, openedRecordKey), () async { - final dhtctx = openedRecordInfo.shared.defaultRoutingContext; + await _stats.measure( + openedRecordKey, openedRecordInfo.debugNames, '_pollWatch', () async { + final dhtctx = openedRecordInfo.shared.defaultRoutingContext; - final currentReport = await dhtctx.inspectDHTRecord(openedRecordKey, - subkeys: unionWatchState.subkeys, scope: DHTReportScope.syncGet); + final currentReport = await dhtctx.inspectDHTRecord(openedRecordKey, + subkeys: unionWatchState.subkeys, scope: DHTReportScope.syncGet); - final fsc = currentReport.firstSeqChange; - if (fsc == null) { - return null; - } - final newerSubkeys = currentReport.newerOnlineSubkeys; + final fsc = currentReport.firstSeqChange; + if (fsc == null) { + return null; + } + final newerSubkeys = currentReport.newerOnlineSubkeys; - final valueData = await dhtctx.getDHTValue(openedRecordKey, fsc.subkey, - forceRefresh: true); - if (valueData == null) { - return; - } + final valueData = await dhtctx.getDHTValue(openedRecordKey, fsc.subkey, + forceRefresh: true); + if (valueData == null) { + return; + } - if (valueData.seq < fsc.newSeq) { - log('inspect returned a newer seq than get: ${valueData.seq} < $fsc'); - } + if (valueData.seq < fsc.newSeq) { + log('inspect returned a newer seq than get: ${valueData.seq} < $fsc'); + } - if (fsc.oldSeq == null || valueData.seq > fsc.oldSeq!) { - processRemoteValueChange(VeilidUpdateValueChange( - key: openedRecordKey, - subkeys: newerSubkeys, - count: 0xFFFFFFFF, - value: valueData)); - } + if (fsc.oldSeq == null || valueData.seq > fsc.oldSeq!) { + processRemoteValueChange(VeilidUpdateValueChange( + key: openedRecordKey, + subkeys: newerSubkeys, + count: 0xFFFFFFFF, + value: valueData)); + } + }); }); } @@ -915,8 +906,11 @@ class DHTRecordPool with TableDBBackedJson { _watchStateProcessors.updateState( openedRecordKey, unionWatchState, - (newState) => - _watchStateChange(openedRecordKey, unionWatchState)); + (newState) => _stats.measure( + openedRecordKey, + openedRecordInfo.debugNames, + '_watchStateChange', + () => _watchStateChange(openedRecordKey, unionWatchState))); } } }); @@ -958,6 +952,8 @@ class DHTRecordPool with TableDBBackedJson { // Watch state processors final _watchStateProcessors = SingleStateProcessorMap(); + // Statistics + final _stats = DHTStats(); static DHTRecordPool? _singleton; } diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index 11c2db6..d86402b 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -331,6 +331,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + indent: + dependency: "direct main" + description: + name: indent + sha256: "819319a5c185f7fe412750c798953378b37a0d0d32564ce33e7c5acfd1372d2a" + url: "https://pub.dev" + source: hosted + version: "2.0.0" io: dependency: transitive description: diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index 548c40e..65ba78d 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: equatable: ^2.0.7 fast_immutable_collections: ^11.0.3 freezed_annotation: ^3.0.0 + indent: ^2.0.0 json_annotation: ^4.9.0 loggy: ^2.0.3 meta: ^1.16.0 diff --git a/pubspec.lock b/pubspec.lock index db2adfc..e7252a3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -809,6 +809,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.3" + indent: + dependency: transitive + description: + name: indent + sha256: "819319a5c185f7fe412750c798953378b37a0d0d32564ce33e7c5acfd1372d2a" + url: "https://pub.dev" + source: hosted + version: "2.0.0" intl: dependency: "direct main" description: From 063eeb8d1253990e4ff9e53793fbafd07b4bae6e Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 12 May 2025 20:18:14 -0400 Subject: [PATCH 244/270] stats --- .../lib/dht_support/src/dht_record/stats.dart | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 packages/veilid_support/lib/dht_support/src/dht_record/stats.dart diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/stats.dart b/packages/veilid_support/lib/dht_support/src/dht_record/stats.dart new file mode 100644 index 0000000..6388f5c --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/dht_record/stats.dart @@ -0,0 +1,175 @@ +import 'package:collection/collection.dart'; +import 'package:indent/indent.dart'; + +import '../../../veilid_support.dart'; + +const maxLatencySamples = 100; +const timeoutDuration = 10; + +extension LatencyStatsExt on LatencyStats { + String debugString() => 'fast($fastest)/avg($average)/slow($slowest)/' + 'tm90($tm90)/tm75($tm75)/p90($p90)/p75($p75)'; +} + +class LatencyStatsAccounting { + LatencyStatsAccounting({required this.maxSamples}); + + LatencyStats record(TimestampDuration dur) { + _samples.add(dur); + if (_samples.length > maxSamples) { + _samples.removeAt(0); + } + + final sortedList = _samples.sorted(); + + final fastest = sortedList.first; + final slowest = sortedList.last; + final average = TimestampDuration( + value: sortedList.fold(BigInt.zero, (acc, x) => acc + x.value) ~/ + BigInt.from(sortedList.length)); + + final tm90len = (sortedList.length * 90 + 99) ~/ 100; + final tm75len = (sortedList.length * 75 + 99) ~/ 100; + final tm90 = TimestampDuration( + value: sortedList + .sublist(0, tm90len) + .fold(BigInt.zero, (acc, x) => acc + x.value) ~/ + BigInt.from(tm90len)); + final tm75 = TimestampDuration( + value: sortedList + .sublist(0, tm75len) + .fold(BigInt.zero, (acc, x) => acc + x.value) ~/ + BigInt.from(tm90len)); + final p90 = sortedList[tm90len - 1]; + final p75 = sortedList[tm75len - 1]; + + final ls = LatencyStats( + fastest: fastest, + slowest: slowest, + average: average, + tm90: tm90, + tm75: tm75, + p90: p90, + p75: p75); + + return ls; + } + + ///////////////////////////// + final int maxSamples; + final _samples = []; +} + +class DHTCallStats { + void record(TimestampDuration dur, Exception? exc) { + final wasTimeout = + exc is VeilidAPIExceptionTimeout || dur.toSecs() >= timeoutDuration; + + calls++; + if (wasTimeout) { + timeouts++; + } else { + successLatency = successLatencyAcct.record(dur); + } + latency = latencyAcct.record(dur); + } + + String debugString() => + ' timeouts/calls: $timeouts/$calls (${(timeouts * 100 / calls).toStringAsFixed(3)}%)\n' + 'success latency: ${successLatency?.debugString()}\n' + ' all latency: ${latency?.debugString()}\n'; + + ///////////////////////////// + int calls = 0; + int timeouts = 0; + LatencyStats? latency; + LatencyStats? successLatency; + final latencyAcct = LatencyStatsAccounting(maxSamples: maxLatencySamples); + final successLatencyAcct = + LatencyStatsAccounting(maxSamples: maxLatencySamples); +} + +class DHTPerKeyStats { + DHTPerKeyStats(this.debugName); + + void record(String func, TimestampDuration dur, Exception? exc) { + final keyFuncStats = _perFuncStats.putIfAbsent(func, DHTCallStats.new); + + _stats.record(dur, exc); + keyFuncStats.record(dur, exc); + } + + String debugString() { + // + final out = StringBuffer() + ..write('Name: $debugName\n') + ..write(_stats.debugString().indent(4)) + ..writeln('Per-Function:'); + for (final entry in _perFuncStats.entries) { + final funcName = entry.key; + final funcStats = entry.value.debugString().indent(4); + out.write('$funcName:\n$funcStats'.indent(4)); + } + + return out.toString(); + } + + ////////////////////////////// + + final String debugName; + final _stats = DHTCallStats(); + final _perFuncStats = {}; +} + +class DHTStats { + DHTStats(); + + Future measure(TypedKey key, String debugName, String func, + Future Function() closure) async { + // + final start = Veilid.instance.now(); + final keyStats = + _statsPerKey.putIfAbsent(key, () => DHTPerKeyStats(debugName)); + final funcStats = _statsPerFunc.putIfAbsent(func, DHTCallStats.new); + + VeilidAPIException? exc; + + try { + final res = await closure(); + + return res; + } on VeilidAPIException catch (e) { + exc = e; + rethrow; + } finally { + final end = Veilid.instance.now(); + final dur = end.diff(start); + + keyStats.record(func, dur, exc); + funcStats.record(dur, exc); + } + } + + String debugString() { + // + final out = StringBuffer()..writeln('Per-Function:'); + for (final entry in _statsPerFunc.entries) { + final funcName = entry.key; + final funcStats = entry.value.debugString().indent(4); + out.write('$funcName:\n$funcStats\n'.indent(4)); + } + out.writeln('Per-Key:'); + for (final entry in _statsPerKey.entries) { + final keyName = entry.key; + final keyStats = entry.value.debugString().indent(4); + out.write('$keyName:\n$keyStats\n'.indent(4)); + } + + return out.toString(); + } + + ////////////////////////////// + + final _statsPerKey = {}; + final _statsPerFunc = {}; +} From 1a9cca0667614cff17209a285b3a7615955b706b Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 17 May 2025 18:02:17 -0400 Subject: [PATCH 245/270] new chat widget --- CHANGELOG.md | 1 + assets/i18n/en.json | 4 + flutter_01.png | 0 flutter_02.png | 0 flutter_03.png | 0 .../models/local_account/local_account.dart | 5 +- .../local_account/local_account.freezed.dart | 3 +- .../models/user_login/user_login.dart | 3 +- .../models/user_login/user_login.freezed.dart | 3 +- .../views/edit_account_page.dart | 1 - .../views/show_recovery_key_page.dart | 1 - lib/chat/cubits/chat_component_cubit.dart | 177 +++---- .../cubits/single_contact_messages_cubit.dart | 64 ++- lib/chat/models/chat_component_state.dart | 23 +- .../models/chat_component_state.freezed.dart | 167 +++--- lib/chat/models/message_state.dart | 3 + lib/chat/models/message_state.freezed.dart | 42 +- lib/chat/models/message_state.g.dart | 2 + .../views/chat_builders/chat_builders.dart | 2 + .../chat_builders/vc_composer_widget.dart | 431 ++++++++++++++++ .../chat_builders/vc_text_message_widget.dart | 269 ++++++++++ lib/chat/views/chat_component_widget.dart | 333 ++++++++---- lib/chat/views/date_formatter.dart | 41 ++ ..._length_limiting_text_input_formatter.dart | 54 ++ lib/chat/views/views.dart | 1 + lib/init.dart | 8 +- lib/keyboard_shortcuts.dart | 80 ++- lib/layout/home/home_account_ready.dart | 22 +- lib/layout/home/home_screen.dart | 54 +- lib/theme/models/chat_theme.dart | 488 ------------------ lib/theme/models/models.dart | 1 - .../models/scale_theme/scale_chat_theme.dart | 369 +++++++++++++ lib/theme/models/scale_theme/scale_color.dart | 1 + lib/theme/models/scale_theme/scale_theme.dart | 1 + lib/theme/views/responsive.dart | 25 +- lib/theme/views/styled_alert.dart | 2 +- lib/tools/loggy.dart | 5 +- lib/tools/state_logger.dart | 21 +- .../lib/identity_support/super_identity.dart | 1 + .../super_identity.freezed.dart | 1 + packages/veilid_support/lib/src/config.dart | 15 +- .../src/table_db_array_protobuf_cubit.dart | 2 +- pubspec.lock | 109 ++-- pubspec.yaml | 50 +- 44 files changed, 1904 insertions(+), 981 deletions(-) create mode 100644 flutter_01.png create mode 100644 flutter_02.png create mode 100644 flutter_03.png create mode 100644 lib/chat/views/chat_builders/chat_builders.dart create mode 100644 lib/chat/views/chat_builders/vc_composer_widget.dart create mode 100644 lib/chat/views/chat_builders/vc_text_message_widget.dart create mode 100644 lib/chat/views/date_formatter.dart create mode 100644 lib/chat/views/utf8_length_limiting_text_input_formatter.dart delete mode 100644 lib/theme/models/chat_theme.dart create mode 100644 lib/theme/models/scale_theme/scale_chat_theme.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 9182300..a7b1930 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Fixed issue with Android 'back' button exiting the app (#331) - Deprecated accounts no longer crash application at startup - Simplify SingleContactMessagesCubit and MessageReconciliation +- Update flutter_chat_ui to 2.0.0 ## v0.4.7 ## - *Community Contributions* diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 9192851..f0b84a0 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -313,5 +313,9 @@ "info": "Info", "debug": "Debug", "trace": "Trace" + }, + "date_formatter": { + "just_now": "Just now", + "yesterday": "Yesterday" } } \ No newline at end of file diff --git a/flutter_01.png b/flutter_01.png new file mode 100644 index 0000000..e69de29 diff --git a/flutter_02.png b/flutter_02.png new file mode 100644 index 0000000..e69de29 diff --git a/flutter_03.png b/flutter_03.png new file mode 100644 index 0000000..e69de29 diff --git a/lib/account_manager/models/local_account/local_account.dart b/lib/account_manager/models/local_account/local_account.dart index 49506d0..81cfb8c 100644 --- a/lib/account_manager/models/local_account/local_account.dart +++ b/lib/account_manager/models/local_account/local_account.dart @@ -15,8 +15,9 @@ part 'local_account.freezed.dart'; // and the identitySecretKey optionally encrypted by an unlock code // This is the root of the account information tree for VeilidChat // -@freezed -sealed class LocalAccount with _$LocalAccount { +@Freezed(toJson: true) +abstract class LocalAccount with _$LocalAccount { + @JsonSerializable() const factory LocalAccount({ // The super identity key record for the account, // containing the publicKey in the currentIdentity 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 e2c3c55..8d7aed1 100644 --- a/lib/account_manager/models/local_account/local_account.freezed.dart +++ b/lib/account_manager/models/local_account/local_account.freezed.dart @@ -153,6 +153,7 @@ class _$LocalAccountCopyWithImpl<$Res> implements $LocalAccountCopyWith<$Res> { } /// @nodoc + @JsonSerializable() class _LocalAccount implements LocalAccount { const _LocalAccount( @@ -162,8 +163,6 @@ class _LocalAccount implements LocalAccount { required this.biometricsEnabled, required this.hiddenAccount, required this.name}); - factory _LocalAccount.fromJson(Map json) => - _$LocalAccountFromJson(json); // The super identity key record for the account, // containing the publicKey in the currentIdentity diff --git a/lib/account_manager/models/user_login/user_login.dart b/lib/account_manager/models/user_login/user_login.dart index 4d96d38..4e2f680 100644 --- a/lib/account_manager/models/user_login/user_login.dart +++ b/lib/account_manager/models/user_login/user_login.dart @@ -8,8 +8,9 @@ part 'user_login.g.dart'; // Represents a currently logged in account // User logins are stored in the user_logins tablestore table // indexed by the accountSuperIdentityRecordKey -@freezed +@Freezed(toJson: true) sealed class UserLogin with _$UserLogin { + @JsonSerializable() const factory UserLogin({ // SuperIdentity record key for the user // used to index the local accounts table 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 b0c6070..c406812 100644 --- a/lib/account_manager/models/user_login/user_login.freezed.dart +++ b/lib/account_manager/models/user_login/user_login.freezed.dart @@ -124,6 +124,7 @@ class _$UserLoginCopyWithImpl<$Res> implements $UserLoginCopyWith<$Res> { } /// @nodoc + @JsonSerializable() class _UserLogin implements UserLogin { const _UserLogin( @@ -131,8 +132,6 @@ class _UserLogin implements UserLogin { required this.identitySecret, required this.accountRecordInfo, required this.lastActive}); - factory _UserLogin.fromJson(Map json) => - _$UserLoginFromJson(json); // SuperIdentity record key for the user // used to index the local accounts table diff --git a/lib/account_manager/views/edit_account_page.dart b/lib/account_manager/views/edit_account_page.dart index bd18967..9af4719 100644 --- a/lib/account_manager/views/edit_account_page.dart +++ b/lib/account_manager/views/edit_account_page.dart @@ -249,7 +249,6 @@ class _EditAccountPageState extends WindowSetupState { final displayModalHUD = _isInAsyncCall; return StyledScaffold( - // resizeToAvoidBottomInset: false, appBar: DefaultAppBar( title: Text(translate('edit_account_page.titlebar')), leading: Navigator.canPop(context) diff --git a/lib/account_manager/views/show_recovery_key_page.dart b/lib/account_manager/views/show_recovery_key_page.dart index bf44dd7..7c971e0 100644 --- a/lib/account_manager/views/show_recovery_key_page.dart +++ b/lib/account_manager/views/show_recovery_key_page.dart @@ -163,7 +163,6 @@ class _ShowRecoveryKeyPageState extends WindowSetupState { final displayModalHUD = _isInAsyncCall; return StyledScaffold( - // resizeToAvoidBottomInset: false, appBar: DefaultAppBar( title: Text(translate('show_recovery_key_page.titlebar')), actions: [ diff --git a/lib/chat/cubits/chat_component_cubit.dart b/lib/chat/cubits/chat_component_cubit.dart index 6112384..76ff660 100644 --- a/lib/chat/cubits/chat_component_cubit.dart +++ b/lib/chat/cubits/chat_component_cubit.dart @@ -4,11 +4,8 @@ import 'dart:typed_data'; import 'package:async_tools/async_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fixnum/fixnum.dart'; -import 'package:flutter/widgets.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:scroll_to_index/scroll_to_index.dart'; +import 'package:flutter_chat_core/flutter_chat_core.dart' as core; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; @@ -19,6 +16,7 @@ import '../../tools/tools.dart'; import '../models/chat_component_state.dart'; import '../models/message_state.dart'; import '../models/window_state.dart'; +import '../views/chat_component_widget.dart'; import 'cubits.dart'; const metadataKeyIdentityPublicKey = 'identityPublicKey'; @@ -39,15 +37,12 @@ class ChatComponentCubit extends Cubit { _contactListCubit = contactListCubit, _conversationCubits = conversationCubits, _messagesCubit = messagesCubit, - super(ChatComponentState( - chatKey: GlobalKey(), - scrollController: AutoScrollController(), - textEditingController: InputTextFieldController(), + super(const ChatComponentState( localUser: null, - remoteUsers: const IMap.empty(), - historicalRemoteUsers: const IMap.empty(), - unknownUsers: const IMap.empty(), - messageWindow: const AsyncLoading(), + remoteUsers: IMap.empty(), + historicalRemoteUsers: IMap.empty(), + unknownUsers: IMap.empty(), + messageWindow: AsyncLoading(), title: '', )) { // Immediate Init @@ -102,6 +97,7 @@ class ChatComponentCubit extends Cubit { await _accountRecordSubscription.cancel(); await _messagesSubscription.cancel(); await _conversationSubscriptions.values.map((v) => v.cancel()).wait; + await super.close(); } @@ -122,32 +118,15 @@ class ChatComponentCubit extends Cubit { } // Send a message - void sendMessage(types.PartialText message) { - final text = message.text; - - final replyId = (message.repliedMessage != null) - ? base64UrlNoPadDecode(message.repliedMessage!.id) + void sendMessage( + {required String text, + String? replyToMessageId, + Timestamp? expiration, + int? viewLimit, + List? attachments}) { + final replyId = (replyToMessageId != null) + ? base64UrlNoPadDecode(replyToMessageId) : null; - Timestamp? expiration; - int? viewLimit; - List? attachments; - final metadata = message.metadata; - if (metadata != null) { - final expirationValue = - metadata[metadataKeyExpirationDuration] as TimestampDuration?; - if (expirationValue != null) { - expiration = Veilid.instance.now().offset(expirationValue); - } - final viewLimitValue = metadata[metadataKeyViewLimit] as int?; - if (viewLimitValue != null) { - viewLimit = viewLimitValue; - } - final attachmentsValue = - metadata[metadataKeyAttachments] as List?; - if (attachmentsValue != null) { - attachments = attachmentsValue; - } - } _addTextMessage( text: text, @@ -172,9 +151,9 @@ class ChatComponentCubit extends Cubit { emit(state.copyWith(localUser: null)); return; } - final localUser = types.User( + final localUser = core.User( id: _localUserIdentityKey.toString(), - firstName: account.profile.name, + name: account.profile.name, metadata: {metadataKeyIdentityPublicKey: _localUserIdentityKey}); emit(state.copyWith(localUser: localUser)); } @@ -199,11 +178,12 @@ class ChatComponentCubit extends Cubit { // Don't change user information on loading state return; } + + final remoteUser = + _convertRemoteUser(remoteIdentityPublicKey, activeConversationState); + emit(_updateTitle(state.copyWith( - remoteUsers: state.remoteUsers.add( - remoteIdentityPublicKey, - _convertRemoteUser( - remoteIdentityPublicKey, activeConversationState))))); + remoteUsers: state.remoteUsers.add(remoteUser.id, remoteUser)))); } static ChatComponentState _updateTitle(ChatComponentState currentState) { @@ -212,13 +192,13 @@ class ChatComponentCubit extends Cubit { } if (currentState.remoteUsers.length == 1) { final remoteUser = currentState.remoteUsers.values.first; - return currentState.copyWith(title: remoteUser.firstName ?? ''); + return currentState.copyWith(title: remoteUser.name ?? ''); } return currentState.copyWith( title: ''); } - types.User _convertRemoteUser(TypedKey remoteIdentityPublicKey, + core.User _convertRemoteUser(TypedKey remoteIdentityPublicKey, ActiveConversationState activeConversationState) { // See if we have a contact for this remote user final contacts = _contactListCubit.state.state.asData?.value; @@ -227,25 +207,24 @@ class ChatComponentCubit extends Cubit { x.value.identityPublicKey.toVeilid() == remoteIdentityPublicKey); if (contactIdx != -1) { final contact = contacts[contactIdx].value; - return types.User( + return core.User( id: remoteIdentityPublicKey.toString(), - firstName: contact.displayName, + name: contact.displayName, metadata: {metadataKeyIdentityPublicKey: remoteIdentityPublicKey}); } } - return types.User( + return core.User( id: remoteIdentityPublicKey.toString(), - firstName: activeConversationState.remoteConversation?.profile.name ?? + name: activeConversationState.remoteConversation?.profile.name ?? '', metadata: {metadataKeyIdentityPublicKey: remoteIdentityPublicKey}); } - types.User _convertUnknownUser(TypedKey remoteIdentityPublicKey) => - types.User( - id: remoteIdentityPublicKey.toString(), - firstName: '<$remoteIdentityPublicKey>', - metadata: {metadataKeyIdentityPublicKey: remoteIdentityPublicKey}); + core.User _convertUnknownUser(TypedKey remoteIdentityPublicKey) => core.User( + id: remoteIdentityPublicKey.toString(), + name: '<$remoteIdentityPublicKey>', + metadata: {metadataKeyIdentityPublicKey: remoteIdentityPublicKey}); Future _updateConversationSubscriptions() async { // Get existing subscription keys and state @@ -267,16 +246,17 @@ class ChatComponentCubit extends Cubit { final activeConversationState = cc.state.asData?.value; if (activeConversationState != null) { - currentRemoteUsersState = currentRemoteUsersState.add( - remoteIdentityPublicKey, - _convertRemoteUser( - remoteIdentityPublicKey, activeConversationState)); + final remoteUser = _convertRemoteUser( + remoteIdentityPublicKey, activeConversationState); + currentRemoteUsersState = + currentRemoteUsersState.add(remoteUser.id, remoteUser); } } // Purge remote users we didn't see in the cubit list any more final cancels = >[]; for (final deadUser in existing) { - currentRemoteUsersState = currentRemoteUsersState.remove(deadUser); + currentRemoteUsersState = + currentRemoteUsersState.remove(deadUser.toString()); cancels.add(_conversationSubscriptions.remove(deadUser)!.cancel()); } await cancels.wait; @@ -285,63 +265,76 @@ class ChatComponentCubit extends Cubit { emit(_updateTitle(state.copyWith(remoteUsers: currentRemoteUsersState))); } - (ChatComponentState, types.Message?) _messageStateToChatMessage( + (ChatComponentState, core.Message?) _messageStateToChatMessage( ChatComponentState currentState, MessageState message) { final authorIdentityPublicKey = message.content.author.toVeilid(); - late final types.User author; + final authorUserId = authorIdentityPublicKey.toString(); + + late final core.User author; if (authorIdentityPublicKey == _localUserIdentityKey && currentState.localUser != null) { author = currentState.localUser!; } else { - final remoteUser = currentState.remoteUsers[authorIdentityPublicKey]; + final remoteUser = currentState.remoteUsers[authorUserId]; if (remoteUser != null) { author = remoteUser; } else { final historicalRemoteUser = - currentState.historicalRemoteUsers[authorIdentityPublicKey]; + currentState.historicalRemoteUsers[authorUserId]; if (historicalRemoteUser != null) { author = historicalRemoteUser; } else { - final unknownRemoteUser = - currentState.unknownUsers[authorIdentityPublicKey]; + final unknownRemoteUser = currentState.unknownUsers[authorUserId]; if (unknownRemoteUser != null) { author = unknownRemoteUser; } else { final unknownUser = _convertUnknownUser(authorIdentityPublicKey); currentState = currentState.copyWith( - unknownUsers: currentState.unknownUsers - .add(authorIdentityPublicKey, unknownUser)); + unknownUsers: + currentState.unknownUsers.add(authorUserId, unknownUser)); author = unknownUser; } } } } - types.Status? status; - if (message.sendState != null) { - assert(author.id == _localUserIdentityKey.toString(), - 'send state should only be on sent messages'); - switch (message.sendState!) { - case MessageSendState.sending: - status = types.Status.sending; - case MessageSendState.sent: - status = types.Status.sent; - case MessageSendState.delivered: - status = types.Status.delivered; - } - } + // types.Status? status; + // if (message.sendState != null) { + // assert(author.id == _localUserIdentityKey.toString(), + // 'send state should only be on sent messages'); + // switch (message.sendState!) { + // case MessageSendState.sending: + // status = types.Status.sending; + // case MessageSendState.sent: + // status = types.Status.sent; + // case MessageSendState.delivered: + // status = types.Status.delivered; + // } + // } + + final reconciledAt = message.reconciledTimestamp == null + ? null + : DateTime.fromMicrosecondsSinceEpoch( + message.reconciledTimestamp!.value.toInt()); + + // print('message seqid: ${message.seqId}'); switch (message.content.whichKind()) { case proto.Message_Kind.text: - final contextText = message.content.text; - final textMessage = types.TextMessage( - author: author, - createdAt: - (message.sentTimestamp.value ~/ BigInt.from(1000)).toInt(), - id: message.content.authorUniqueIdString, - text: contextText.text, - showStatus: status != null, - status: status); + final reconciledId = message.content.authorUniqueIdString; + final contentText = message.content.text; + final textMessage = core.TextMessage( + authorId: author.id, + createdAt: DateTime.fromMicrosecondsSinceEpoch( + message.sentTimestamp.value.toInt()), + sentAt: reconciledAt, + id: reconciledId, + text: '${contentText.text} (${message.seqId})', + //text: contentText.text, + metadata: { + kSeqId: message.seqId, + if (core.isOnlyEmoji(contentText.text)) 'isOnlyEmoji': true, + }); return (currentState, textMessage); case proto.Message_Kind.secret: case proto.Message_Kind.delete: @@ -375,7 +368,7 @@ class ChatComponentCubit extends Cubit { final messagesState = avMessagesState.asData!.value; // Convert protobuf messages to chat messages - final chatMessages = []; + final chatMessages = []; final tsSet = {}; for (final message in messagesState.window) { final (newState, chatMessage) = @@ -390,11 +383,11 @@ class ChatComponentCubit extends Cubit { // '\nChatMessages:\n$chatMessages' ); } else { - chatMessages.insert(0, chatMessage); + chatMessages.add(chatMessage); } } return currentState.copyWith( - messageWindow: AsyncValue.data(WindowState( + messageWindow: AsyncValue.data(WindowState( window: chatMessages.toIList(), length: messagesState.length, windowTail: messagesState.windowTail, diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 66032f6..d93c4be 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -3,6 +3,8 @@ 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:uuid/uuid.dart'; +import 'package:uuid/v4.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; @@ -11,9 +13,12 @@ import '../../tools/tools.dart'; import '../models/models.dart'; import 'reconciliation/reconciliation.dart'; +const _sfSendMessageTag = 'sfSendMessageTag'; + class RenderStateElement { RenderStateElement( - {required this.message, + {required this.seqId, + required this.message, required this.isLocal, this.reconciledTimestamp, this.sent = false, @@ -36,6 +41,7 @@ class RenderStateElement { return null; } + int seqId; proto.Message message; bool isLocal; Timestamp? reconciledTimestamp; @@ -71,6 +77,8 @@ class SingleContactMessagesCubit extends Cubit { Future close() async { await _initWait(); + await serialFutureClose((this, _sfSendMessageTag)); + await _commandController.close(); await _commandRunnerFut; await _unsentMessagesQueue.close(); @@ -309,9 +317,6 @@ class SingleContactMessagesCubit extends Cubit { // Async process to send messages in the background Future _processUnsentMessages(IList messages) async { - // _sendingMessages = messages; - - // _renderState(); try { await _sentMessagesDHTLog!.operateAppendEventual((writer) async { // Get the previous message if we have one @@ -337,8 +342,6 @@ class SingleContactMessagesCubit extends Cubit { } on Exception catch (e, st) { log.error('Exception appending unsent messages: $e:\n$st\n'); } - - // _sendingMessages = const IList.empty(); } // Produce a state for this cubit from the input cubits and queues @@ -349,8 +352,9 @@ class SingleContactMessagesCubit extends Cubit { // Get all sent messages that are still offline //final sentMessages = _sentMessagesDHTLog. - //Get all items in the unsent queue - //final unsentMessages = _unsentMessagesQueue.queue; + + // Get all items in the unsent queue + final unsentMessages = _unsentMessagesQueue.queue; // If we aren't ready to render a state, say we're loading if (reconciledMessages == null) { @@ -374,8 +378,19 @@ class SingleContactMessagesCubit extends Cubit { // values: unsentMessages, // ); + // List of all rendered state elements that we will turn into + // message states final renderedElements = []; + + // Keep track of the ids we have rendered + // because there can be an overlap between the 'unsent messages' + // and the reconciled messages as the async state catches up final renderedIds = {}; + + var seqId = (reconciledMessages.windowTail == 0 + ? reconciledMessages.length + : reconciledMessages.windowTail) - + reconciledMessages.windowElements.length; for (final m in reconciledMessages.windowElements) { final isLocal = m.content.author.toVeilid() == _accountInfo.identityTypedPublicKey; @@ -387,33 +402,44 @@ class SingleContactMessagesCubit extends Cubit { final sent = isLocal; final sentOffline = false; // + if (renderedIds.contains(m.content.authorUniqueIdString)) { + seqId++; + continue; + } renderedElements.add(RenderStateElement( + seqId: seqId, message: m.content, isLocal: isLocal, reconciledTimestamp: reconciledTimestamp, sent: sent, sentOffline: sentOffline, )); - renderedIds.add(m.content.authorUniqueIdString); + seqId++; } // Render in-flight messages at the bottom - // for (final m in _sendingMessages) { + // + // for (final m in unsentMessages) { // if (renderedIds.contains(m.authorUniqueIdString)) { + // seqId++; // continue; // } // renderedElements.add(RenderStateElement( + // seqId: seqId, // message: m, // isLocal: true, // sent: true, // sentOffline: true, // )); + // renderedIds.add(m.authorUniqueIdString); + // seqId++; // } // Render the state final messages = renderedElements .map((x) => MessageState( + seqId: x.seqId, content: x.message, sentTimestamp: Timestamp.fromInt64(x.message.timestamp), reconciledTimestamp: x.reconciledTimestamp, @@ -431,20 +457,26 @@ class SingleContactMessagesCubit extends Cubit { void _sendMessage({required proto.Message message}) { // Add common fields - // id and signature will get set by _processMessageToSend + // real id and signature will get set by _processMessageToSend + // temporary id set here is random and not 'valid' in the eyes + // of reconcilation, noting that reconciled timestamp is not yet set. message ..author = _accountInfo.identityTypedPublicKey.toProto() - ..timestamp = Veilid.instance.now().toInt64(); + ..timestamp = Veilid.instance.now().toInt64() + ..id = Uuid.parse(_uuidGen.generate()); if ((message.writeToBuffer().lengthInBytes + 256) > 4096) { throw const FormatException('message is too long'); } // Put in the queue - _unsentMessagesQueue.addSync(message); + serialFuture((this, _sfSendMessageTag), () async { + // Add the message to the persistent queue + await _unsentMessagesQueue.add(message); - // Update the view - _renderState(); + // Update the view + _renderState(); + }); } Future _commandRunner() async { @@ -487,7 +519,6 @@ class SingleContactMessagesCubit extends Cubit { late final MessageReconciliation _reconciliation; late final PersistentQueue _unsentMessagesQueue; - // IList _sendingMessages = const IList.empty(); StreamSubscription? _sentSubscription; StreamSubscription? _rcvdSubscription; StreamSubscription>? @@ -496,4 +527,5 @@ class SingleContactMessagesCubit extends Cubit { late final Future _commandRunnerFut; final _sspRemoteConversationRecordKey = SingleStateProcessor(); + final _uuidGen = const UuidV4(); } diff --git a/lib/chat/models/chat_component_state.dart b/lib/chat/models/chat_component_state.dart index 82e492d..1a52524 100644 --- a/lib/chat/models/chat_component_state.dart +++ b/lib/chat/models/chat_component_state.dart @@ -1,12 +1,7 @@ import 'package:async_tools/async_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_chat_types/flutter_chat_types.dart' show Message, User; -import 'package:flutter_chat_ui/flutter_chat_ui.dart' - show ChatState, InputTextFieldController; +import 'package:flutter_chat_core/flutter_chat_core.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:scroll_to_index/scroll_to_index.dart'; -import 'package:veilid_support/veilid_support.dart'; import 'window_state.dart'; @@ -16,26 +11,16 @@ part 'chat_component_state.freezed.dart'; sealed class ChatComponentState with _$ChatComponentState { const factory ChatComponentState( { - // GlobalKey for the chat - required GlobalKey chatKey, - // ScrollController for the chat - required AutoScrollController scrollController, - // TextEditingController for the chat - required InputTextFieldController textEditingController, // Local user required User? localUser, // Active remote users - required IMap remoteUsers, + required IMap remoteUsers, // Historical remote users - required IMap historicalRemoteUsers, + required IMap historicalRemoteUsers, // Unknown users - required IMap unknownUsers, + required IMap unknownUsers, // Messages state required AsyncValue> messageWindow, // Title of the chat required String title}) = _ChatComponentState; } - -extension ChatComponentStateExt on ChatComponentState { - // -} diff --git a/lib/chat/models/chat_component_state.freezed.dart b/lib/chat/models/chat_component_state.freezed.dart index ae3acee..dd5e68e 100644 --- a/lib/chat/models/chat_component_state.freezed.dart +++ b/lib/chat/models/chat_component_state.freezed.dart @@ -15,15 +15,11 @@ T _$identity(T value) => value; /// @nodoc mixin _$ChatComponentState { -// GlobalKey for the chat - GlobalKey get chatKey; // ScrollController for the chat - AutoScrollController - get scrollController; // TextEditingController for the chat - InputTextFieldController get textEditingController; // Local user +// Local user User? get localUser; // Active remote users - IMap get remoteUsers; // Historical remote users - IMap get historicalRemoteUsers; // Unknown users - IMap get unknownUsers; // Messages state + IMap get remoteUsers; // Historical remote users + IMap get historicalRemoteUsers; // Unknown users + IMap get unknownUsers; // Messages state AsyncValue> get messageWindow; // Title of the chat String get title; @@ -40,11 +36,6 @@ mixin _$ChatComponentState { return identical(this, other) || (other.runtimeType == runtimeType && other is ChatComponentState && - (identical(other.chatKey, chatKey) || other.chatKey == chatKey) && - (identical(other.scrollController, scrollController) || - other.scrollController == scrollController) && - (identical(other.textEditingController, textEditingController) || - other.textEditingController == textEditingController) && (identical(other.localUser, localUser) || other.localUser == localUser) && (identical(other.remoteUsers, remoteUsers) || @@ -59,21 +50,12 @@ mixin _$ChatComponentState { } @override - int get hashCode => Object.hash( - runtimeType, - chatKey, - scrollController, - textEditingController, - localUser, - remoteUsers, - historicalRemoteUsers, - unknownUsers, - messageWindow, - title); + int get hashCode => Object.hash(runtimeType, localUser, remoteUsers, + historicalRemoteUsers, unknownUsers, messageWindow, title); @override String toString() { - return 'ChatComponentState(chatKey: $chatKey, scrollController: $scrollController, textEditingController: $textEditingController, localUser: $localUser, remoteUsers: $remoteUsers, historicalRemoteUsers: $historicalRemoteUsers, unknownUsers: $unknownUsers, messageWindow: $messageWindow, title: $title)'; + return 'ChatComponentState(localUser: $localUser, remoteUsers: $remoteUsers, historicalRemoteUsers: $historicalRemoteUsers, unknownUsers: $unknownUsers, messageWindow: $messageWindow, title: $title)'; } } @@ -84,16 +66,14 @@ abstract mixin class $ChatComponentStateCopyWith<$Res> { _$ChatComponentStateCopyWithImpl; @useResult $Res call( - {GlobalKey chatKey, - AutoScrollController scrollController, - InputTextFieldController textEditingController, - User? localUser, - IMap, User> remoteUsers, - IMap, User> historicalRemoteUsers, - IMap, User> unknownUsers, + {User? localUser, + IMap remoteUsers, + IMap historicalRemoteUsers, + IMap unknownUsers, AsyncValue> messageWindow, String title}); + $UserCopyWith<$Res>? get localUser; $AsyncValueCopyWith, $Res> get messageWindow; } @@ -110,9 +90,6 @@ class _$ChatComponentStateCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? chatKey = null, - Object? scrollController = null, - Object? textEditingController = null, Object? localUser = freezed, Object? remoteUsers = null, Object? historicalRemoteUsers = null, @@ -121,18 +98,6 @@ class _$ChatComponentStateCopyWithImpl<$Res> Object? title = null, }) { return _then(_self.copyWith( - chatKey: null == chatKey - ? _self.chatKey - : chatKey // ignore: cast_nullable_to_non_nullable - as GlobalKey, - scrollController: null == scrollController - ? _self.scrollController - : scrollController // ignore: cast_nullable_to_non_nullable - as AutoScrollController, - textEditingController: null == textEditingController - ? _self.textEditingController - : textEditingController // ignore: cast_nullable_to_non_nullable - as InputTextFieldController, localUser: freezed == localUser ? _self.localUser : localUser // ignore: cast_nullable_to_non_nullable @@ -140,15 +105,15 @@ class _$ChatComponentStateCopyWithImpl<$Res> remoteUsers: null == remoteUsers ? _self.remoteUsers! : remoteUsers // ignore: cast_nullable_to_non_nullable - as IMap, User>, + as IMap, historicalRemoteUsers: null == historicalRemoteUsers ? _self.historicalRemoteUsers! : historicalRemoteUsers // ignore: cast_nullable_to_non_nullable - as IMap, User>, + as IMap, unknownUsers: null == unknownUsers ? _self.unknownUsers! : unknownUsers // ignore: cast_nullable_to_non_nullable - as IMap, User>, + as IMap, messageWindow: null == messageWindow ? _self.messageWindow : messageWindow // ignore: cast_nullable_to_non_nullable @@ -160,6 +125,20 @@ class _$ChatComponentStateCopyWithImpl<$Res> )); } + /// Create a copy of ChatComponentState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $UserCopyWith<$Res>? get localUser { + if (_self.localUser == null) { + return null; + } + + return $UserCopyWith<$Res>(_self.localUser!, (value) { + return _then(_self.copyWith(localUser: value)); + }); + } + /// Create a copy of ChatComponentState /// with the given fields replaced by the non-null parameter values. @override @@ -176,37 +155,25 @@ class _$ChatComponentStateCopyWithImpl<$Res> class _ChatComponentState implements ChatComponentState { const _ChatComponentState( - {required this.chatKey, - required this.scrollController, - required this.textEditingController, - required this.localUser, + {required this.localUser, required this.remoteUsers, required this.historicalRemoteUsers, required this.unknownUsers, required this.messageWindow, required this.title}); -// GlobalKey for the chat - @override - final GlobalKey chatKey; -// ScrollController for the chat - @override - final AutoScrollController scrollController; -// TextEditingController for the chat - @override - final InputTextFieldController textEditingController; // Local user @override final User? localUser; // Active remote users @override - final IMap, User> remoteUsers; + final IMap remoteUsers; // Historical remote users @override - final IMap, User> historicalRemoteUsers; + final IMap historicalRemoteUsers; // Unknown users @override - final IMap, User> unknownUsers; + final IMap unknownUsers; // Messages state @override final AsyncValue> messageWindow; @@ -227,11 +194,6 @@ class _ChatComponentState implements ChatComponentState { return identical(this, other) || (other.runtimeType == runtimeType && other is _ChatComponentState && - (identical(other.chatKey, chatKey) || other.chatKey == chatKey) && - (identical(other.scrollController, scrollController) || - other.scrollController == scrollController) && - (identical(other.textEditingController, textEditingController) || - other.textEditingController == textEditingController) && (identical(other.localUser, localUser) || other.localUser == localUser) && (identical(other.remoteUsers, remoteUsers) || @@ -246,21 +208,12 @@ class _ChatComponentState implements ChatComponentState { } @override - int get hashCode => Object.hash( - runtimeType, - chatKey, - scrollController, - textEditingController, - localUser, - remoteUsers, - historicalRemoteUsers, - unknownUsers, - messageWindow, - title); + int get hashCode => Object.hash(runtimeType, localUser, remoteUsers, + historicalRemoteUsers, unknownUsers, messageWindow, title); @override String toString() { - return 'ChatComponentState(chatKey: $chatKey, scrollController: $scrollController, textEditingController: $textEditingController, localUser: $localUser, remoteUsers: $remoteUsers, historicalRemoteUsers: $historicalRemoteUsers, unknownUsers: $unknownUsers, messageWindow: $messageWindow, title: $title)'; + return 'ChatComponentState(localUser: $localUser, remoteUsers: $remoteUsers, historicalRemoteUsers: $historicalRemoteUsers, unknownUsers: $unknownUsers, messageWindow: $messageWindow, title: $title)'; } } @@ -273,16 +226,15 @@ abstract mixin class _$ChatComponentStateCopyWith<$Res> @override @useResult $Res call( - {GlobalKey chatKey, - AutoScrollController scrollController, - InputTextFieldController textEditingController, - User? localUser, - IMap, User> remoteUsers, - IMap, User> historicalRemoteUsers, - IMap, User> unknownUsers, + {User? localUser, + IMap remoteUsers, + IMap historicalRemoteUsers, + IMap unknownUsers, AsyncValue> messageWindow, String title}); + @override + $UserCopyWith<$Res>? get localUser; @override $AsyncValueCopyWith, $Res> get messageWindow; } @@ -300,9 +252,6 @@ class __$ChatComponentStateCopyWithImpl<$Res> @override @pragma('vm:prefer-inline') $Res call({ - Object? chatKey = null, - Object? scrollController = null, - Object? textEditingController = null, Object? localUser = freezed, Object? remoteUsers = null, Object? historicalRemoteUsers = null, @@ -311,18 +260,6 @@ class __$ChatComponentStateCopyWithImpl<$Res> Object? title = null, }) { return _then(_ChatComponentState( - chatKey: null == chatKey - ? _self.chatKey - : chatKey // ignore: cast_nullable_to_non_nullable - as GlobalKey, - scrollController: null == scrollController - ? _self.scrollController - : scrollController // ignore: cast_nullable_to_non_nullable - as AutoScrollController, - textEditingController: null == textEditingController - ? _self.textEditingController - : textEditingController // ignore: cast_nullable_to_non_nullable - as InputTextFieldController, localUser: freezed == localUser ? _self.localUser : localUser // ignore: cast_nullable_to_non_nullable @@ -330,15 +267,15 @@ class __$ChatComponentStateCopyWithImpl<$Res> remoteUsers: null == remoteUsers ? _self.remoteUsers : remoteUsers // ignore: cast_nullable_to_non_nullable - as IMap, User>, + as IMap, historicalRemoteUsers: null == historicalRemoteUsers ? _self.historicalRemoteUsers : historicalRemoteUsers // ignore: cast_nullable_to_non_nullable - as IMap, User>, + as IMap, unknownUsers: null == unknownUsers ? _self.unknownUsers : unknownUsers // ignore: cast_nullable_to_non_nullable - as IMap, User>, + as IMap, messageWindow: null == messageWindow ? _self.messageWindow : messageWindow // ignore: cast_nullable_to_non_nullable @@ -350,6 +287,20 @@ class __$ChatComponentStateCopyWithImpl<$Res> )); } + /// Create a copy of ChatComponentState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $UserCopyWith<$Res>? get localUser { + if (_self.localUser == null) { + return null; + } + + return $UserCopyWith<$Res>(_self.localUser!, (value) { + return _then(_self.copyWith(localUser: value)); + }); + } + /// Create a copy of ChatComponentState /// with the given fields replaced by the non-null parameter values. @override diff --git a/lib/chat/models/message_state.dart b/lib/chat/models/message_state.dart index cf82021..80852e6 100644 --- a/lib/chat/models/message_state.dart +++ b/lib/chat/models/message_state.dart @@ -25,7 +25,10 @@ enum MessageSendState { @freezed sealed class MessageState with _$MessageState { + @JsonSerializable() const factory MessageState({ + // Sequence number of the message for display purposes + required int seqId, // Content of the message @JsonKey(fromJson: messageFromJson, toJson: messageToJson) required proto.Message content, diff --git a/lib/chat/models/message_state.freezed.dart b/lib/chat/models/message_state.freezed.dart index 0900f8b..342b305 100644 --- a/lib/chat/models/message_state.freezed.dart +++ b/lib/chat/models/message_state.freezed.dart @@ -15,7 +15,8 @@ T _$identity(T value) => value; /// @nodoc mixin _$MessageState implements DiagnosticableTreeMixin { -// Content of the message +// Sequence number of the message for display purposes + int get seqId; // Content of the message @JsonKey(fromJson: messageFromJson, toJson: messageToJson) proto.Message get content; // Sent timestamp Timestamp get sentTimestamp; // Reconciled timestamp @@ -37,6 +38,7 @@ mixin _$MessageState implements DiagnosticableTreeMixin { void debugFillProperties(DiagnosticPropertiesBuilder properties) { properties ..add(DiagnosticsProperty('type', 'MessageState')) + ..add(DiagnosticsProperty('seqId', seqId)) ..add(DiagnosticsProperty('content', content)) ..add(DiagnosticsProperty('sentTimestamp', sentTimestamp)) ..add(DiagnosticsProperty('reconciledTimestamp', reconciledTimestamp)) @@ -48,6 +50,7 @@ mixin _$MessageState implements DiagnosticableTreeMixin { return identical(this, other) || (other.runtimeType == runtimeType && other is MessageState && + (identical(other.seqId, seqId) || other.seqId == seqId) && (identical(other.content, content) || other.content == content) && (identical(other.sentTimestamp, sentTimestamp) || other.sentTimestamp == sentTimestamp) && @@ -59,12 +62,12 @@ mixin _$MessageState implements DiagnosticableTreeMixin { @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash( - runtimeType, content, sentTimestamp, reconciledTimestamp, sendState); + int get hashCode => Object.hash(runtimeType, seqId, content, sentTimestamp, + reconciledTimestamp, sendState); @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'MessageState(content: $content, sentTimestamp: $sentTimestamp, reconciledTimestamp: $reconciledTimestamp, sendState: $sendState)'; + return 'MessageState(seqId: $seqId, content: $content, sentTimestamp: $sentTimestamp, reconciledTimestamp: $reconciledTimestamp, sendState: $sendState)'; } } @@ -75,7 +78,8 @@ abstract mixin class $MessageStateCopyWith<$Res> { _$MessageStateCopyWithImpl; @useResult $Res call( - {@JsonKey(fromJson: messageFromJson, toJson: messageToJson) + {int seqId, + @JsonKey(fromJson: messageFromJson, toJson: messageToJson) proto.Message content, Timestamp sentTimestamp, Timestamp? reconciledTimestamp, @@ -94,12 +98,17 @@ class _$MessageStateCopyWithImpl<$Res> implements $MessageStateCopyWith<$Res> { @pragma('vm:prefer-inline') @override $Res call({ + Object? seqId = null, Object? content = null, Object? sentTimestamp = null, Object? reconciledTimestamp = freezed, Object? sendState = freezed, }) { return _then(_self.copyWith( + seqId: null == seqId + ? _self.seqId + : seqId // ignore: cast_nullable_to_non_nullable + as int, content: null == content ? _self.content : content // ignore: cast_nullable_to_non_nullable @@ -121,10 +130,12 @@ class _$MessageStateCopyWithImpl<$Res> implements $MessageStateCopyWith<$Res> { } /// @nodoc + @JsonSerializable() class _MessageState with DiagnosticableTreeMixin implements MessageState { const _MessageState( - {@JsonKey(fromJson: messageFromJson, toJson: messageToJson) + {required this.seqId, + @JsonKey(fromJson: messageFromJson, toJson: messageToJson) required this.content, required this.sentTimestamp, required this.reconciledTimestamp, @@ -132,6 +143,9 @@ class _MessageState with DiagnosticableTreeMixin implements MessageState { factory _MessageState.fromJson(Map json) => _$MessageStateFromJson(json); +// Sequence number of the message for display purposes + @override + final int seqId; // Content of the message @override @JsonKey(fromJson: messageFromJson, toJson: messageToJson) @@ -165,6 +179,7 @@ class _MessageState with DiagnosticableTreeMixin implements MessageState { void debugFillProperties(DiagnosticPropertiesBuilder properties) { properties ..add(DiagnosticsProperty('type', 'MessageState')) + ..add(DiagnosticsProperty('seqId', seqId)) ..add(DiagnosticsProperty('content', content)) ..add(DiagnosticsProperty('sentTimestamp', sentTimestamp)) ..add(DiagnosticsProperty('reconciledTimestamp', reconciledTimestamp)) @@ -176,6 +191,7 @@ class _MessageState with DiagnosticableTreeMixin implements MessageState { return identical(this, other) || (other.runtimeType == runtimeType && other is _MessageState && + (identical(other.seqId, seqId) || other.seqId == seqId) && (identical(other.content, content) || other.content == content) && (identical(other.sentTimestamp, sentTimestamp) || other.sentTimestamp == sentTimestamp) && @@ -187,12 +203,12 @@ class _MessageState with DiagnosticableTreeMixin implements MessageState { @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash( - runtimeType, content, sentTimestamp, reconciledTimestamp, sendState); + int get hashCode => Object.hash(runtimeType, seqId, content, sentTimestamp, + reconciledTimestamp, sendState); @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'MessageState(content: $content, sentTimestamp: $sentTimestamp, reconciledTimestamp: $reconciledTimestamp, sendState: $sendState)'; + return 'MessageState(seqId: $seqId, content: $content, sentTimestamp: $sentTimestamp, reconciledTimestamp: $reconciledTimestamp, sendState: $sendState)'; } } @@ -205,7 +221,8 @@ abstract mixin class _$MessageStateCopyWith<$Res> @override @useResult $Res call( - {@JsonKey(fromJson: messageFromJson, toJson: messageToJson) + {int seqId, + @JsonKey(fromJson: messageFromJson, toJson: messageToJson) proto.Message content, Timestamp sentTimestamp, Timestamp? reconciledTimestamp, @@ -225,12 +242,17 @@ class __$MessageStateCopyWithImpl<$Res> @override @pragma('vm:prefer-inline') $Res call({ + Object? seqId = null, Object? content = null, Object? sentTimestamp = null, Object? reconciledTimestamp = freezed, Object? sendState = freezed, }) { return _then(_MessageState( + seqId: null == seqId + ? _self.seqId + : seqId // ignore: cast_nullable_to_non_nullable + as int, content: null == content ? _self.content : content // ignore: cast_nullable_to_non_nullable diff --git a/lib/chat/models/message_state.g.dart b/lib/chat/models/message_state.g.dart index daae37f..2eee78d 100644 --- a/lib/chat/models/message_state.g.dart +++ b/lib/chat/models/message_state.g.dart @@ -8,6 +8,7 @@ part of 'message_state.dart'; _MessageState _$MessageStateFromJson(Map json) => _MessageState( + seqId: (json['seq_id'] as num).toInt(), content: messageFromJson(json['content'] as Map), sentTimestamp: Timestamp.fromJson(json['sent_timestamp']), reconciledTimestamp: json['reconciled_timestamp'] == null @@ -20,6 +21,7 @@ _MessageState _$MessageStateFromJson(Map json) => Map _$MessageStateToJson(_MessageState instance) => { + 'seq_id': instance.seqId, 'content': messageToJson(instance.content), 'sent_timestamp': instance.sentTimestamp.toJson(), 'reconciled_timestamp': instance.reconciledTimestamp?.toJson(), diff --git a/lib/chat/views/chat_builders/chat_builders.dart b/lib/chat/views/chat_builders/chat_builders.dart new file mode 100644 index 0000000..529341f --- /dev/null +++ b/lib/chat/views/chat_builders/chat_builders.dart @@ -0,0 +1,2 @@ +export 'vc_composer_widget.dart'; +export 'vc_text_message_widget.dart'; diff --git a/lib/chat/views/chat_builders/vc_composer_widget.dart b/lib/chat/views/chat_builders/vc_composer_widget.dart new file mode 100644 index 0000000..b3eb1e5 --- /dev/null +++ b/lib/chat/views/chat_builders/vc_composer_widget.dart @@ -0,0 +1,431 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_chat_ui/flutter_chat_ui.dart'; +// Typedefs need to come out +// ignore: implementation_imports +import 'package:flutter_chat_ui/src/utils/typedefs.dart'; +import 'package:provider/provider.dart'; + +import '../../../theme/theme.dart'; +import '../../chat.dart'; + +enum ShiftEnterAction { newline, send } + +/// The message composer widget positioned at the bottom of the chat screen. +/// +/// Includes a text input field, an optional attachment button, +/// and a send button. +class VcComposerWidget extends StatefulWidget { + /// Creates a message composer widget. + const VcComposerWidget({ + super.key, + this.textEditingController, + this.left = 0, + this.right = 0, + this.top, + this.bottom = 0, + this.sigmaX = 20, + this.sigmaY = 20, + this.padding = const EdgeInsets.all(8), + this.attachmentIcon = const Icon(Icons.attachment), + this.sendIcon = const Icon(Icons.send), + this.gap = 8, + this.inputBorder, + this.filled, + this.topWidget, + this.handleSafeArea = true, + this.backgroundColor, + this.attachmentIconColor, + this.sendIconColor, + this.hintColor, + this.textColor, + this.inputFillColor, + this.hintText = 'Type a message', + this.keyboardAppearance, + this.autocorrect, + this.autofocus = false, + this.textCapitalization = TextCapitalization.sentences, + this.keyboardType, + this.textInputAction = TextInputAction.newline, + this.shiftEnterAction = ShiftEnterAction.send, + this.focusNode, + this.maxLength, + this.minLines = 1, + this.maxLines = 3, + }); + + /// Optional controller for the text input field. + final TextEditingController? textEditingController; + + /// Optional left position. + final double? left; + + /// Optional right position. + final double? right; + + /// Optional top position. + final double? top; + + /// Optional bottom position. + final double? bottom; + + /// Optional X blur value for the background (if using glassmorphism). + final double? sigmaX; + + /// Optional Y blur value for the background (if using glassmorphism). + final double? sigmaY; + + /// Padding around the composer content. + final EdgeInsetsGeometry? padding; + + /// Icon for the attachment button. Defaults to [Icons.attachment]. + final Widget? attachmentIcon; + + /// Icon for the send button. Defaults to [Icons.send]. + final Widget? sendIcon; + + /// Horizontal gap between elements (attachment icon, text field, send icon). + final double? gap; + + /// Border style for the text input field. + final InputBorder? inputBorder; + + /// Whether the text input field should be filled. + final bool? filled; + + /// Optional widget to display above the main composer row. + final Widget? topWidget; + + /// Whether to adjust padding for the bottom safe area. + final bool handleSafeArea; + + /// Background color of the composer container. + final Color? backgroundColor; + + /// Color of the attachment icon. + final Color? attachmentIconColor; + + /// Color of the send icon. + final Color? sendIconColor; + + /// Color of the hint text in the input field. + final Color? hintColor; + + /// Color of the text entered in the input field. + final Color? textColor; + + /// Fill color for the text input field when [filled] is true. + final Color? inputFillColor; + + /// Placeholder text for the input field. + final String? hintText; + + /// Appearance of the keyboard. + final Brightness? keyboardAppearance; + + /// Whether to enable autocorrect for the input field. + final bool? autocorrect; + + /// Whether the input field should autofocus. + final bool autofocus; + + /// Capitalization behavior for the input field. + final TextCapitalization textCapitalization; + + /// Type of keyboard to display. + final TextInputType? keyboardType; + + /// Action button type for the keyboard (e.g., newline, send). + final TextInputAction textInputAction; + + /// Action when shift-enter is pressed (e.g., newline, send). + final ShiftEnterAction shiftEnterAction; + + /// Focus node for the text input field. + final FocusNode? focusNode; + + /// Maximum character length for the input field. + final int? maxLength; + + /// Minimum number of lines for the input field. + final int? minLines; + + /// Maximum number of lines the input field can expand to. + final int? maxLines; + + @override + State createState() => _VcComposerState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty( + 'textEditingController', textEditingController)) + ..add(DoubleProperty('left', left)) + ..add(DoubleProperty('right', right)) + ..add(DoubleProperty('top', top)) + ..add(DoubleProperty('bottom', bottom)) + ..add(DoubleProperty('sigmaX', sigmaX)) + ..add(DoubleProperty('sigmaY', sigmaY)) + ..add(DiagnosticsProperty('padding', padding)) + ..add(DoubleProperty('gap', gap)) + ..add(DiagnosticsProperty('inputBorder', inputBorder)) + ..add(DiagnosticsProperty('filled', filled)) + ..add(DiagnosticsProperty('handleSafeArea', handleSafeArea)) + ..add(ColorProperty('backgroundColor', backgroundColor)) + ..add(ColorProperty('attachmentIconColor', attachmentIconColor)) + ..add(ColorProperty('sendIconColor', sendIconColor)) + ..add(ColorProperty('hintColor', hintColor)) + ..add(ColorProperty('textColor', textColor)) + ..add(ColorProperty('inputFillColor', inputFillColor)) + ..add(StringProperty('hintText', hintText)) + ..add(EnumProperty('keyboardAppearance', keyboardAppearance)) + ..add(DiagnosticsProperty('autocorrect', autocorrect)) + ..add(DiagnosticsProperty('autofocus', autofocus)) + ..add(EnumProperty( + 'textCapitalization', textCapitalization)) + ..add(DiagnosticsProperty('keyboardType', keyboardType)) + ..add(EnumProperty('textInputAction', textInputAction)) + ..add( + EnumProperty('shiftEnterAction', shiftEnterAction)) + ..add(DiagnosticsProperty('focusNode', focusNode)) + ..add(IntProperty('maxLength', maxLength)) + ..add(IntProperty('minLines', minLines)) + ..add(IntProperty('maxLines', maxLines)); + } +} + +class _VcComposerState extends State { + final _key = GlobalKey(); + late final TextEditingController _textController; + late final FocusNode _focusNode; + late String _suffixText; + + @override + void initState() { + super.initState(); + _textController = widget.textEditingController ?? TextEditingController(); + _focusNode = widget.focusNode ?? FocusNode(); + _focusNode.onKeyEvent = _handleKeyEvent; + _updateSuffixText(); + WidgetsBinding.instance.addPostFrameCallback((_) => _measure()); + } + + void _updateSuffixText() { + final utf8Length = utf8.encode(_textController.text).length; + _suffixText = '$utf8Length/${widget.maxLength}'; + } + + KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) { + // Check for Shift+Enter + if (event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.enter && + HardwareKeyboard.instance.isShiftPressed) { + if (widget.shiftEnterAction == ShiftEnterAction.send) { + _handleSubmitted(_textController.text); + return KeyEventResult.handled; + } else if (widget.shiftEnterAction == ShiftEnterAction.newline) { + final val = _textController.value; + final insertOffset = val.selection.extent.offset; + final messageWithNewLine = + '${_textController.text.substring(0, insertOffset)}\n' + '${_textController.text.substring(insertOffset)}'; + _textController.value = TextEditingValue( + text: messageWithNewLine, + selection: TextSelection.fromPosition( + TextPosition(offset: insertOffset + 1), + ), + ); + return KeyEventResult.handled; + } + } + return KeyEventResult.ignored; + } + + @override + void didUpdateWidget(covariant VcComposerWidget oldWidget) { + super.didUpdateWidget(oldWidget); + WidgetsBinding.instance.addPostFrameCallback((_) => _measure()); + } + + @override + void dispose() { + // Only try to dispose text controller if it's not provided, let + // user handle disposing it how they want. + if (widget.textEditingController == null) { + _textController.dispose(); + } + if (widget.focusNode == null) { + _focusNode.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final bottomSafeArea = + widget.handleSafeArea ? MediaQuery.of(context).padding.bottom : 0.0; + final onAttachmentTap = context.read(); + final theme = Theme.of(context); + final scaleTheme = theme.extension()!; + final config = scaleTheme.config; + final scheme = scaleTheme.scheme; + final scale = scaleTheme.scheme.scale(ScaleKind.primary); + final textTheme = theme.textTheme; + final scaleChatTheme = scaleTheme.chatTheme(); + final chatTheme = scaleChatTheme.chatTheme; + + final suffixTextStyle = + textTheme.bodySmall!.copyWith(color: scale.subtleText); + + return Positioned( + left: widget.left, + right: widget.right, + top: widget.top, + bottom: widget.bottom, + child: ClipRect( + child: DecoratedBox( + key: _key, + decoration: BoxDecoration( + border: config.preferBorders + ? Border(top: BorderSide(color: scale.border, width: 2)) + : null, + color: config.preferBorders + ? scale.elementBackground + : scale.border), + child: Column( + children: [ + if (widget.topWidget != null) widget.topWidget!, + Padding( + padding: widget.handleSafeArea + ? (widget.padding?.add( + EdgeInsets.only(bottom: bottomSafeArea), + ) ?? + EdgeInsets.only(bottom: bottomSafeArea)) + : (widget.padding ?? EdgeInsets.zero), + child: Row( + children: [ + if (widget.attachmentIcon != null && + onAttachmentTap != null) + IconButton( + icon: widget.attachmentIcon!, + color: widget.attachmentIconColor ?? + chatTheme.colors.onSurface.withValues(alpha: 0.5), + onPressed: onAttachmentTap, + ) + else + const SizedBox.shrink(), + SizedBox(width: widget.gap), + Expanded( + child: TextField( + controller: _textController, + decoration: InputDecoration( + filled: widget.filled ?? !config.preferBorders, + fillColor: widget.inputFillColor ?? + scheme.primaryScale.subtleBackground, + isDense: true, + contentPadding: + const EdgeInsets.fromLTRB(8, 8, 8, 8), + disabledBorder: OutlineInputBorder( + borderSide: config.preferBorders + ? BorderSide( + color: scheme.grayScale.border, + width: 2) + : BorderSide.none, + borderRadius: BorderRadius.all(Radius.circular( + 8 * config.borderRadiusScale))), + enabledBorder: OutlineInputBorder( + borderSide: config.preferBorders + ? BorderSide( + color: scheme.primaryScale.border, + width: 2) + : BorderSide.none, + borderRadius: BorderRadius.all(Radius.circular( + 8 * config.borderRadiusScale))), + focusedBorder: OutlineInputBorder( + borderSide: config.preferBorders + ? BorderSide( + color: scheme.primaryScale.border, + width: 2) + : BorderSide.none, + borderRadius: BorderRadius.all(Radius.circular( + 8 * config.borderRadiusScale))), + hintText: widget.hintText, + hintStyle: chatTheme.typography.bodyMedium.copyWith( + color: widget.hintColor ?? + chatTheme.colors.onSurface + .withValues(alpha: 0.5), + ), + border: widget.inputBorder, + hoverColor: Colors.transparent, + suffix: Text(_suffixText, style: suffixTextStyle)), + onSubmitted: _handleSubmitted, + onChanged: (value) { + setState(_updateSuffixText); + }, + textInputAction: widget.textInputAction, + keyboardAppearance: widget.keyboardAppearance, + autocorrect: widget.autocorrect ?? true, + autofocus: widget.autofocus, + textCapitalization: widget.textCapitalization, + keyboardType: widget.keyboardType, + focusNode: _focusNode, + //maxLength: widget.maxLength, + minLines: widget.minLines, + maxLines: widget.maxLines, + maxLengthEnforcement: MaxLengthEnforcement.none, + inputFormatters: [ + Utf8LengthLimitingTextInputFormatter( + maxLength: widget.maxLength), + ], + ), + ), + SizedBox(width: widget.gap), + if ((widget.sendIcon ?? scaleChatTheme.sendButtonIcon) != + null) + IconButton( + icon: + (widget.sendIcon ?? scaleChatTheme.sendButtonIcon)!, + color: widget.sendIconColor, + onPressed: () => _handleSubmitted(_textController.text), + ) + else + const SizedBox.shrink(), + ], + ), + ), + ], + ), + ), + ), + ); + } + + void _measure() { + if (!mounted) { + return; + } + + final renderBox = _key.currentContext?.findRenderObject() as RenderBox?; + if (renderBox != null) { + final height = renderBox.size.height; + final bottomSafeArea = MediaQuery.of(context).padding.bottom; + + context.read().setHeight( + // only set real height of the composer, ignoring safe area + widget.handleSafeArea ? height - bottomSafeArea : height, + ); + } + } + + void _handleSubmitted(String text) { + if (text.isNotEmpty) { + context.read()?.call(text); + _textController.clear(); + } + } +} diff --git a/lib/chat/views/chat_builders/vc_text_message_widget.dart b/lib/chat/views/chat_builders/vc_text_message_widget.dart new file mode 100644 index 0000000..fc1fe80 --- /dev/null +++ b/lib/chat/views/chat_builders/vc_text_message_widget.dart @@ -0,0 +1,269 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_chat_core/flutter_chat_core.dart'; +import 'package:provider/provider.dart'; + +import '../../../theme/theme.dart'; +import '../date_formatter.dart'; + +/// A widget that displays a text message. +class VcTextMessageWidget extends StatelessWidget { + /// Creates a widget to display a simple text message. + const VcTextMessageWidget({ + required this.message, + required this.index, + this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + this.borderRadius, + this.onlyEmojiFontSize, + this.sentBackgroundColor, + this.receivedBackgroundColor, + this.sentTextStyle, + this.receivedTextStyle, + this.timeStyle, + this.showTime = true, + this.showStatus = true, + this.timeAndStatusPosition = TimeAndStatusPosition.end, + super.key, + }); + + /// The text message data model. + final TextMessage message; + + /// The index of the message in the list. + final int index; + + /// Padding around the message bubble content. + final EdgeInsetsGeometry? padding; + + /// Border radius of the message bubble. + final BorderRadiusGeometry? borderRadius; + + /// Font size for messages containing only emojis. + final double? onlyEmojiFontSize; + + /// Background color for messages sent by the current user. + final Color? sentBackgroundColor; + + /// Background color for messages received from other users. + final Color? receivedBackgroundColor; + + /// Text style for messages sent by the current user. + final TextStyle? sentTextStyle; + + /// Text style for messages received from other users. + final TextStyle? receivedTextStyle; + + /// Text style for the message timestamp and status. + final TextStyle? timeStyle; + + /// Whether to display the message timestamp. + final bool showTime; + + /// Whether to display the message status (sent, delivered, seen) + /// for sent messages. + final bool showStatus; + + /// Position of the timestamp and status indicator relative to the text. + final TimeAndStatusPosition timeAndStatusPosition; + + bool get _isOnlyEmoji => message.metadata?['isOnlyEmoji'] == true; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scaleTheme = theme.extension()!; + final config = scaleTheme.config; + final scheme = scaleTheme.scheme; + final scale = scaleTheme.scheme.scale(ScaleKind.primary); + final textTheme = theme.textTheme; + final scaleChatTheme = scaleTheme.chatTheme(); + final chatTheme = scaleChatTheme.chatTheme; + + final isSentByMe = context.watch() == message.authorId; + final backgroundColor = _resolveBackgroundColor(isSentByMe, scaleChatTheme); + final textStyle = _resolveTextStyle(isSentByMe, scaleChatTheme); + final timeStyle = _resolveTimeStyle(isSentByMe, scaleChatTheme); + final emojiFontSize = onlyEmojiFontSize ?? scaleChatTheme.onlyEmojiFontSize; + + final timeAndStatus = showTime || (isSentByMe && showStatus) + ? TimeAndStatus( + time: message.time, + status: message.status, + showTime: showTime, + showStatus: isSentByMe && showStatus, + textStyle: timeStyle, + ) + : null; + + final textContent = Text( + message.text, + style: _isOnlyEmoji + ? textStyle.copyWith(fontSize: emojiFontSize) + : textStyle, + ); + + return Container( + padding: _isOnlyEmoji + ? EdgeInsets.symmetric( + horizontal: (padding?.horizontal ?? 0) / 2, + // vertical: 0, + ) + : padding, + decoration: _isOnlyEmoji + ? null + : BoxDecoration( + color: backgroundColor, + borderRadius: borderRadius ?? chatTheme.shape, + ), + child: _buildContentBasedOnPosition( + context: context, + textContent: textContent, + timeAndStatus: timeAndStatus, + textStyle: textStyle, + ), + ); + } + + Widget _buildContentBasedOnPosition({ + required BuildContext context, + required Widget textContent, + TimeAndStatus? timeAndStatus, + TextStyle? textStyle, + }) { + if (timeAndStatus == null) { + return textContent; + } + + switch (timeAndStatusPosition) { + case TimeAndStatusPosition.start: + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [textContent, timeAndStatus], + ); + case TimeAndStatusPosition.inline: + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Flexible(child: textContent), + const SizedBox(width: 4), + timeAndStatus, + ], + ); + case TimeAndStatusPosition.end: + return Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [textContent, timeAndStatus], + ); + } + } + + Color _resolveBackgroundColor(bool isSentByMe, ScaleChatTheme theme) { + if (isSentByMe) { + return sentBackgroundColor ?? theme.primaryColor; + } + return receivedBackgroundColor ?? theme.secondaryColor; + } + + TextStyle _resolveTextStyle(bool isSentByMe, ScaleChatTheme theme) { + if (isSentByMe) { + return sentTextStyle ?? theme.sentMessageBodyTextStyle; + } + return receivedTextStyle ?? theme.receivedMessageBodyTextStyle; + } + + TextStyle _resolveTimeStyle(bool isSentByMe, ScaleChatTheme theme) { + final ts = _resolveTextStyle(isSentByMe, theme); + + return theme.timeStyle.copyWith(color: ts.color); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('message', message)) + ..add(IntProperty('index', index)) + ..add(DiagnosticsProperty('padding', padding)) + ..add(DiagnosticsProperty( + 'borderRadius', borderRadius)) + ..add(DoubleProperty('onlyEmojiFontSize', onlyEmojiFontSize)) + ..add(ColorProperty('sentBackgroundColor', sentBackgroundColor)) + ..add(ColorProperty('receivedBackgroundColor', receivedBackgroundColor)) + ..add(DiagnosticsProperty('sentTextStyle', sentTextStyle)) + ..add(DiagnosticsProperty( + 'receivedTextStyle', receivedTextStyle)) + ..add(DiagnosticsProperty('timeStyle', timeStyle)) + ..add(DiagnosticsProperty('showTime', showTime)) + ..add(DiagnosticsProperty('showStatus', showStatus)) + ..add(EnumProperty( + 'timeAndStatusPosition', timeAndStatusPosition)); + } +} + +/// A widget to display the message timestamp and status indicator. +class TimeAndStatus extends StatelessWidget { + /// Creates a widget for displaying time and status. + const TimeAndStatus({ + required this.time, + this.status, + this.showTime = true, + this.showStatus = true, + this.textStyle, + super.key, + }); + + /// The time the message was created. + final DateTime? time; + + /// The status of the message. + final MessageStatus? status; + + /// Whether to display the timestamp. + final bool showTime; + + /// Whether to display the status indicator. + final bool showStatus; + + /// The text style for the time and status. + final TextStyle? textStyle; + + @override + Widget build(BuildContext context) { + final dformat = DateFormatter(); + + return Row( + spacing: 2, + mainAxisSize: MainAxisSize.min, + children: [ + if (showTime && time != null) + Text(dformat.chatDateTimeFormat(time!.toLocal()), style: textStyle), + if (showStatus && status != null) + if (status == MessageStatus.sending) + SizedBox( + width: 6, + height: 6, + child: CircularProgressIndicator( + color: textStyle?.color, + strokeWidth: 2, + ), + ) + else + Icon(getIconForStatus(status!), color: textStyle?.color, size: 12), + ], + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('time', time)) + ..add(EnumProperty('status', status)) + ..add(DiagnosticsProperty('showTime', showTime)) + ..add(DiagnosticsProperty('showStatus', showStatus)) + ..add(DiagnosticsProperty('textStyle', textStyle)); + } +} diff --git a/lib/chat/views/chat_component_widget.dart b/lib/chat/views/chat_component_widget.dart index 1646772..aed7356 100644 --- a/lib/chat/views/chat_component_widget.dart +++ b/lib/chat/views/chat_component_widget.dart @@ -1,11 +1,13 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:math'; 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; +import 'package:flutter_chat_core/flutter_chat_core.dart' as core; import 'package:flutter_chat_ui/flutter_chat_ui.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:veilid_support/veilid_support.dart'; @@ -16,11 +18,15 @@ import '../../conversation/conversation.dart'; import '../../notifications/notifications.dart'; import '../../theme/theme.dart'; import '../chat.dart'; +import 'chat_builders/chat_builders.dart'; const onEndReachedThreshold = 0.75; +const _kScrollTag = 'kScrollTag'; +const kSeqId = 'seqId'; +const maxMessageLength = 2048; -class ChatComponentWidget extends StatelessWidget { - const ChatComponentWidget({ +class ChatComponentWidget extends StatefulWidget { + const ChatComponentWidget._({ required super.key, required TypedKey localConversationRecordKey, required void Function() onCancel, @@ -29,10 +35,14 @@ class ChatComponentWidget extends StatelessWidget { _onCancel = onCancel, _onClose = onClose; - ///////////////////////////////////////////////////////////////////// - - @override - Widget build(BuildContext context) { + // Create a single-contact chat and its associated state + static Widget singleContact({ + required BuildContext context, + required TypedKey localConversationRecordKey, + required void Function() onCancel, + required void Function() onClose, + Key? key, + }) { // Get the account info final accountInfo = context.watch().state; @@ -45,19 +55,19 @@ class ChatComponentWidget extends StatelessWidget { // Get the active conversation cubit final activeConversationCubit = context .select( - (x) => x.tryOperateSync(_localConversationRecordKey, + (x) => x.tryOperateSync(localConversationRecordKey, closure: (cubit) => cubit)); if (activeConversationCubit == null) { - return waitingPage(onCancel: _onCancel); + return waitingPage(onCancel: onCancel); } // Get the messages cubit final messagesCubit = context.select( - (x) => x.tryOperateSync(_localConversationRecordKey, + (x) => x.tryOperateSync(localConversationRecordKey, closure: (cubit) => cubit)); if (messagesCubit == null) { - return waitingPage(onCancel: _onCancel); + return waitingPage(onCancel: onCancel); } // Make chat component state @@ -70,26 +80,65 @@ class ChatComponentWidget extends StatelessWidget { activeConversationCubit: activeConversationCubit, messagesCubit: messagesCubit, ), - child: Builder(builder: _buildChatComponent)); + child: ChatComponentWidget._( + key: ValueKey(localConversationRecordKey), + localConversationRecordKey: localConversationRecordKey, + onCancel: onCancel, + onClose: onClose)); } - ///////////////////////////////////////////////////////////////////// + @override + State createState() => _ChatComponentWidgetState(); - Widget _buildChatComponent(BuildContext context) { + //////////////////////////////////////////////////////////////////////////// + final TypedKey _localConversationRecordKey; + final void Function() _onCancel; + final void Function() _onClose; +} + +class _ChatComponentWidgetState extends State { + //////////////////////////////////////////////////////////////////// + + @override + void initState() { + _chatController = core.InMemoryChatController(); + _textEditingController = TextEditingController(); + _scrollController = ScrollController(); + _chatStateProcessor = SingleStateProcessor(); + + final _chatComponentCubit = context.read(); + _chatStateProcessor.follow(_chatComponentCubit.stream, + _chatComponentCubit.state, _updateChatState); + + super.initState(); + } + + @override + void dispose() { + unawaited(_chatStateProcessor.close()); + + _chatController.dispose(); + _scrollController.dispose(); + _textEditingController.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { final theme = Theme.of(context); - final scaleScheme = theme.extension()!; - final scaleConfig = theme.extension()!; - final scale = scaleScheme.scale(ScaleKind.primary); + final scaleTheme = theme.extension()!; + final scale = scaleTheme.scheme.scale(ScaleKind.primary); final textTheme = theme.textTheme; - final chatTheme = makeChatTheme(scaleScheme, scaleConfig, textTheme); - final errorChatTheme = (ChatThemeEditor(chatTheme) - ..inputTextColor = scaleScheme.errorScale.primary - ..sendButtonIcon = Image.asset( - 'assets/icon-send.png', - color: scaleScheme.errorScale.primary, - package: 'flutter_chat_ui', - )) - .commit(); + final scaleChatTheme = scaleTheme.chatTheme(); + // final errorChatTheme = chatTheme.copyWith(color:) + // ..inputTextColor = scaleScheme.errorScale.primary + // ..sendButtonIcon = Image.asset( + // 'assets/icon-send.png', + // color: scaleScheme.errorScale.primary, + // package: 'flutter_chat_ui', + // )) + // .commit(); // Get the enclosing chat component cubit that contains our state // (created by ChatComponentWidget.builder()) @@ -110,9 +159,8 @@ class ChatComponentWidget extends StatelessWidget { final title = chatComponentState.title; if (chatComponentCubit.scrollOffset != 0) { - chatComponentState.scrollController.position.correctPixels( - chatComponentState.scrollController.position.pixels + - chatComponentCubit.scrollOffset); + _scrollController.position.correctPixels( + _scrollController.position.pixels + chatComponentCubit.scrollOffset); chatComponentCubit.scrollOffset = 0; } @@ -138,7 +186,7 @@ class ChatComponentWidget extends StatelessWidget { IconButton( iconSize: 24, icon: Icon(Icons.close, color: scale.borderText), - onPressed: _onClose) + onPressed: widget._onClose) .paddingLTRB(0, 0, 8, 0) ]), ), @@ -164,7 +212,7 @@ class ChatComponentWidget extends StatelessWidget { chatComponentCubit.scrollOffset = scrollOffset; // - singleFuture(chatComponentState.chatKey, () async { + singleFuture((chatComponentCubit, _kScrollTag), () async { await _handlePageForward( chatComponentCubit, messageWindow, notification); }); @@ -182,7 +230,7 @@ class ChatComponentWidget extends StatelessWidget { chatComponentCubit.scrollOffset = scrollOffset; // - singleFuture(chatComponentState.chatKey, () async { + singleFuture((chatComponentCubit, _kScrollTag), () async { await _handlePageBackward( chatComponentCubit, messageWindow, notification); }); @@ -190,82 +238,181 @@ class ChatComponentWidget extends StatelessWidget { return false; }, child: ValueListenableBuilder( - valueListenable: chatComponentState.textEditingController, + valueListenable: _textEditingController, builder: (context, textEditingValue, __) { final messageIsValid = - utf8.encode(textEditingValue.text).lengthInBytes < - 2048; + _messageIsValid(textEditingValue.text); + var sendIconColor = scaleTheme.config.preferBorders + ? scale.border + : scale.borderText; + + if (!messageIsValid || + _textEditingController.text.isEmpty) { + sendIconColor = sendIconColor.withAlpha(128); + } return Chat( - key: chatComponentState.chatKey, - theme: messageIsValid ? chatTheme : errorChatTheme, - messages: messageWindow.window.toList(), - scrollToBottomOnSend: isFirstPage, - scrollController: chatComponentState.scrollController, - inputOptions: InputOptions( - inputClearMode: messageIsValid - ? InputClearMode.always - : InputClearMode.never, - textEditingController: - chatComponentState.textEditingController), - // isLastPage: isLastPage, - // onEndReached: () async { - // await _handlePageBackward( - // chatComponentCubit, messageWindow); - // }, - //onEndReachedThreshold: onEndReachedThreshold, - //onAttachmentPressed: _handleAttachmentPressed, - //onMessageTap: _handleMessageTap, - //onPreviewDataFetched: _handlePreviewDataFetched, - usePreviewData: false, // - onSendPressed: (pt) { - try { - if (!messageIsValid) { - context.read().error( - text: translate('chat.message_too_long')); - return; - } - _handleSendPressed(chatComponentCubit, pt); - } on FormatException { - context.read().error( - text: translate('chat.message_too_long')); - } - }, - listBottomWidget: messageIsValid - ? null - : Text(translate('chat.message_too_long'), - style: TextStyle( - color: - scaleScheme.errorScale.primary)) - .toCenter(), - //showUserAvatars: false, - //showUserNames: true, - user: localUser, - emptyState: const EmptyChatWidget()); + currentUserId: localUser.id, + resolveUser: (id) async { + if (id == localUser.id) { + return localUser; + } + return chatComponentState.remoteUsers.get(id); + }, + chatController: _chatController, + onMessageSend: (text) => + _handleSendPressed(chatComponentCubit, text), + theme: scaleChatTheme.chatTheme, + builders: core.Builders( + // Chat list builder + chatAnimatedListBuilder: (context, itemBuilder) => + ChatAnimatedListReversed( + scrollController: _scrollController, + itemBuilder: itemBuilder), + // Text message builder + textMessageBuilder: (context, message, index) => + VcTextMessageWidget( + message: message, + index: index, + // showTime: true, + // showStatus: true, + ), + // Composer builder + composerBuilder: (ctx) => VcComposerWidget( + autofocus: true, + textInputAction: isAnyMobile + ? TextInputAction.newline + : TextInputAction.send, + shiftEnterAction: isAnyMobile + ? ShiftEnterAction.send + : ShiftEnterAction.newline, + textEditingController: _textEditingController, + maxLength: maxMessageLength, + keyboardType: TextInputType.multiline, + sendIconColor: sendIconColor, + topWidget: messageIsValid + ? null + : Text(translate('chat.message_too_long'), + style: TextStyle( + color: scaleTheme + .scheme.errorScale.primary)) + .toCenter(), + ), + ), + timeFormat: core.DateFormat.jm(), + ); }))).expanded(), ], ); } - void _handleSendPressed( - ChatComponentCubit chatComponentCubit, types.PartialText message) { - final text = message.text; + ///////////////////////////////////////////////////////////////////// + bool _messageIsValid(String text) => + utf8.encode(text).lengthInBytes < maxMessageLength; + + Future _updateChatState(ChatComponentState chatComponentState) async { + // Update message window state + final data = chatComponentState.messageWindow.asData; + if (data == null) { + await _chatController.setMessages([]); + return; + } + + final windowState = data.value; + + await _chatController.setMessages(windowState.window.toList()); + + // final newMessagesSet = windowState.window.toSet(); + // final newMessagesById = + // Map.fromEntries(newMessagesSet.map((m) => MapEntry(m.id, m))); + // final newMessagesBySeqId = Map.fromEntries( + // newMessagesSet.map((m) => MapEntry(m.metadata![kSeqId], m))); + // final oldMessagesSet = _chatController.messages.toSet(); + + // if (oldMessagesSet.isEmpty) { + // await _chatController.setMessages(windowState.window.toList()); + // return; + // } + + // // See how many messages differ by equality (not identity) + // // If there are more than `replaceAllMessagesThreshold` differences + // // just replace the whole list of messages + // final diffs = newMessagesSet.diffAndIntersect(oldMessagesSet, + // diffThisMinusOther: true, diffOtherMinusThis: true); + // final addedMessages = diffs.diffThisMinusOther!; + // final removedMessages = diffs.diffOtherMinusThis!; + + // final replaceAllPaginationLimit = windowState.windowCount / 3; + + // if ((addedMessages.length >= replaceAllPaginationLimit) || + // removedMessages.length >= replaceAllPaginationLimit) { + // await _chatController.setMessages(windowState.window.toList()); + // return; + // } + + // // Remove messages that are gone, and replace the ones that have changed + // for (final m in removedMessages) { + // final newm = newMessagesById[m.id]; + // if (newm != null) { + // await _chatController.updateMessage(m, newm); + // } else { + // final newm = newMessagesBySeqId[m.metadata![kSeqId]]; + // if (newm != null) { + // await _chatController.updateMessage(m, newm); + // addedMessages.remove(newm); + // } else { + // await _chatController.removeMessage(m); + // } + // } + // } + + // // // Check for append + // if (addedMessages.isNotEmpty) { + // if (_chatController.messages.isNotEmpty && + // (addedMessages.first.metadata![kSeqId] as int) > + // (_chatController.messages.reversed.last.metadata![kSeqId] + // as int)) { + // await _chatController.insertAllMessages(addedMessages.reversedView, + // index: 0); + // } + // // Check for prepend + // else if (_chatController.messages.isNotEmpty && + // (addedMessages.last.metadata![kSeqId] as int) < + // (_chatController.messages.reversed.first.metadata![kSeqId] + // as int)) { + // await _chatController.insertAllMessages( + // addedMessages.reversedView, + // ); + // } + // // Otherwise just replace + // // xxx could use a better algorithm here to merge added messages in + // else { + // await _chatController.setMessages(windowState.window.toList()); + // } + // } + } + + void _handleSendPressed(ChatComponentCubit chatComponentCubit, String text) { if (text.startsWith('/')) { chatComponentCubit.runCommand(text); return; } - chatComponentCubit.sendMessage(message); + if (!_messageIsValid(text)) { + context + .read() + .error(text: translate('chat.message_too_long')); + return; + } + + chatComponentCubit.sendMessage(text: text); } // void _handleAttachmentPressed() async { - // // - // } - Future _handlePageForward( ChatComponentCubit chatComponentCubit, - WindowState messageWindow, + WindowState messageWindow, ScrollNotification notification) async { debugPrint( '_handlePageForward: messagesState.length=${messageWindow.length} ' @@ -299,7 +446,7 @@ class ChatComponentWidget extends StatelessWidget { Future _handlePageBackward( ChatComponentCubit chatComponentCubit, - WindowState messageWindow, + WindowState messageWindow, ScrollNotification notification, ) async { debugPrint( @@ -335,8 +482,8 @@ class ChatComponentWidget extends StatelessWidget { //chatComponentCubit.scrollOffset = 0; } - //////////////////////////////////////////////////////////////////////////// - final TypedKey _localConversationRecordKey; - final void Function() _onCancel; - final void Function() _onClose; + late final core.ChatController _chatController; + late final TextEditingController _textEditingController; + late final ScrollController _scrollController; + late final SingleStateProcessor _chatStateProcessor; } diff --git a/lib/chat/views/date_formatter.dart b/lib/chat/views/date_formatter.dart new file mode 100644 index 0000000..2835b70 --- /dev/null +++ b/lib/chat/views/date_formatter.dart @@ -0,0 +1,41 @@ +import 'package:flutter_translate/flutter_translate.dart'; +import 'package:intl/intl.dart'; + +class DateFormatter { + DateFormatter(); + + String chatDateTimeFormat(DateTime dateTime) { + final now = DateTime.now(); + + final justNow = now.subtract(const Duration(minutes: 1)); + + final localDateTime = dateTime.toLocal(); + + if (!localDateTime.difference(justNow).isNegative) { + return translate('date_formatter.just_now'); + } + + final roughTimeString = DateFormat.jm().format(dateTime); + + if (localDateTime.day == now.day && + localDateTime.month == now.month && + localDateTime.year == now.year) { + return roughTimeString; + } + + final yesterday = now.subtract(const Duration(days: 1)); + + if (localDateTime.day == yesterday.day && + localDateTime.month == now.month && + localDateTime.year == now.year) { + return translate('date_formatter.yesterday'); + } + + if (now.difference(localDateTime).inDays < 4) { + final weekday = DateFormat(DateFormat.WEEKDAY).format(localDateTime); + + return '$weekday, $roughTimeString'; + } + return '${DateFormat.yMd().format(dateTime)}, $roughTimeString'; + } +} diff --git a/lib/chat/views/utf8_length_limiting_text_input_formatter.dart b/lib/chat/views/utf8_length_limiting_text_input_formatter.dart new file mode 100644 index 0000000..f037ca8 --- /dev/null +++ b/lib/chat/views/utf8_length_limiting_text_input_formatter.dart @@ -0,0 +1,54 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter { + Utf8LengthLimitingTextInputFormatter({this.maxLength}) + : assert(maxLength != null || maxLength! >= 0, 'maxLength is invalid'); + + final int? maxLength; + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + if (maxLength != null && _bytesLength(newValue.text) > maxLength!) { + // If already at the maximum and tried to enter even more, + // keep the old value. + if (_bytesLength(oldValue.text) == maxLength) { + return oldValue; + } + return _truncate(newValue, maxLength!); + } + return newValue; + } + + static TextEditingValue _truncate(TextEditingValue value, int maxLength) { + var newValue = ''; + if (_bytesLength(value.text) > maxLength) { + var length = 0; + + value.text.characters.takeWhile((char) { + final nbBytes = _bytesLength(char); + if (length + nbBytes <= maxLength) { + newValue += char; + length += nbBytes; + return true; + } + return false; + }); + } + return TextEditingValue( + text: newValue, + selection: value.selection.copyWith( + baseOffset: min(value.selection.start, newValue.length), + extentOffset: min(value.selection.end, newValue.length), + ), + ); + } + + static int _bytesLength(String value) => utf8.encode(value).length; +} diff --git a/lib/chat/views/views.dart b/lib/chat/views/views.dart index 9703643..41b1936 100644 --- a/lib/chat/views/views.dart +++ b/lib/chat/views/views.dart @@ -1,3 +1,4 @@ export 'chat_component_widget.dart'; export 'empty_chat_widget.dart'; export 'no_conversation_widget.dart'; +export 'utf8_length_limiting_text_input_formatter.dart'; diff --git a/lib/init.dart b/lib/init.dart index 8c80bf9..958cd08 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -17,8 +17,12 @@ class VeilidChatGlobalInit { // Initialize Veilid Future _initializeVeilid() async { // Init Veilid - Veilid.instance.initializeVeilidCore( - await getDefaultVeilidPlatformConfig(kIsWeb, VeilidChatApp.name)); + try { + Veilid.instance.initializeVeilidCore( + await getDefaultVeilidPlatformConfig(kIsWeb, VeilidChatApp.name)); + } on VeilidAPIExceptionAlreadyInitialized { + log.debug('Already initialized, not reinitializing veilid-core'); + } // Veilid logging initVeilidLog(kIsDebugMode); diff --git a/lib/keyboard_shortcuts.dart b/lib/keyboard_shortcuts.dart index 0c531a9..1e25dac 100644 --- a/lib/keyboard_shortcuts.dart +++ b/lib/keyboard_shortcuts.dart @@ -16,6 +16,14 @@ class ReloadThemeIntent extends Intent { const ReloadThemeIntent(); } +class ChangeBrightnessIntent extends Intent { + const ChangeBrightnessIntent(); +} + +class ChangeColorIntent extends Intent { + const ChangeColorIntent(); +} + class AttachDetachIntent extends Intent { const AttachDetachIntent(); } @@ -49,6 +57,49 @@ class KeyboardShortcuts extends StatelessWidget { }); } + void changeBrightness(BuildContext context) { + singleFuture(this, () async { + final prefs = PreferencesRepository.instance.value; + + final oldBrightness = prefs.themePreference.brightnessPreference; + final newBrightness = BrightnessPreference.values[ + (oldBrightness.index + 1) % BrightnessPreference.values.length]; + + log.info('Changing brightness to $newBrightness'); + + final newPrefs = prefs.copyWith( + themePreference: prefs.themePreference + .copyWith(brightnessPreference: newBrightness)); + await PreferencesRepository.instance.set(newPrefs); + + if (context.mounted) { + ThemeSwitcher.of(context) + .changeTheme(theme: newPrefs.themePreference.themeData()); + } + }); + } + + void changeColor(BuildContext context) { + singleFuture(this, () async { + final prefs = PreferencesRepository.instance.value; + final oldColor = prefs.themePreference.colorPreference; + final newColor = ColorPreference + .values[(oldColor.index + 1) % ColorPreference.values.length]; + + log.info('Changing color to $newColor'); + + final newPrefs = prefs.copyWith( + themePreference: + prefs.themePreference.copyWith(colorPreference: newColor)); + await PreferencesRepository.instance.set(newPrefs); + + if (context.mounted) { + ThemeSwitcher.of(context) + .changeTheme(theme: newPrefs.themePreference.themeData()); + } + }); + } + void _attachDetach(BuildContext context) { singleFuture(this, () async { if (ProcessorRepository.instance.processorConnectionState.isAttached) { @@ -75,17 +126,34 @@ class KeyboardShortcuts extends StatelessWidget { Widget build(BuildContext context) => ThemeSwitcher( builder: (context) => Shortcuts( shortcuts: { - LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.keyR): - const ReloadThemeIntent(), - LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.keyD): - const AttachDetachIntent(), LogicalKeySet( - LogicalKeyboardKey.alt, LogicalKeyboardKey.backquote): - const DeveloperPageIntent(), + LogicalKeyboardKey.alt, + LogicalKeyboardKey.control, + LogicalKeyboardKey.keyR): const ReloadThemeIntent(), + LogicalKeySet( + LogicalKeyboardKey.alt, + LogicalKeyboardKey.control, + LogicalKeyboardKey.keyB): const ChangeBrightnessIntent(), + LogicalKeySet( + LogicalKeyboardKey.alt, + LogicalKeyboardKey.control, + LogicalKeyboardKey.keyC): const ChangeColorIntent(), + LogicalKeySet( + LogicalKeyboardKey.alt, + LogicalKeyboardKey.control, + LogicalKeyboardKey.keyD): const AttachDetachIntent(), + LogicalKeySet( + LogicalKeyboardKey.alt, + LogicalKeyboardKey.control, + LogicalKeyboardKey.backquote): const DeveloperPageIntent(), }, child: Actions(actions: >{ ReloadThemeIntent: CallbackAction( onInvoke: (intent) => reloadTheme(context)), + ChangeBrightnessIntent: CallbackAction( + onInvoke: (intent) => changeBrightness(context)), + ChangeColorIntent: CallbackAction( + onInvoke: (intent) => changeColor(context)), AttachDetachIntent: CallbackAction( onInvoke: (intent) => _attachDetach(context)), DeveloperPageIntent: CallbackAction( diff --git a/lib/layout/home/home_account_ready.dart b/lib/layout/home/home_account_ready.dart index 3674a08..a2966a6 100644 --- a/lib/layout/home/home_account_ready.dart +++ b/lib/layout/home/home_account_ready.dart @@ -130,15 +130,19 @@ class _HomeAccountReadyState extends State { if (activeChatLocalConversationKey == null) { return const NoConversationWidget(); } - return ChatComponentWidget( - localConversationRecordKey: activeChatLocalConversationKey, - onCancel: () { - activeChatCubit.setActiveChat(null); - }, - onClose: () { - activeChatCubit.setActiveChat(null); - }, - key: ValueKey(activeChatLocalConversationKey)); + return Material( + color: Colors.transparent, + child: Builder( + builder: (context) => ChatComponentWidget.singleContact( + context: context, + localConversationRecordKey: activeChatLocalConversationKey, + onCancel: () { + activeChatCubit.setActiveChat(null); + }, + onClose: () { + activeChatCubit.setActiveChat(null); + }, + key: ValueKey(activeChatLocalConversationKey)))); } @override diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index 3d1b14f..b4c3b58 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart'; +import 'package:keyboard_avoider/keyboard_avoider.dart'; import 'package:provider/provider.dart'; import 'package:transitioned_indexed_stack/transitioned_indexed_stack.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -207,33 +208,32 @@ class HomeScreenState extends State return DefaultTextStyle( style: theme.textTheme.bodySmall!, - child: ZoomDrawer( - controller: _zoomDrawerController, - menuScreen: Builder(builder: (context) { - final zoomDrawer = ZoomDrawer.of(context); - zoomDrawer!.stateNotifier.addListener(() { - if (zoomDrawer.isOpen()) { - FocusManager.instance.primaryFocus?.unfocus(); - } - }); - return const DrawerMenu(); - }), - mainScreen: Provider.value( - value: _zoomDrawerController, - child: Builder(builder: _buildAccountPageView)), - borderRadius: 0, - angle: 0, - //mainScreenOverlayColor: theme.shadowColor.withAlpha(0x2F), - openCurve: Curves.fastEaseInToSlowEaseOut, - closeCurve: Curves.fastEaseInToSlowEaseOut, - // duration: const Duration(milliseconds: 250), - // reverseDuration: const Duration(milliseconds: 250), - menuScreenTapClose: canClose, - mainScreenTapClose: canClose, - disableDragGesture: !canClose, - mainScreenScale: .25, - slideWidth: min(360, MediaQuery.of(context).size.width * 0.9), - )); + child: KeyboardAvoider( + curve: Curves.ease, + child: ZoomDrawer( + controller: _zoomDrawerController, + menuScreen: Builder(builder: (context) { + final zoomDrawer = ZoomDrawer.of(context); + zoomDrawer!.stateNotifier.addListener(() { + if (zoomDrawer.isOpen()) { + FocusManager.instance.primaryFocus?.unfocus(); + } + }); + return const DrawerMenu(); + }), + mainScreen: Provider.value( + value: _zoomDrawerController, + child: Builder(builder: _buildAccountPageView)), + borderRadius: 0, + angle: 0, + openCurve: Curves.fastEaseInToSlowEaseOut, + closeCurve: Curves.fastEaseInToSlowEaseOut, + menuScreenTapClose: canClose, + mainScreenTapClose: canClose, + disableDragGesture: !canClose, + mainScreenScale: .25, + slideWidth: min(360, MediaQuery.of(context).size.width * 0.9), + ))); } //////////////////////////////////////////////////////////////////////////// diff --git a/lib/theme/models/chat_theme.dart b/lib/theme/models/chat_theme.dart deleted file mode 100644 index b650e17..0000000 --- a/lib/theme/models/chat_theme.dart +++ /dev/null @@ -1,488 +0,0 @@ -// ignore_for_file: always_put_required_named_parameters_first - -import 'package:flutter/material.dart'; -import 'package:flutter_chat_ui/flutter_chat_ui.dart'; - -import 'scale_theme/scale_scheme.dart'; - -ChatTheme makeChatTheme( - ScaleScheme scale, ScaleConfig scaleConfig, TextTheme textTheme) => - DefaultChatTheme( - primaryColor: scaleConfig.preferBorders - ? scale.primaryScale.calloutText - : scale.primaryScale.calloutBackground, - secondaryColor: scaleConfig.preferBorders - ? scale.secondaryScale.calloutText - : scale.secondaryScale.calloutBackground, - backgroundColor: - scale.grayScale.appBackground.withAlpha(scaleConfig.wallpaperAlpha), - messageBorderRadius: scaleConfig.borderRadiusScale * 12, - bubbleBorderSide: scaleConfig.preferBorders - ? BorderSide( - color: scale.primaryScale.calloutBackground, - width: 2, - ) - : BorderSide(width: 2, color: Colors.black.withAlpha(96)), - sendButtonIcon: Image.asset( - 'assets/icon-send.png', - color: scaleConfig.preferBorders - ? scale.primaryScale.border - : scale.primaryScale.borderText, - package: 'flutter_chat_ui', - ), - inputBackgroundColor: Colors.blue, - inputBorderRadius: BorderRadius.zero, - inputTextStyle: textTheme.bodyLarge!, - inputTextDecoration: InputDecoration( - filled: !scaleConfig.preferBorders, - fillColor: scale.primaryScale.subtleBackground, - isDense: true, - contentPadding: const EdgeInsets.fromLTRB(8, 8, 8, 8), - disabledBorder: OutlineInputBorder( - borderSide: scaleConfig.preferBorders - ? BorderSide(color: scale.grayScale.border, width: 2) - : BorderSide.none, - borderRadius: BorderRadius.all( - Radius.circular(8 * scaleConfig.borderRadiusScale))), - enabledBorder: OutlineInputBorder( - borderSide: scaleConfig.preferBorders - ? BorderSide(color: scale.primaryScale.border, width: 2) - : BorderSide.none, - borderRadius: BorderRadius.all( - Radius.circular(8 * scaleConfig.borderRadiusScale))), - focusedBorder: OutlineInputBorder( - borderSide: scaleConfig.preferBorders - ? BorderSide(color: scale.primaryScale.border, width: 2) - : BorderSide.none, - borderRadius: BorderRadius.all( - Radius.circular(8 * scaleConfig.borderRadiusScale))), - ), - inputContainerDecoration: BoxDecoration( - border: scaleConfig.preferBorders - ? Border( - top: BorderSide(color: scale.primaryScale.border, width: 2)) - : null, - color: scaleConfig.preferBorders - ? scale.primaryScale.elementBackground - : scale.primaryScale.border), - inputPadding: const EdgeInsets.all(6), - inputTextColor: !scaleConfig.preferBorders - ? scale.primaryScale.appText - : scale.primaryScale.border, - messageInsetsHorizontal: 12, - messageInsetsVertical: 8, - attachmentButtonIcon: const Icon(Icons.attach_file), - sentMessageBodyTextStyle: textTheme.bodyLarge!.copyWith( - color: scaleConfig.preferBorders - ? scale.primaryScale.calloutBackground - : scale.primaryScale.calloutText, - ), - sentEmojiMessageTextStyle: const TextStyle( - color: Colors.white, - fontSize: 64, - ), - receivedMessageBodyTextStyle: textTheme.bodyLarge!.copyWith( - color: scaleConfig.preferBorders - ? scale.secondaryScale.calloutBackground - : scale.secondaryScale.calloutText, - ), - receivedEmojiMessageTextStyle: const TextStyle( - color: Colors.white, - fontSize: 64, - ), - dateDividerTextStyle: textTheme.labelSmall!); - -class EditedChatTheme extends ChatTheme { - const EditedChatTheme({ - required super.attachmentButtonIcon, - required super.attachmentButtonMargin, - required super.backgroundColor, - super.bubbleMargin, - required super.dateDividerMargin, - required super.dateDividerTextStyle, - required super.deliveredIcon, - required super.documentIcon, - required super.emptyChatPlaceholderTextStyle, - required super.errorColor, - required super.errorIcon, - required super.inputBackgroundColor, - required super.inputSurfaceTintColor, - required super.inputElevation, - required super.inputBorderRadius, - super.inputContainerDecoration, - required super.inputMargin, - required super.inputPadding, - required super.inputTextColor, - super.inputTextCursorColor, - required super.inputTextDecoration, - required super.inputTextStyle, - required super.messageBorderRadius, - required super.messageInsetsHorizontal, - required super.messageInsetsVertical, - required super.messageMaxWidth, - required super.primaryColor, - required super.receivedEmojiMessageTextStyle, - super.receivedMessageBodyBoldTextStyle, - super.receivedMessageBodyCodeTextStyle, - super.receivedMessageBodyLinkTextStyle, - required super.receivedMessageBodyTextStyle, - required super.receivedMessageCaptionTextStyle, - required super.receivedMessageDocumentIconColor, - required super.receivedMessageLinkDescriptionTextStyle, - required super.receivedMessageLinkTitleTextStyle, - required super.secondaryColor, - required super.seenIcon, - required super.sendButtonIcon, - required super.sendButtonMargin, - required super.sendingIcon, - required super.sentEmojiMessageTextStyle, - super.sentMessageBodyBoldTextStyle, - super.sentMessageBodyCodeTextStyle, - super.sentMessageBodyLinkTextStyle, - required super.sentMessageBodyTextStyle, - required super.sentMessageCaptionTextStyle, - required super.sentMessageDocumentIconColor, - required super.sentMessageLinkDescriptionTextStyle, - required super.sentMessageLinkTitleTextStyle, - required super.statusIconPadding, - required super.systemMessageTheme, - required super.typingIndicatorTheme, - required super.unreadHeaderTheme, - required super.userAvatarImageBackgroundColor, - required super.userAvatarNameColors, - required super.userAvatarTextStyle, - required super.userNameTextStyle, - super.highlightMessageColor, - }); -} - -class ChatThemeEditor { - ChatThemeEditor(ChatTheme base) - : attachmentButtonIcon = base.attachmentButtonIcon, - attachmentButtonMargin = base.attachmentButtonMargin, - backgroundColor = base.backgroundColor, - bubbleMargin = base.bubbleMargin, - dateDividerMargin = base.dateDividerMargin, - dateDividerTextStyle = base.dateDividerTextStyle, - deliveredIcon = base.deliveredIcon, - documentIcon = base.documentIcon, - emptyChatPlaceholderTextStyle = base.emptyChatPlaceholderTextStyle, - errorColor = base.errorColor, - errorIcon = base.errorIcon, - inputBackgroundColor = base.inputBackgroundColor, - inputSurfaceTintColor = base.inputSurfaceTintColor, - inputElevation = base.inputElevation, - inputBorderRadius = base.inputBorderRadius, - inputContainerDecoration = base.inputContainerDecoration, - inputMargin = base.inputMargin, - inputPadding = base.inputPadding, - inputTextColor = base.inputTextColor, - inputTextCursorColor = base.inputTextCursorColor, - inputTextDecoration = base.inputTextDecoration, - inputTextStyle = base.inputTextStyle, - messageBorderRadius = base.messageBorderRadius, - messageInsetsHorizontal = base.messageInsetsHorizontal, - messageInsetsVertical = base.messageInsetsVertical, - messageMaxWidth = base.messageMaxWidth, - primaryColor = base.primaryColor, - receivedEmojiMessageTextStyle = base.receivedEmojiMessageTextStyle, - receivedMessageBodyBoldTextStyle = - base.receivedMessageBodyBoldTextStyle, - receivedMessageBodyCodeTextStyle = - base.receivedMessageBodyCodeTextStyle, - receivedMessageBodyLinkTextStyle = - base.receivedMessageBodyLinkTextStyle, - receivedMessageBodyTextStyle = base.receivedMessageBodyTextStyle, - receivedMessageCaptionTextStyle = base.receivedMessageCaptionTextStyle, - receivedMessageDocumentIconColor = - base.receivedMessageDocumentIconColor, - receivedMessageLinkDescriptionTextStyle = - base.receivedMessageLinkDescriptionTextStyle, - receivedMessageLinkTitleTextStyle = - base.receivedMessageLinkTitleTextStyle, - secondaryColor = base.secondaryColor, - seenIcon = base.seenIcon, - sendButtonIcon = base.sendButtonIcon, - sendButtonMargin = base.sendButtonMargin, - sendingIcon = base.sendingIcon, - sentEmojiMessageTextStyle = base.sentEmojiMessageTextStyle, - sentMessageBodyBoldTextStyle = base.sentMessageBodyBoldTextStyle, - sentMessageBodyCodeTextStyle = base.sentMessageBodyCodeTextStyle, - sentMessageBodyLinkTextStyle = base.sentMessageBodyLinkTextStyle, - sentMessageBodyTextStyle = base.sentMessageBodyTextStyle, - sentMessageCaptionTextStyle = base.sentMessageCaptionTextStyle, - sentMessageDocumentIconColor = base.sentMessageDocumentIconColor, - sentMessageLinkDescriptionTextStyle = - base.sentMessageLinkDescriptionTextStyle, - sentMessageLinkTitleTextStyle = base.sentMessageLinkTitleTextStyle, - statusIconPadding = base.statusIconPadding, - systemMessageTheme = base.systemMessageTheme, - typingIndicatorTheme = base.typingIndicatorTheme, - unreadHeaderTheme = base.unreadHeaderTheme, - userAvatarImageBackgroundColor = base.userAvatarImageBackgroundColor, - userAvatarNameColors = base.userAvatarNameColors, - userAvatarTextStyle = base.userAvatarTextStyle, - userNameTextStyle = base.userNameTextStyle, - highlightMessageColor = base.highlightMessageColor; - - EditedChatTheme commit() => EditedChatTheme( - attachmentButtonIcon: attachmentButtonIcon, - attachmentButtonMargin: attachmentButtonMargin, - backgroundColor: backgroundColor, - bubbleMargin: bubbleMargin, - dateDividerMargin: dateDividerMargin, - dateDividerTextStyle: dateDividerTextStyle, - deliveredIcon: deliveredIcon, - documentIcon: documentIcon, - emptyChatPlaceholderTextStyle: emptyChatPlaceholderTextStyle, - errorColor: errorColor, - errorIcon: errorIcon, - inputBackgroundColor: inputBackgroundColor, - inputSurfaceTintColor: inputSurfaceTintColor, - inputElevation: inputElevation, - inputBorderRadius: inputBorderRadius, - inputContainerDecoration: inputContainerDecoration, - inputMargin: inputMargin, - inputPadding: inputPadding, - inputTextColor: inputTextColor, - inputTextCursorColor: inputTextCursorColor, - inputTextDecoration: inputTextDecoration, - inputTextStyle: inputTextStyle, - messageBorderRadius: messageBorderRadius, - messageInsetsHorizontal: messageInsetsHorizontal, - messageInsetsVertical: messageInsetsVertical, - messageMaxWidth: messageMaxWidth, - primaryColor: primaryColor, - receivedEmojiMessageTextStyle: receivedEmojiMessageTextStyle, - receivedMessageBodyBoldTextStyle: receivedMessageBodyBoldTextStyle, - receivedMessageBodyCodeTextStyle: receivedMessageBodyCodeTextStyle, - receivedMessageBodyLinkTextStyle: receivedMessageBodyLinkTextStyle, - receivedMessageBodyTextStyle: receivedMessageBodyTextStyle, - receivedMessageCaptionTextStyle: receivedMessageCaptionTextStyle, - receivedMessageDocumentIconColor: receivedMessageDocumentIconColor, - receivedMessageLinkDescriptionTextStyle: - receivedMessageLinkDescriptionTextStyle, - receivedMessageLinkTitleTextStyle: receivedMessageLinkTitleTextStyle, - secondaryColor: secondaryColor, - seenIcon: seenIcon, - sendButtonIcon: sendButtonIcon, - sendButtonMargin: sendButtonMargin, - sendingIcon: sendingIcon, - sentEmojiMessageTextStyle: sentEmojiMessageTextStyle, - sentMessageBodyBoldTextStyle: sentMessageBodyBoldTextStyle, - sentMessageBodyCodeTextStyle: sentMessageBodyCodeTextStyle, - sentMessageBodyLinkTextStyle: sentMessageBodyLinkTextStyle, - sentMessageBodyTextStyle: sentMessageBodyTextStyle, - sentMessageCaptionTextStyle: sentMessageCaptionTextStyle, - sentMessageDocumentIconColor: sentMessageDocumentIconColor, - sentMessageLinkDescriptionTextStyle: - sentMessageLinkDescriptionTextStyle, - sentMessageLinkTitleTextStyle: sentMessageLinkTitleTextStyle, - statusIconPadding: statusIconPadding, - systemMessageTheme: systemMessageTheme, - typingIndicatorTheme: typingIndicatorTheme, - unreadHeaderTheme: unreadHeaderTheme, - userAvatarImageBackgroundColor: userAvatarImageBackgroundColor, - userAvatarNameColors: userAvatarNameColors, - userAvatarTextStyle: userAvatarTextStyle, - userNameTextStyle: userNameTextStyle, - highlightMessageColor: highlightMessageColor, - ); - - ///////////////////////////////////////////////////////////////////////////// - - /// Icon for select attachment button. - Widget? attachmentButtonIcon; - - /// Margin of attachment button. - EdgeInsets? attachmentButtonMargin; - - /// Used as a background color of a chat widget. - Color backgroundColor; - - // Margin around the message bubble. - EdgeInsetsGeometry? bubbleMargin; - - /// Margin around date dividers. - EdgeInsets dateDividerMargin; - - /// Text style of the date dividers. - TextStyle dateDividerTextStyle; - - /// Icon for message's `delivered` status. For the best look use size of 16. - Widget? deliveredIcon; - - /// Icon inside file message. - Widget? documentIcon; - - /// Text style of the empty chat placeholder. - TextStyle emptyChatPlaceholderTextStyle; - - /// Color to indicate something bad happened (usually - shades of red). - Color errorColor; - - /// Icon for message's `error` status. For the best look use size of 16. - Widget? errorIcon; - - /// Color of the bottom bar where text field is. - Color inputBackgroundColor; - - /// Surface Tint Color of the bottom bar where text field is. - Color inputSurfaceTintColor; - - double inputElevation; - - /// Top border radius of the bottom bar where text field is. - BorderRadius inputBorderRadius; - - /// Decoration of the container wrapping the text field. - Decoration? inputContainerDecoration; - - /// Outer insets of the bottom bar where text field is. - EdgeInsets inputMargin; - - /// Inner insets of the bottom bar where text field is. - EdgeInsets inputPadding; - - /// Color of the text field's text and attachment/send buttons. - Color inputTextColor; - - /// Color of the text field's cursor. - Color? inputTextCursorColor; - - /// Decoration of the input text field. - InputDecoration inputTextDecoration; - - /// Text style of the message input. To change the color use [inputTextColor]. - TextStyle inputTextStyle; - - /// Border radius of message container. - double messageBorderRadius; - - /// Horizontal message bubble insets. - double messageInsetsHorizontal; - - /// Vertical message bubble insets. - double messageInsetsVertical; - - /// Message bubble max width. set to [double.infinity] adaptive screen. - double messageMaxWidth; - - /// Primary color of the chat used as a background of sent messages - /// and statuses. - Color primaryColor; - - /// Text style used for displaying emojis on text messages. - TextStyle receivedEmojiMessageTextStyle; - - /// Body text style used for displaying bold text on received text messages. - /// Default to a bold version of [receivedMessageBodyTextStyle]. - TextStyle? receivedMessageBodyBoldTextStyle; - - /// Body text style used for displaying code text on received text messages. - /// Defaults to a mono version of [receivedMessageBodyTextStyle]. - TextStyle? receivedMessageBodyCodeTextStyle; - - /// Text style used for displaying link text on received text messages. - /// Defaults to [receivedMessageBodyTextStyle]. - TextStyle? receivedMessageBodyLinkTextStyle; - - /// Body text style used for displaying text on different types - /// of received messages. - TextStyle receivedMessageBodyTextStyle; - - /// Caption text style used for displaying secondary info (e.g. file size) on - /// different types of received messages. - TextStyle receivedMessageCaptionTextStyle; - - /// Color of the document icon on received messages. Has no effect when - /// [documentIcon] is used. - Color receivedMessageDocumentIconColor; - - /// Text style used for displaying link description on received messages. - TextStyle receivedMessageLinkDescriptionTextStyle; - - /// Text style used for displaying link title on received messages. - TextStyle receivedMessageLinkTitleTextStyle; - - /// Secondary color, used as a background of received messages. - Color secondaryColor; - - /// Icon for message's `seen` status. For the best look use size of 16. - Widget? seenIcon; - - /// Icon for send button. - Widget? sendButtonIcon; - - /// Margin of send button. - EdgeInsets? sendButtonMargin; - - /// Icon for message's `sending` status. For the best look use size of 10. - Widget? sendingIcon; - - /// Text style used for displaying emojis on text messages. - TextStyle sentEmojiMessageTextStyle; - - /// Body text style used for displaying bold text on sent text messages. - /// Defaults to a bold version of [sentMessageBodyTextStyle]. - TextStyle? sentMessageBodyBoldTextStyle; - - /// Body text style used for displaying code text on sent text messages. - /// Defaults to a mono version of [sentMessageBodyTextStyle]. - TextStyle? sentMessageBodyCodeTextStyle; - - /// Text style used for displaying link text on sent text messages. - /// Defaults to [sentMessageBodyTextStyle]. - TextStyle? sentMessageBodyLinkTextStyle; - - /// Body text style used for displaying text on different types - /// of sent messages. - TextStyle sentMessageBodyTextStyle; - - /// Caption text style used for displaying secondary info (e.g. file size) on - /// different types of sent messages. - TextStyle sentMessageCaptionTextStyle; - - /// Color of the document icon on sent messages. Has no effect when - /// [documentIcon] is used. - Color sentMessageDocumentIconColor; - - /// Text style used for displaying link description on sent messages. - TextStyle sentMessageLinkDescriptionTextStyle; - - /// Text style used for displaying link title on sent messages. - TextStyle sentMessageLinkTitleTextStyle; - - /// Padding around status icons. - EdgeInsets statusIconPadding; - - /// Theme for the system message. Will not have an effect if a custom builder - /// is provided. - SystemMessageTheme systemMessageTheme; - - /// Theme for typing indicator. See [TypingIndicator]. - TypingIndicatorTheme typingIndicatorTheme; - - /// Theme for the unread header. - UnreadHeaderTheme unreadHeaderTheme; - - /// Color used as a background for user avatar if an image is provided. - /// Visible if the image has some transparent parts. - Color userAvatarImageBackgroundColor; - - /// Colors used as backgrounds for user avatars with no image and so, - /// corresponding user names. - /// Calculated based on a user ID, so unique across the whole app. - List userAvatarNameColors; - - /// Text style used for displaying initials on user avatar if no - /// image is provided. - TextStyle userAvatarTextStyle; - - /// User names text style. Color will be overwritten - /// with [userAvatarNameColors]. - TextStyle userNameTextStyle; - - /// Color used as background of message row on highligth. - Color? highlightMessageColor; -} diff --git a/lib/theme/models/models.dart b/lib/theme/models/models.dart index be80542..7806ede 100644 --- a/lib/theme/models/models.dart +++ b/lib/theme/models/models.dart @@ -1,4 +1,3 @@ -export 'chat_theme.dart'; export 'radix_generator.dart'; export 'scale_theme/scale_theme.dart'; export 'theme_preference.dart'; diff --git a/lib/theme/models/scale_theme/scale_chat_theme.dart b/lib/theme/models/scale_theme/scale_chat_theme.dart new file mode 100644 index 0000000..5da0fe2 --- /dev/null +++ b/lib/theme/models/scale_theme/scale_chat_theme.dart @@ -0,0 +1,369 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_chat_core/flutter_chat_core.dart' as core; + +import 'scale_theme.dart'; + +class ScaleChatTheme { + ScaleChatTheme({ + // Default chat theme + required this.chatTheme, + + // Customization fields (from v1 of flutter chat ui) + required this.attachmentButtonIcon, + // required this.attachmentButtonMargin, + required this.backgroundColor, + required this.bubbleBorderSide, + // required this.dateDividerMargin, + // required this.chatContentMargin, + required this.dateDividerTextStyle, + // required this.deliveredIcon, + // required this.documentIcon, + // required this.emptyChatPlaceholderTextStyle, + // required this.errorColor, + // required this.errorIcon, + required this.inputBackgroundColor, + // required this.inputSurfaceTintColor, + // required this.inputElevation, + required this.inputBorderRadius, + // required this.inputMargin, + required this.inputPadding, + required this.inputTextColor, + required this.inputTextStyle, + required this.messageBorderRadius, + required this.messageInsetsHorizontal, + required this.messageInsetsVertical, + // required this.messageMaxWidth, + required this.primaryColor, + required this.receivedEmojiMessageTextStyle, + required this.receivedMessageBodyTextStyle, + // required this.receivedMessageCaptionTextStyle, + // required this.receivedMessageDocumentIconColor, + // required this.receivedMessageLinkDescriptionTextStyle, + // required this.receivedMessageLinkTitleTextStyle, + required this.secondaryColor, + // required this.seenIcon, + required this.sendButtonIcon, + // required this.sendButtonMargin, + // required this.sendingIcon, + required this.onlyEmojiFontSize, + required this.timeStyle, + required this.sentMessageBodyTextStyle, + // required this.sentMessageCaptionTextStyle, + // required this.sentMessageDocumentIconColor, + // required this.sentMessageLinkDescriptionTextStyle, + // required this.sentMessageLinkTitleTextStyle, + // required this.statusIconPadding, + // required this.userAvatarImageBackgroundColor, + // required this.userAvatarNameColors, + // required this.userAvatarTextStyle, + // required this.userNameTextStyle, + // required this.bubbleMargin, + required this.inputContainerDecoration, + // required this.inputTextCursorColor, + // required this.receivedMessageBodyBoldTextStyle, + // required this.receivedMessageBodyCodeTextStyle, + // required this.receivedMessageBodyLinkTextStyle, + // required this.sentMessageBodyBoldTextStyle, + // required this.sentMessageBodyCodeTextStyle, + // required this.sentMessageBodyLinkTextStyle, + // required this.highlightMessageColor, + }); + + final core.ChatTheme chatTheme; + + /// Icon for select attachment button. + final Widget? attachmentButtonIcon; + + /// Margin of attachment button. + // final EdgeInsets? attachmentButtonMargin; + + /// Used as a background color of a chat widget. + final Color backgroundColor; + + // Margin around the message bubble. + // final EdgeInsetsGeometry? bubbleMargin; + + /// Border for chat bubbles + final BorderSide bubbleBorderSide; + + /// Margin around date dividers. + // final EdgeInsets dateDividerMargin; + + /// Margin inside chat area. + // final EdgeInsets chatContentMargin; + + /// Text style of the date dividers. + final TextStyle dateDividerTextStyle; + + /// Icon for message's `delivered` status. For the best look use size of 16. + // final Widget? deliveredIcon; + + /// Icon inside file message. + // final Widget? documentIcon; + + /// Text style of the empty chat placeholder. + // final TextStyle emptyChatPlaceholderTextStyle; + + /// Color to indicate something bad happened (usually - shades of red). + // final Color errorColor; + + /// Icon for message's `error` status. For the best look use size of 16. + // final Widget? errorIcon; + + /// Color of the bottom bar where text field is. + final Color inputBackgroundColor; + + /// Surface Tint Color of the bottom bar where text field is. + // final Color inputSurfaceTintColor; + + /// Elevation to use for input material + // final double inputElevation; + + /// Top border radius of the bottom bar where text field is. + final BorderRadius inputBorderRadius; + + /// Decoration of the container wrapping the text field. + final Decoration? inputContainerDecoration; + + /// Outer insets of the bottom bar where text field is. + // final EdgeInsets inputMargin; + + /// Inner insets of the bottom bar where text field is. + final EdgeInsets inputPadding; + + /// Color of the text field's text and attachment/send buttons. + final Color inputTextColor; + + /// Color of the text field's cursor. + // final Color? inputTextCursorColor; + + /// Text style of the message input. To change the color use [inputTextColor]. + final TextStyle inputTextStyle; + + /// Border radius of message container. + final double messageBorderRadius; + + /// Horizontal message bubble insets. + final double messageInsetsHorizontal; + + /// Vertical message bubble insets. + final double messageInsetsVertical; + + /// Message bubble max width. set to [double.infinity] adaptive screen. + // final double messageMaxWidth; + + /// Primary color of the chat used as a background of sent messages + /// and statuses. + final Color primaryColor; + + /// Text style used for displaying emojis on text messages. + final TextStyle receivedEmojiMessageTextStyle; + + /// Body text style used for displaying bold text on received text messages. + // Default to a bold version of [receivedMessageBodyTextStyle]. + // final TextStyle? receivedMessageBodyBoldTextStyle; + + /// Body text style used for displaying code text on received text messages. + // Defaults to a mono version of [receivedMessageBodyTextStyle]. + // final TextStyle? receivedMessageBodyCodeTextStyle; + + /// Text style used for displaying link text on received text messages. + // Defaults to [receivedMessageBodyTextStyle]. + // final TextStyle? receivedMessageBodyLinkTextStyle; + + /// Body text style used for displaying text on different types + /// of received messages. + final TextStyle receivedMessageBodyTextStyle; + + /// Caption text style used for displaying secondary info (e.g. file size) on + /// different types of received messages. + // final TextStyle receivedMessageCaptionTextStyle; + + /// Color of the document icon on received messages. Has no effect when + // [documentIcon] is used. + // final Color receivedMessageDocumentIconColor; + + /// Text style used for displaying link description on received messages. + // final TextStyle receivedMessageLinkDescriptionTextStyle; + + /// Text style used for displaying link title on received messages. + // final TextStyle receivedMessageLinkTitleTextStyle; + + /// Secondary color, used as a background of received messages. + final Color secondaryColor; + + /// Icon for message's `seen` status. For the best look use size of 16. + // final Widget? seenIcon; + + /// Icon for send button. + final Widget? sendButtonIcon; + + /// Margin of send button. + // final EdgeInsets? sendButtonMargin; + + /// Icon for message's `sending` status. For the best look use size of 10. + // final Widget? sendingIcon; + + /// Text size for displaying emojis on text messages. + final double onlyEmojiFontSize; + + /// Text style used for time and status + final TextStyle timeStyle; + + /// Body text style used for displaying bold text on sent text messages. + /// Defaults to a bold version of [sentMessageBodyTextStyle]. + // final TextStyle? sentMessageBodyBoldTextStyle; + + /// Body text style used for displaying code text on sent text messages. + /// Defaults to a mono version of [sentMessageBodyTextStyle]. + // final TextStyle? sentMessageBodyCodeTextStyle; + + /// Text style used for displaying link text on sent text messages. + /// Defaults to [sentMessageBodyTextStyle]. + // final TextStyle? sentMessageBodyLinkTextStyle; + + /// Body text style used for displaying text on different types + /// of sent messages. + final TextStyle sentMessageBodyTextStyle; + + /// Caption text style used for displaying secondary info (e.g. file size) on + /// different types of sent messages. + // final TextStyle sentMessageCaptionTextStyle; + + /// Color of the document icon on sent messages. Has no effect when + // [documentIcon] is used. + // final Color sentMessageDocumentIconColor; + + /// Text style used for displaying link description on sent messages. + // final TextStyle sentMessageLinkDescriptionTextStyle; + + /// Text style used for displaying link title on sent messages. + // final TextStyle sentMessageLinkTitleTextStyle; + + /// Padding around status icons. + // final EdgeInsets statusIconPadding; + + /// Color used as a background for user avatar if an image is provided. + /// Visible if the image has some transparent parts. + // final Color userAvatarImageBackgroundColor; + + /// Colors used as backgrounds for user avatars with no image and so, + /// corresponding user names. + /// Calculated based on a user ID, so unique across the whole app. + // final List userAvatarNameColors; + + /// Text style used for displaying initials on user avatar if no + /// image is provided. + // final TextStyle userAvatarTextStyle; + + /// User names text style. Color will be overwritten with + // [userAvatarNameColors]. + // final TextStyle userNameTextStyle; + + /// Color used as background of message row on highlight. + // final Color? highlightMessageColor; +} + +extension ScaleChatThemeExt on ScaleTheme { + ScaleChatTheme chatTheme() { + // 'brightness' is not actually used by ChatColors.fromThemeData, + // or ChatTypography.fromThemeData so just say 'light' here + final themeData = toThemeData(Brightness.light); + final typography = core.ChatTypography.fromThemeData(themeData); + + final surfaceContainer = config.preferBorders + ? scheme.secondaryScale.calloutText + : scheme.secondaryScale.calloutBackground; + + final colors = core.ChatColors( + // Primary color, often used for sent messages and accents. + primary: config.preferBorders + ? scheme.primaryScale.calloutText + : scheme.primaryScale.calloutBackground, + // Color for text and icons displayed on top of [primary]. + onPrimary: scheme.primaryScale.primaryText, + // The main background color of the chat screen. + surface: + scheme.grayScale.appBackground.withAlpha(config.wallpaperAlpha), + + // Color for text and icons displayed on top of [surface]. + onSurface: scheme.primaryScale.appText, + + // Background color for elements like received messages. + surfaceContainer: surfaceContainer, + + // A slightly lighter/darker variant of [surfaceContainer]. + surfaceContainerLow: surfaceContainer.darken(25), + + // A slightly lighter/darker variant of [surfaceContainer]. + surfaceContainerHigh: surfaceContainer.lighten(25)); + + final chatTheme = core.ChatTheme( + colors: colors, + typography: typography, + shape: + BorderRadius.all(Radius.circular(config.borderRadiusScale * 12))); + + return ScaleChatTheme( + chatTheme: chatTheme, + primaryColor: config.preferBorders + ? scheme.primaryScale.calloutText + : scheme.primaryScale.calloutBackground, + secondaryColor: config.preferBorders + ? scheme.secondaryScale.calloutText + : scheme.secondaryScale.calloutBackground, + backgroundColor: + scheme.grayScale.appBackground.withAlpha(config.wallpaperAlpha), + messageBorderRadius: config.borderRadiusScale * 12, + bubbleBorderSide: config.preferBorders + ? BorderSide( + color: scheme.primaryScale.calloutBackground, + width: 2, + ) + : BorderSide(width: 2, color: Colors.black.withAlpha(96)), + sendButtonIcon: Image.asset( + 'assets/icon-send.png', + color: config.preferBorders + ? scheme.primaryScale.border + : scheme.primaryScale.borderText, + package: 'flutter_chat_ui', + ), + inputBackgroundColor: Colors.blue, + inputBorderRadius: BorderRadius.zero, + inputTextStyle: textTheme.bodyLarge!, + inputContainerDecoration: BoxDecoration( + border: config.preferBorders + ? Border( + top: + BorderSide(color: scheme.primaryScale.border, width: 2)) + : null, + color: config.preferBorders + ? scheme.primaryScale.elementBackground + : scheme.primaryScale.border), + inputPadding: const EdgeInsets.all(6), + inputTextColor: !config.preferBorders + ? scheme.primaryScale.appText + : scheme.primaryScale.border, + messageInsetsHorizontal: 12, + messageInsetsVertical: 8, + attachmentButtonIcon: const Icon(Icons.attach_file), + sentMessageBodyTextStyle: textTheme.bodyLarge!.copyWith( + color: config.preferBorders + ? scheme.primaryScale.calloutBackground + : scheme.primaryScale.calloutText, + ), + onlyEmojiFontSize: 64, + timeStyle: textTheme.bodySmall!.copyWith(fontSize: 9), + receivedMessageBodyTextStyle: textTheme.bodyLarge!.copyWith( + color: config.preferBorders + ? scheme.secondaryScale.calloutBackground + : scheme.secondaryScale.calloutText, + ), + receivedEmojiMessageTextStyle: const TextStyle( + color: Colors.white, + fontSize: 64, + ), + dateDividerTextStyle: textTheme.labelSmall!); + } +} diff --git a/lib/theme/models/scale_theme/scale_color.dart b/lib/theme/models/scale_theme/scale_color.dart index d79d878..f3c884f 100644 --- a/lib/theme/models/scale_theme/scale_color.dart +++ b/lib/theme/models/scale_theme/scale_color.dart @@ -83,6 +83,7 @@ class ScaleColor { calloutBackground: calloutBackground ?? this.calloutBackground, calloutText: calloutText ?? this.calloutText); + // Use static method // ignore: prefer_constructors_over_static_methods static ScaleColor lerp(ScaleColor a, ScaleColor b, double t) => ScaleColor( appBackground: Color.lerp(a.appBackground, b.appBackground, t) ?? diff --git a/lib/theme/models/scale_theme/scale_theme.dart b/lib/theme/models/scale_theme/scale_theme.dart index ef430d2..c3217ea 100644 --- a/lib/theme/models/scale_theme/scale_theme.dart +++ b/lib/theme/models/scale_theme/scale_theme.dart @@ -4,6 +4,7 @@ import 'scale_input_decorator_theme.dart'; import 'scale_scheme.dart'; export 'scale_app_bar_theme.dart'; +export 'scale_chat_theme.dart'; export 'scale_color.dart'; export 'scale_input_decorator_theme.dart'; export 'scale_scheme.dart'; diff --git a/lib/theme/views/responsive.dart b/lib/theme/views/responsive.dart index 91af81d..0182c9f 100644 --- a/lib/theme/views/responsive.dart +++ b/lib/theme/views/responsive.dart @@ -1,13 +1,24 @@ -import 'dart:io'; - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -bool get isAndroid => !kIsWeb && Platform.isAndroid; -bool get isiOS => !kIsWeb && Platform.isIOS; -bool get isWeb => kIsWeb; -bool get isDesktop => - !isWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS); +final isAndroid = !kIsWeb && defaultTargetPlatform == TargetPlatform.android; +final isiOS = !kIsWeb && defaultTargetPlatform == TargetPlatform.iOS; +final isMobile = !kIsWeb && + (defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.android); +final isDesktop = !kIsWeb && + !(defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.android); + +const isWeb = kIsWeb; +final isWebMobile = kIsWeb && + (defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.android); +final isWebDesktop = kIsWeb && + !(defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.android); + +final isAnyMobile = isMobile || isWebMobile; const kMobileWidthCutoff = 500.0; diff --git a/lib/theme/views/styled_alert.dart b/lib/theme/views/styled_alert.dart index 8602c22..1215c84 100644 --- a/lib/theme/views/styled_alert.dart +++ b/lib/theme/views/styled_alert.dart @@ -128,7 +128,7 @@ Future showErrorStacktraceModal( await showErrorModal( context: context, title: translate('toast.error'), - text: 'Error: {e}\n StackTrace: {st}', + text: 'Error: $error\n StackTrace: $stackTrace', ); } diff --git a/lib/tools/loggy.dart b/lib/tools/loggy.dart index 47a1ffd..adc62d1 100644 --- a/lib/tools/loggy.dart +++ b/lib/tools/loggy.dart @@ -165,5 +165,8 @@ void initLoggy() { registerVeilidProtoToDebug(); registerVeilidDHTProtoToDebug(); registerVeilidchatProtoToDebug(); - Bloc.observer = const StateLogger(); + + if (kIsDebugMode) { + Bloc.observer = const StateLogger(); + } } diff --git a/lib/tools/state_logger.dart b/lib/tools/state_logger.dart index 50dec46..a133346 100644 --- a/lib/tools/state_logger.dart +++ b/lib/tools/state_logger.dart @@ -6,14 +6,17 @@ import 'package:veilid_support/veilid_support.dart'; import 'loggy.dart'; const Map _blocChangeLogLevels = { - 'ConnectionStateCubit': LogLevel.off, - 'ActiveSingleContactChatBlocMapCubit': LogLevel.off, - 'ActiveConversationsBlocMapCubit': LogLevel.off, - 'PersistentQueueCubit': LogLevel.off, - 'TableDBArrayProtobufCubit': LogLevel.off, - 'DHTLogCubit': LogLevel.off, - 'SingleContactMessagesCubit': LogLevel.off, - 'ChatComponentCubit': LogLevel.off, + 'RouterCubit': LogLevel.debug, + 'PerAccountCollectionBlocMapCubit': LogLevel.debug, + 'PerAccountCollectionCubit': LogLevel.debug, + 'ActiveChatCubit': LogLevel.debug, + 'AccountRecordCubit': LogLevel.debug, + 'ContactListCubit': LogLevel.debug, + 'ContactInvitationListCubit': LogLevel.debug, + 'ChatListCubit': LogLevel.debug, + 'PreferencesCubit': LogLevel.debug, + 'ConversationCubit': LogLevel.debug, + 'DefaultDHTRecordCubit': LogLevel.debug, }; const Map _blocCreateCloseLogLevels = {}; @@ -40,7 +43,7 @@ class StateLogger extends BlocObserver { @override void onChange(BlocBase bloc, Change change) { super.onChange(bloc, change); - _checkLogLevel(_blocChangeLogLevels, LogLevel.debug, bloc, (logLevel) { + _checkLogLevel(_blocChangeLogLevels, LogLevel.off, bloc, (logLevel) { const encoder = JsonEncoder.withIndent(' ', DynamicDebug.toDebug); log.log( logLevel, diff --git a/packages/veilid_support/lib/identity_support/super_identity.dart b/packages/veilid_support/lib/identity_support/super_identity.dart index 5ee8c43..5dc0e90 100644 --- a/packages/veilid_support/lib/identity_support/super_identity.dart +++ b/packages/veilid_support/lib/identity_support/super_identity.dart @@ -23,6 +23,7 @@ part 'super_identity.g.dart'; /// Encryption: None @freezed sealed class SuperIdentity with _$SuperIdentity { + @JsonSerializable() const factory SuperIdentity({ /// Public DHT record storing this structure for account recovery /// changing this can migrate/forward the SuperIdentity to a new DHT record diff --git a/packages/veilid_support/lib/identity_support/super_identity.freezed.dart b/packages/veilid_support/lib/identity_support/super_identity.freezed.dart index b142373..3144205 100644 --- a/packages/veilid_support/lib/identity_support/super_identity.freezed.dart +++ b/packages/veilid_support/lib/identity_support/super_identity.freezed.dart @@ -169,6 +169,7 @@ class _$SuperIdentityCopyWithImpl<$Res> } /// @nodoc + @JsonSerializable() class _SuperIdentity extends SuperIdentity { const _SuperIdentity( diff --git a/packages/veilid_support/lib/src/config.dart b/packages/veilid_support/lib/src/config.dart index 47bd84e..6902479 100644 --- a/packages/veilid_support/lib/src/config.dart +++ b/packages/veilid_support/lib/src/config.dart @@ -4,8 +4,10 @@ import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:veilid/veilid.dart'; +// Allowed to pull sentinel value // ignore: do_not_use_environment const bool kIsReleaseMode = bool.fromEnvironment('dart.vm.product'); +// Allowed to pull sentinel value // ignore: do_not_use_environment const bool kIsProfileMode = bool.fromEnvironment('dart.vm.profile'); const bool kIsDebugMode = !kIsReleaseMode && !kIsProfileMode; @@ -13,18 +15,21 @@ const bool kIsDebugMode = !kIsReleaseMode && !kIsProfileMode; Future> getDefaultVeilidPlatformConfig( bool isWeb, String appName) async { final ignoreLogTargetsStr = + // Allowed to change settings // ignore: do_not_use_environment const String.fromEnvironment('IGNORE_LOG_TARGETS').trim(); final ignoreLogTargets = ignoreLogTargetsStr.isEmpty ? [] : ignoreLogTargetsStr.split(',').map((e) => e.trim()).toList(); + // Allowed to change settings // ignore: do_not_use_environment var flamePathStr = const String.fromEnvironment('FLAME').trim(); if (flamePathStr == '1') { flamePathStr = p.join( (await getApplicationSupportDirectory()).absolute.path, '$appName.folded'); + // Allowed for debugging // ignore: avoid_print print('Flame data logged to $flamePathStr'); } @@ -73,30 +78,37 @@ Future getVeilidConfig(bool isWeb, String programName) async { var config = await getDefaultVeilidConfig( isWeb: isWeb, programName: programName, + // Allowed to change settings // ignore: avoid_redundant_argument_values, do_not_use_environment namespace: const String.fromEnvironment('NAMESPACE'), + // Allowed to change settings // ignore: avoid_redundant_argument_values, do_not_use_environment bootstrap: const String.fromEnvironment('BOOTSTRAP'), + // Allowed to change settings // ignore: avoid_redundant_argument_values, do_not_use_environment networkKeyPassword: const String.fromEnvironment('NETWORK_KEY'), ); + // Allowed to change settings // ignore: do_not_use_environment if (const String.fromEnvironment('DELETE_TABLE_STORE') == '1') { config = config.copyWith(tableStore: config.tableStore.copyWith(delete: true)); } + // Allowed to change settings // ignore: do_not_use_environment if (const String.fromEnvironment('DELETE_PROTECTED_STORE') == '1') { config = config.copyWith( protectedStore: config.protectedStore.copyWith(delete: true)); } + // Allowed to change settings // ignore: do_not_use_environment if (const String.fromEnvironment('DELETE_BLOCK_STORE') == '1') { config = config.copyWith(blockStore: config.blockStore.copyWith(delete: true)); } + // Allowed to change settings // ignore: do_not_use_environment const envNetwork = String.fromEnvironment('NETWORK'); if (envNetwork.isNotEmpty) { @@ -111,7 +123,8 @@ Future getVeilidConfig(bool isWeb, String programName) async { return config.copyWith( capabilities: - // XXX: Remove DHTV and DHTW when we get background sync implemented + // XXX: Remove DHTV and DHTW after DHT widening (and maybe remote + // rehydration?) const VeilidConfigCapabilities(disable: ['DHTV', 'DHTW', 'TUNL']), protectedStore: // XXX: Linux often does not have a secret storage mechanism installed diff --git a/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart b/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart index 81d7f57..92b920f 100644 --- a/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart +++ b/packages/veilid_support/lib/src/table_db_array_protobuf_cubit.dart @@ -24,7 +24,7 @@ class TableDBArrayProtobufStateData final IList windowElements; // The length of the entire array final int length; - // One past the end of the last element + // One past the end of the last element (modulo length, can be zero) final int windowTail; // The total number of elements to try to keep in 'elements' final int windowCount; diff --git a/pubspec.lock b/pubspec.lock index e7252a3..947b17e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -393,6 +393,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + cross_cache: + dependency: transitive + description: + name: cross_cache + sha256: "007d0340c19d4d201192a3335c4034f4b79eae5ea53f90b69eeb5d239d9fbd1d" + url: "https://pub.dev" + source: hosted + version: "1.0.2" cross_file: dependency: transitive description: @@ -542,23 +550,20 @@ packages: url: "https://pub.dev" source: hosted version: "3.4.1" - flutter_chat_types: + flutter_chat_core: dependency: "direct main" description: - name: flutter_chat_types - sha256: e285b588f6d19d907feb1f6d912deaf22e223656769c34093b64e1c59b094fb9 - url: "https://pub.dev" - source: hosted - version: "3.6.2" + path: "../flutter_chat_ui/packages/flutter_chat_core" + relative: true + source: path + version: "2.1.2" flutter_chat_ui: dependency: "direct main" description: - path: "." - ref: main - resolved-ref: d4b9d507d10f5d640156cacfd754f661f8c0f4c1 - url: "https://gitlab.com/veilid/flutter-chat-ui.git" - source: git - version: "1.6.14" + path: "../flutter_chat_ui/packages/flutter_chat_ui" + relative: true + source: path + version: "2.1.3" flutter_form_builder: dependency: "direct main" description: @@ -575,22 +580,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.21.2" - flutter_link_previewer: - dependency: transitive - description: - name: flutter_link_previewer - sha256: "007069e60f42419fb59872beb7a3cc3ea21e9f1bdff5d40239f376fa62ca9f20" - url: "https://pub.dev" - source: hosted - version: "3.2.2" - flutter_linkify: - dependency: transitive - description: - name: flutter_linkify - sha256: "74669e06a8f358fee4512b4320c0b80e51cffc496607931de68d28f099254073" - url: "https://pub.dev" - source: hosted - version: "6.0.0" flutter_localizations: dependency: "direct main" description: flutter @@ -604,14 +593,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.5" - flutter_parsed_text: - dependency: transitive - description: - name: flutter_parsed_text - sha256: "529cf5793b7acdf16ee0f97b158d0d4ba0bf06e7121ef180abe1a5b59e32c1e2" - url: "https://pub.dev" - source: hosted - version: "2.2.1" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -801,6 +782,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + idb_shim: + dependency: transitive + description: + name: idb_shim + sha256: d3dae2085f2dcc9d05b851331fddb66d57d3447ff800de9676b396795436e135 + url: "https://pub.dev" + source: hosted + version: "2.6.5+1" image: dependency: "direct main" description: @@ -857,14 +846,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.9.4" - linkify: - dependency: transitive + keyboard_avoider: + dependency: "direct main" description: - name: linkify - sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832" + name: keyboard_avoider + sha256: d2917bd52c6612bf8d1ff97f74049ddf3592a81d44e814f0e7b07dcfd245b75c url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "0.2.0" lint_hard: dependency: "direct dev" description: @@ -1073,14 +1062,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" - photo_view: - dependency: transitive - description: - name: photo_view - sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e" - url: "https://pub.dev" - source: hosted - version: "0.15.0" pinput: dependency: "direct main" description: @@ -1157,10 +1138,10 @@ packages: dependency: "direct main" description: name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.5" pub_semver: dependency: transitive description: @@ -1305,6 +1286,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + scrollview_observer: + dependency: transitive + description: + name: scrollview_observer + sha256: "437c930927c5a3240ed2d40398f99d96eaca58f861817ff44f6d0c60113bcf9d" + url: "https://pub.dev" + source: hosted + version: "1.26.0" searchable_listview: dependency: "direct main" description: @@ -1314,6 +1303,14 @@ packages: url: "https://gitlab.com/veilid/Searchable-Listview.git" source: git version: "2.16.0" + sembast: + dependency: transitive + description: + name: sembast + sha256: d3f0d0ba501a5f1fd7d6c8532ee01385977c8a069c334635dae390d059ae3d6d + url: "https://pub.dev" + source: hosted + version: "3.8.5" share_plus: dependency: "direct main" description: @@ -1774,7 +1771,7 @@ packages: path: "../veilid/veilid-flutter" relative: true source: path - version: "0.4.4" + version: "0.4.6" veilid_support: dependency: "direct main" description: @@ -1782,14 +1779,6 @@ packages: relative: true source: path version: "1.0.2+0" - visibility_detector: - dependency: transitive - description: - name: visibility_detector - sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 - url: "https://pub.dev" - source: hosted - version: "0.4.0+2" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ac985bf..9cf61e8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,11 +1,11 @@ name: veilidchat description: VeilidChat -publish_to: 'none' +publish_to: "none" version: 0.4.7+20 environment: - sdk: '>=3.2.0 <4.0.0' - flutter: '>=3.22.1' + sdk: ">=3.2.0 <4.0.0" + flutter: ">=3.22.1" dependencies: accordion: ^2.6.0 @@ -37,11 +37,16 @@ dependencies: sdk: flutter flutter_animate: ^4.5.2 flutter_bloc: ^9.1.0 - flutter_chat_types: ^3.6.2 + flutter_chat_core: + git: + url: https://gitlab.com/veilid/flutter-chat-ui.git + path: packages/flutter_chat_core + ref: veilidchat flutter_chat_ui: git: url: https://gitlab.com/veilid/flutter-chat-ui.git - ref: main + path: packages/flutter_chat_ui + ref: veilidchat flutter_form_builder: ^10.0.1 flutter_hooks: ^0.21.2 flutter_localizations: @@ -59,6 +64,7 @@ dependencies: image: ^4.5.3 intl: ^0.19.0 json_annotation: ^4.9.0 + keyboard_avoider: ^0.2.0 loggy: ^2.0.3 meta: ^1.16.0 mobile_scanner: ^6.0.7 @@ -110,15 +116,17 @@ dependencies: xterm: ^4.0.0 zxing2: ^0.2.3 -# dependency_overrides: -# async_tools: -# path: ../dart_async_tools -# bloc_advanced_tools: -# path: ../bloc_advanced_tools -# searchable_listview: -# path: ../Searchable-Listview -# flutter_chat_ui: -# path: ../flutter_chat_ui +dependency_overrides: + # async_tools: + # path: ../dart_async_tools + # bloc_advanced_tools: + # path: ../bloc_advanced_tools + # searchable_listview: + # path: ../Searchable-Listview + flutter_chat_core: + path: ../flutter_chat_ui/packages/flutter_chat_core + flutter_chat_ui: + path: ../flutter_chat_ui/packages/flutter_chat_ui dev_dependencies: build_runner: ^2.4.15 @@ -129,22 +137,22 @@ dev_dependencies: flutter_native_splash: color: "#8588D0" - + icons_launcher: - image_path: 'assets/launcher/icon.png' + image_path: "assets/launcher/icon.png" platforms: android: enable: true - adaptive_background_color: '#ffffff' - adaptive_foreground_image: 'assets/launcher/icon.png' - adaptive_round_image: 'assets/launcher/icon.png' + adaptive_background_color: "#ffffff" + adaptive_foreground_image: "assets/launcher/icon.png" + adaptive_round_image: "assets/launcher/icon.png" ios: enable: true web: enable: true macos: enable: true - image_path: 'assets/launcher/macos_icon.png' + image_path: "assets/launcher/macos_icon.png" windows: enable: true linux: @@ -192,7 +200,7 @@ flutter: - asset: assets/fonts/SourceCodePro-Regular.ttf - asset: assets/fonts/SourceCodePro-Bold.ttf weight: 700 - + # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware From 576c8e477a57ded4b10574601cf684b3dc8a56e5 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 17 May 2025 18:09:33 -0400 Subject: [PATCH 246/270] switch to hosted package --- lib/chat/cubits/chat_component_cubit.dart | 4 ++-- pubspec.lock | 18 ++++++++++-------- pubspec.yaml | 22 +++++++--------------- 3 files changed, 19 insertions(+), 25 deletions(-) diff --git a/lib/chat/cubits/chat_component_cubit.dart b/lib/chat/cubits/chat_component_cubit.dart index 76ff660..a1c64bf 100644 --- a/lib/chat/cubits/chat_component_cubit.dart +++ b/lib/chat/cubits/chat_component_cubit.dart @@ -329,8 +329,8 @@ class ChatComponentCubit extends Cubit { message.sentTimestamp.value.toInt()), sentAt: reconciledAt, id: reconciledId, - text: '${contentText.text} (${message.seqId})', - //text: contentText.text, + //text: '${contentText.text} (${message.seqId})', + text: contentText.text, metadata: { kSeqId: message.seqId, if (core.isOnlyEmoji(contentText.text)) 'isOnlyEmoji': true, diff --git a/pubspec.lock b/pubspec.lock index 947b17e..47990cf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -553,17 +553,19 @@ packages: flutter_chat_core: dependency: "direct main" description: - path: "../flutter_chat_ui/packages/flutter_chat_core" - relative: true - source: path - version: "2.1.2" + name: flutter_chat_core + sha256: "529959634622e9df3b96a4a3764ecc61ec6f0dfa3258a52c139ae10a56ccad80" + url: "https://pub.dev" + source: hosted + version: "2.2.0" flutter_chat_ui: dependency: "direct main" description: - path: "../flutter_chat_ui/packages/flutter_chat_ui" - relative: true - source: path - version: "2.1.3" + name: flutter_chat_ui + sha256: c63df9cd05fe86a3588b4e47f184fbb9e9c3b86153b8a97f3a789e6edb03d28e + url: "https://pub.dev" + source: hosted + version: "2.2.0" flutter_form_builder: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 9cf61e8..73ec83a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,16 +37,8 @@ dependencies: sdk: flutter flutter_animate: ^4.5.2 flutter_bloc: ^9.1.0 - flutter_chat_core: - git: - url: https://gitlab.com/veilid/flutter-chat-ui.git - path: packages/flutter_chat_core - ref: veilidchat - flutter_chat_ui: - git: - url: https://gitlab.com/veilid/flutter-chat-ui.git - path: packages/flutter_chat_ui - ref: veilidchat + flutter_chat_core: ^2.2.0 + flutter_chat_ui: ^2.2.0 flutter_form_builder: ^10.0.1 flutter_hooks: ^0.21.2 flutter_localizations: @@ -116,17 +108,17 @@ dependencies: xterm: ^4.0.0 zxing2: ^0.2.3 -dependency_overrides: + # dependency_overrides: # async_tools: # path: ../dart_async_tools # bloc_advanced_tools: # path: ../bloc_advanced_tools # searchable_listview: # path: ../Searchable-Listview - flutter_chat_core: - path: ../flutter_chat_ui/packages/flutter_chat_core - flutter_chat_ui: - path: ../flutter_chat_ui/packages/flutter_chat_ui + # flutter_chat_core: + # path: ../flutter_chat_ui/packages/flutter_chat_core + # flutter_chat_ui: + # path: ../flutter_chat_ui/packages/flutter_chat_ui dev_dependencies: build_runner: ^2.4.15 From 34f9bea6eb4654bde345d18600717313624f7dbe Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 17 May 2025 19:30:45 -0400 Subject: [PATCH 247/270] add unsent messages, fix hotkeys so they work with chrome --- lib/chat/cubits/chat_component_cubit.dart | 1 + .../cubits/single_contact_messages_cubit.dart | 32 ++--- lib/chat/views/chat_component_widget.dart | 83 ++++++------ lib/keyboard_shortcuts.dart | 47 ++++--- pubspec.lock | 124 +++++++++--------- 5 files changed, 149 insertions(+), 138 deletions(-) diff --git a/lib/chat/cubits/chat_component_cubit.dart b/lib/chat/cubits/chat_component_cubit.dart index a1c64bf..737b1a4 100644 --- a/lib/chat/cubits/chat_component_cubit.dart +++ b/lib/chat/cubits/chat_component_cubit.dart @@ -333,6 +333,7 @@ class ChatComponentCubit extends Cubit { text: contentText.text, metadata: { kSeqId: message.seqId, + kSending: message.sendState == MessageSendState.sending, if (core.isOnlyEmoji(contentText.text)) 'isOnlyEmoji': true, }); return (currentState, textMessage); diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index d93c4be..77b1bb9 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -419,22 +419,22 @@ class SingleContactMessagesCubit extends Cubit { } // Render in-flight messages at the bottom - // - // for (final m in unsentMessages) { - // if (renderedIds.contains(m.authorUniqueIdString)) { - // seqId++; - // continue; - // } - // renderedElements.add(RenderStateElement( - // seqId: seqId, - // message: m, - // isLocal: true, - // sent: true, - // sentOffline: true, - // )); - // renderedIds.add(m.authorUniqueIdString); - // seqId++; - // } + + for (final m in unsentMessages) { + if (renderedIds.contains(m.authorUniqueIdString)) { + seqId++; + continue; + } + renderedElements.add(RenderStateElement( + seqId: seqId, + message: m, + isLocal: true, + sent: true, + sentOffline: true, + )); + renderedIds.add(m.authorUniqueIdString); + seqId++; + } // Render the state final messages = renderedElements diff --git a/lib/chat/views/chat_component_widget.dart b/lib/chat/views/chat_component_widget.dart index aed7356..5ecf6e9 100644 --- a/lib/chat/views/chat_component_widget.dart +++ b/lib/chat/views/chat_component_widget.dart @@ -23,6 +23,7 @@ import 'chat_builders/chat_builders.dart'; const onEndReachedThreshold = 0.75; const _kScrollTag = 'kScrollTag'; const kSeqId = 'seqId'; +const kSending = 'sending'; const maxMessageLength = 2048; class ChatComponentWidget extends StatefulWidget { @@ -321,51 +322,55 @@ class _ChatComponentWidgetState extends State { final windowState = data.value; - await _chatController.setMessages(windowState.window.toList()); + // await _chatController.setMessages(windowState.window.toList()); - // final newMessagesSet = windowState.window.toSet(); - // final newMessagesById = - // Map.fromEntries(newMessagesSet.map((m) => MapEntry(m.id, m))); - // final newMessagesBySeqId = Map.fromEntries( - // newMessagesSet.map((m) => MapEntry(m.metadata![kSeqId], m))); - // final oldMessagesSet = _chatController.messages.toSet(); + final newMessagesSet = windowState.window.toSet(); + final newMessagesById = + Map.fromEntries(newMessagesSet.map((m) => MapEntry(m.id, m))); + final newMessagesBySeqId = Map.fromEntries( + newMessagesSet.map((m) => MapEntry(m.metadata![kSeqId], m))); + final oldMessagesSet = _chatController.messages.toSet(); - // if (oldMessagesSet.isEmpty) { - // await _chatController.setMessages(windowState.window.toList()); - // return; - // } + if (oldMessagesSet.isEmpty) { + await _chatController.setMessages(windowState.window.toList()); + return; + } - // // See how many messages differ by equality (not identity) - // // If there are more than `replaceAllMessagesThreshold` differences - // // just replace the whole list of messages - // final diffs = newMessagesSet.diffAndIntersect(oldMessagesSet, - // diffThisMinusOther: true, diffOtherMinusThis: true); - // final addedMessages = diffs.diffThisMinusOther!; - // final removedMessages = diffs.diffOtherMinusThis!; + // See how many messages differ by equality (not identity) + // If there are more than `replaceAllMessagesThreshold` differences + // just replace the whole list of messages + final diffs = newMessagesSet.diffAndIntersect(oldMessagesSet, + diffThisMinusOther: true, diffOtherMinusThis: true); + final addedMessages = diffs.diffThisMinusOther!; + final removedMessages = diffs.diffOtherMinusThis!; - // final replaceAllPaginationLimit = windowState.windowCount / 3; + final replaceAllPaginationLimit = windowState.windowCount / 3; - // if ((addedMessages.length >= replaceAllPaginationLimit) || - // removedMessages.length >= replaceAllPaginationLimit) { - // await _chatController.setMessages(windowState.window.toList()); - // return; - // } + if ((addedMessages.length >= replaceAllPaginationLimit) || + removedMessages.length >= replaceAllPaginationLimit) { + await _chatController.setMessages(windowState.window.toList()); + return; + } - // // Remove messages that are gone, and replace the ones that have changed - // for (final m in removedMessages) { - // final newm = newMessagesById[m.id]; - // if (newm != null) { - // await _chatController.updateMessage(m, newm); - // } else { - // final newm = newMessagesBySeqId[m.metadata![kSeqId]]; - // if (newm != null) { - // await _chatController.updateMessage(m, newm); - // addedMessages.remove(newm); - // } else { - // await _chatController.removeMessage(m); - // } - // } - // } + // Remove messages that are gone, and replace the ones that have changed + for (final m in removedMessages) { + final newm = newMessagesById[m.id]; + if (newm != null) { + await _chatController.updateMessage(m, newm); + } else { + final newm = newMessagesBySeqId[m.metadata![kSeqId]]; + if (newm != null) { + await _chatController.updateMessage(m, newm); + addedMessages.remove(newm); + } else { + await _chatController.removeMessage(m); + } + } + } + + if (addedMessages.isNotEmpty) { + await _chatController.setMessages(windowState.window.toList()); + } // // // Check for append // if (addedMessages.isNotEmpty) { diff --git a/lib/keyboard_shortcuts.dart b/lib/keyboard_shortcuts.dart index 1e25dac..6708d72 100644 --- a/lib/keyboard_shortcuts.dart +++ b/lib/keyboard_shortcuts.dart @@ -125,27 +125,32 @@ class KeyboardShortcuts extends StatelessWidget { @override Widget build(BuildContext context) => ThemeSwitcher( builder: (context) => Shortcuts( - shortcuts: { - LogicalKeySet( - LogicalKeyboardKey.alt, - LogicalKeyboardKey.control, - LogicalKeyboardKey.keyR): const ReloadThemeIntent(), - LogicalKeySet( - LogicalKeyboardKey.alt, - LogicalKeyboardKey.control, - LogicalKeyboardKey.keyB): const ChangeBrightnessIntent(), - LogicalKeySet( - LogicalKeyboardKey.alt, - LogicalKeyboardKey.control, - LogicalKeyboardKey.keyC): const ChangeColorIntent(), - LogicalKeySet( - LogicalKeyboardKey.alt, - LogicalKeyboardKey.control, - LogicalKeyboardKey.keyD): const AttachDetachIntent(), - LogicalKeySet( - LogicalKeyboardKey.alt, - LogicalKeyboardKey.control, - LogicalKeyboardKey.backquote): const DeveloperPageIntent(), + shortcuts: const { + SingleActivator( + LogicalKeyboardKey.keyR, + control: true, + alt: true, + ): ReloadThemeIntent(), + SingleActivator( + LogicalKeyboardKey.keyB, + control: true, + alt: true, + ): ChangeBrightnessIntent(), + SingleActivator( + LogicalKeyboardKey.keyC, + control: true, + alt: true, + ): ChangeColorIntent(), + SingleActivator( + LogicalKeyboardKey.keyA, + control: true, + alt: true, + ): AttachDetachIntent(), + SingleActivator( + LogicalKeyboardKey.keyD, + control: true, + alt: true, + ): DeveloperPageIntent(), }, child: Actions(actions: >{ ReloadThemeIntent: CallbackAction( diff --git a/pubspec.lock b/pubspec.lock index 47990cf..cdec931 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 + sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f url: "https://pub.dev" source: hosted - version: "80.0.0" + version: "82.0.0" accordion: dependency: "direct main" description: @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: analyzer - sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" + sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0" url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.4.5" animated_bottom_navigation_bar: dependency: "direct main" description: @@ -69,10 +69,10 @@ packages: dependency: "direct main" description: name: archive - sha256: "0c64e928dcbefddecd234205422bcfc2b5e6d31be0b86fef0d0dd48d7b4c9742" + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" url: "https://pub.dev" source: hosted - version: "4.0.4" + version: "4.0.7" args: dependency: transitive description: @@ -141,10 +141,10 @@ packages: dependency: transitive description: name: bidi - sha256: "9a712c7ddf708f7c41b1923aa83648a3ed44cfd75b04f72d598c45e5be287f9d" + sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d" url: "https://pub.dev" source: hosted - version: "2.0.12" + version: "2.0.13" bloc: dependency: "direct main" description: @@ -277,26 +277,26 @@ packages: dependency: transitive description: name: camera_android_camerax - sha256: "13784f539c7f104766bff84e4479a70f03b29d78b208278be45c939250d9d7f5" + sha256: ea7e40bd63afb8f55058e48ec529ce96562be9d08393f79631a06f781161fd0d url: "https://pub.dev" source: hosted - version: "0.6.14+1" + version: "0.6.16" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: ba48b65a3a97004276ede882e6b838d9667642ff462c95a8bb57ca8a82b6bd25 + sha256: ca36181194f429eef3b09de3c96280f2400693f9735025f90d1f4a27465fdd72 url: "https://pub.dev" source: hosted - version: "0.9.18+11" + version: "0.9.19" camera_platform_interface: dependency: transitive description: name: camera_platform_interface - sha256: "953e7baed3a7c8fae92f7200afeb2be503ff1a17c3b4e4ed7b76f008c2810a31" + sha256: "2f757024a48696ff4814a789b0bd90f5660c0fb25f393ab4564fb483327930e2" url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.10.0" camera_web: dependency: transitive description: @@ -485,10 +485,10 @@ packages: dependency: "direct main" description: name: fast_immutable_collections - sha256: "95a69b9380483dff49ae2c12c9eb92e2b4e1aeff481a33c2a20883471771598a" + sha256: d1aa3d7788fab06cce7f303f4969c7a16a10c865e1bd2478291a8ebcbee084e5 url: "https://pub.dev" source: hosted - version: "11.0.3" + version: "11.0.4" ffi: dependency: transitive description: @@ -538,10 +538,10 @@ packages: dependency: "direct main" description: name: flutter_bloc - sha256: "1046d719fbdf230330d3443187cc33cc11963d15c9089f6cc56faa42a4c5f0cc" + sha256: cf51747952201a455a1c840f8171d273be009b932c75093020f9af64f2123e38 url: "https://pub.dev" source: hosted - version: "9.1.0" + version: "9.1.1" flutter_cache_manager: dependency: transitive description: @@ -591,18 +591,18 @@ packages: dependency: "direct main" description: name: flutter_native_splash - sha256: edb09c35ee9230c4b03f13dd45bb3a276d0801865f0a4650b7e2a3bba61a803a + sha256: "8321a6d11a8d13977fa780c89de8d257cce3d841eecfb7a4cadffcc4f12d82dc" url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.4.6" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "5a1e6fb2c0561958d7e4c33574674bda7b77caaca7a33b758876956f2902eea3" + sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e url: "https://pub.dev" source: hosted - version: "2.0.27" + version: "2.0.28" flutter_shaders: dependency: transitive description: @@ -639,10 +639,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b + sha256: d44bf546b13025ec7353091516f6881f1d4c633993cb109c3916c3a0159dadf1 url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.1.0" flutter_translate: dependency: "direct main" description: @@ -676,10 +676,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "7ed2ddaa47524976d5f2aa91432a79da36a76969edd84170777ac5bea82d797c" + sha256: "6022db4c7bfa626841b2a10f34dd1e1b68e8f8f9650db6112dcdeeca45ca793c" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.0.6" freezed_annotation: dependency: "direct main" description: @@ -740,18 +740,18 @@ packages: dependency: transitive description: name: html - sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" url: "https://pub.dev" source: hosted - version: "0.15.5" + version: "0.15.6" http: dependency: transitive description: name: http - sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" http_multi_server: dependency: transitive description: @@ -796,10 +796,10 @@ packages: dependency: "direct main" description: name: image - sha256: "13d3349ace88f12f4a0d175eb5c12dcdd39d35c4c109a8a13dfeb6d0bd9e31c3" + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" url: "https://pub.dev" source: hosted - version: "4.5.3" + version: "4.5.4" indent: dependency: transitive description: @@ -844,10 +844,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: "81f04dee10969f89f604e1249382d46b97a1ccad53872875369622b5bfc9e58a" + sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c url: "https://pub.dev" source: hosted - version: "6.9.4" + version: "6.9.5" keyboard_avoider: dependency: "direct main" description: @@ -860,10 +860,10 @@ packages: dependency: "direct dev" description: name: lint_hard - sha256: ffe7058cb49e021d244d67e650a63380445b56643c2849c6929e938246b99058 + sha256: "2073d4e83ac4e3f2b87cc615fff41abb5c2c5618e117edcd3d71f40f2186f4d5" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "6.1.1" logging: dependency: transitive description: @@ -916,10 +916,10 @@ packages: dependency: "direct main" description: name: mobile_scanner - sha256: "9cb9e371ee9b5b548714f9ab5fd33b530d799745c83d5729ecd1e8ab2935dbd1" + sha256: f536c5b8cadcf73d764bdce09c94744f06aa832264730f8971b21a60c5666826 url: "https://pub.dev" source: hosted - version: "6.0.7" + version: "6.0.10" nested: dependency: transitive description: @@ -996,10 +996,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.16" + version: "2.2.17" path_provider_foundation: dependency: transitive description: @@ -1108,10 +1108,10 @@ packages: dependency: transitive description: name: posix - sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a + sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62 url: "https://pub.dev" source: hosted - version: "6.0.1" + version: "6.0.2" preload_page_view: dependency: "direct main" description: @@ -1292,10 +1292,10 @@ packages: dependency: transitive description: name: scrollview_observer - sha256: "437c930927c5a3240ed2d40398f99d96eaca58f861817ff44f6d0c60113bcf9d" + sha256: "174d4efe7b79459a07662175c4db42c9862dcf78d3978e6e9c2d6c0d8137f4ca" url: "https://pub.dev" source: hosted - version: "1.26.0" + version: "1.26.1" searchable_listview: dependency: "direct main" description: @@ -1333,18 +1333,18 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "3ec7210872c4ba945e3244982918e502fa2bfb5230dff6832459ca0e1879b7ad" + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.10" shared_preferences_foundation: dependency: transitive description: @@ -1603,10 +1603,10 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "6c7653816b1c938e121b69ff63a33c9dc68102b65a5fb0a5c0f9786256ed33e6" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.5" timing: dependency: transitive description: @@ -1667,18 +1667,18 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4" + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" url: "https://pub.dev" source: hosted - version: "6.3.15" + version: "6.3.16" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.3.3" url_launcher_linux: dependency: transitive description: @@ -1707,10 +1707,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" url_launcher_windows: dependency: transitive description: @@ -1801,26 +1801,26 @@ packages: dependency: transitive description: name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" win32: dependency: transitive description: name: win32 - sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f + sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba" url: "https://pub.dev" source: hosted - version: "5.12.0" + version: "5.13.0" window_manager: dependency: "direct main" description: From 61855521dc078076234a73a118629a2d97a6fb4e Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 18 May 2025 11:33:08 -0400 Subject: [PATCH 248/270] eliminate race condition with listen/watch --- .../cubits/reconciliation/message_reconciliation.dart | 11 +++++------ lib/chat/cubits/single_contact_messages_cubit.dart | 9 ++++++--- .../lib/dht_support/src/dht_log/dht_log_spine.dart | 3 +-- .../src/dht_short_array/dht_short_array_head.dart | 4 ++-- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/lib/chat/cubits/reconciliation/message_reconciliation.dart b/lib/chat/cubits/reconciliation/message_reconciliation.dart index 683b46d..f6d46c3 100644 --- a/lib/chat/cubits/reconciliation/message_reconciliation.dart +++ b/lib/chat/cubits/reconciliation/message_reconciliation.dart @@ -32,11 +32,10 @@ class MessageReconciliation { final activeInputQueues = await _updateAuthorInputQueues(); // Process all input queues together - await _outputCubit - .operate((reconciledArray) async => _reconcileInputQueues( - reconciledArray: reconciledArray, - activeInputQueues: activeInputQueues, - )); + await _outputCubit.operate((reconciledArray) => _reconcileInputQueues( + reconciledArray: reconciledArray, + activeInputQueues: activeInputQueues, + )); }); } @@ -273,5 +272,5 @@ class MessageReconciliation { final TableDBArrayProtobufCubit _outputCubit; final void Function(Object, StackTrace?) _onError; - static const int _maxReconcileChunk = 65536; + static const _maxReconcileChunk = 65536; } diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 77b1bb9..0e20229 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -102,7 +102,7 @@ class SingleContactMessagesCubit extends Cubit { } // Initialize everything - Future _init(Completer _cancel) async { + Future _init(Completer _) async { _unsentMessagesQueue = PersistentQueue( table: 'SingleContactUnsentMessages', key: _remoteConversationRecordKey.toString(), @@ -126,6 +126,9 @@ class SingleContactMessagesCubit extends Cubit { // Command execution background process _commandRunnerFut = Future.delayed(Duration.zero, _commandRunner); + + // Run reconciliation once for all input queues + _reconciliation.reconcileMessages(null); } // Make crypto @@ -198,7 +201,7 @@ class SingleContactMessagesCubit extends Cubit { }); } - Future _makeLocalMessagesCrypto() async => + Future _makeLocalMessagesCrypto() => VeilidCryptoPrivate.fromTypedKey( _accountInfo.userLogin!.identitySecret, 'tabledb'); @@ -210,7 +213,7 @@ class SingleContactMessagesCubit extends Cubit { final crypto = await _makeLocalMessagesCrypto(); _reconciledMessagesCubit = TableDBArrayProtobufCubit( - open: () async => TableDBArrayProtobuf.make( + open: () => TableDBArrayProtobuf.make( table: tableName, crypto: crypto, fromBuffer: proto.ReconciledMessage.fromBuffer), diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index d9f5df2..93cdcc4 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -608,14 +608,13 @@ class _DHTLogSpine { Future watch() async { // This will update any existing watches if necessary try { - await _spineRecord.watch(subkeys: [ValueSubkeyRange.single(0)]); - // Update changes to the head record // xxx: check if this localChanges can be false... // xxx: Don't watch for local changes because this class already handles // xxx: notifying listeners and knows when it makes local changes _subscription ??= await _spineRecord.listen(localChanges: true, _onSpineChanged); + await _spineRecord.watch(subkeys: [ValueSubkeyRange.single(0)]); } on Exception { // If anything fails, try to cancel the watches await cancelWatch(); 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 66b5baa..1785b28 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 @@ -482,13 +482,13 @@ class _DHTShortArrayHead { 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, _onHeadValueChanged); + + await _headRecord.watch(subkeys: [ValueSubkeyRange.single(0)]); } on Exception { // If anything fails, try to cancel the watches await cancelWatch(); From be8014c97aaf9d99b2496142319962942837d599 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 23 May 2025 11:30:26 -0400 Subject: [PATCH 249/270] State machine work --- assets/i18n/en.json | 4 +- ...per_account_collection_bloc_map_cubit.dart | 14 +- .../per_account_collection_state.freezed.dart | 12 +- .../models/user_login/user_login.freezed.dart | 24 +- .../views/edit_account_page.dart | 6 +- .../cubits/single_contact_messages_cubit.dart | 7 +- .../models/chat_component_state.freezed.dart | 36 +-- lib/chat/views/chat_component_widget.dart | 22 +- .../cubits/contact_request_inbox_cubit.dart | 3 + .../cubits/waiting_invitation_cubit.dart | 288 ++++++++++++------ .../waiting_invitations_bloc_map_cubit.dart | 120 +++++--- .../models/valid_contact_invitation.dart | 51 ++-- .../views/paste_invitation_dialog.dart | 21 +- .../views/scan_invitation_dialog.dart | 6 +- .../views/contact_details_widget.dart | 3 +- lib/contacts/views/contact_item_widget.dart | 1 - lib/contacts/views/contacts_browser.dart | 37 +-- lib/contacts/views/contacts_page.dart | 37 ++- .../active_conversations_bloc_map_cubit.dart | 13 +- ...ve_single_contact_chat_bloc_map_cubit.dart | 25 +- .../cubits/conversation_cubit.dart | 96 +++--- lib/tools/state_logger.dart | 1 + macos/Podfile.lock | 9 +- packages/veilid_support/example/pubspec.lock | 70 +++-- .../src/dht_log/dht_log_spine.dart | 2 +- .../dht_record/default_dht_record_cubit.dart | 6 +- .../src/dht_record/dht_record_cubit.dart | 6 +- .../identity_support/identity_support.dart | 1 + .../lib/identity_support/super_identity.dart | 67 ++-- .../super_identity_cubit.dart | 21 ++ packages/veilid_support/pubspec.lock | 82 ++--- packages/veilid_support/pubspec.yaml | 4 +- pubspec.lock | 74 ++--- pubspec.yaml | 39 +-- 34 files changed, 703 insertions(+), 505 deletions(-) create mode 100644 packages/veilid_support/lib/identity_support/super_identity_cubit.dart diff --git a/assets/i18n/en.json b/assets/i18n/en.json index f0b84a0..53bf461 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -213,7 +213,9 @@ }, "waiting_invitation": { "accepted": "Contact invitation accepted from {name}", - "rejected": "Contact invitation was rejected" + "rejected": "Contact invitation was rejected", + "invalid": "Contact invitation was not valid", + "init_failed": "Contact initialization failed" }, "paste_invitation_dialog": { "title": "Paste Contact Invite", diff --git a/lib/account_manager/cubits/per_account_collection_bloc_map_cubit.dart b/lib/account_manager/cubits/per_account_collection_bloc_map_cubit.dart index ded50e4..e63b53e 100644 --- a/lib/account_manager/cubits/per_account_collection_bloc_map_cubit.dart +++ b/lib/account_manager/cubits/per_account_collection_bloc_map_cubit.dart @@ -22,11 +22,11 @@ class PerAccountCollectionBlocMapCubit extends BlocMapCubit _addPerAccountCollectionCubit( - {required TypedKey superIdentityRecordKey}) async => + void _addPerAccountCollectionCubit( + {required TypedKey superIdentityRecordKey}) => add( superIdentityRecordKey, - () async => PerAccountCollectionCubit( + () => PerAccountCollectionCubit( locator: _locator, accountInfoCubit: AccountInfoCubit( accountRepository: _accountRepository, @@ -35,11 +35,11 @@ class PerAccountCollectionBlocMapCubit extends BlocMapCubit removeFromState(TypedKey key) => remove(key); + void removeFromState(TypedKey key) => remove(key); @override - Future updateState( - TypedKey key, LocalAccount? oldValue, LocalAccount newValue) async { + void updateState( + TypedKey key, LocalAccount? oldValue, LocalAccount newValue) { // Don't replace unless this is a totally different account // The sub-cubit's subscription will update our state later if (oldValue != null) { @@ -53,7 +53,7 @@ class PerAccountCollectionBlocMapCubit extends BlocMapCubit { @useResult $Res call( {AccountInfo accountInfo, - AsyncValue? avAccountRecordState, + AsyncValue? avAccountRecordState, AccountInfoCubit? accountInfoCubit, AccountRecordCubit? accountRecordCubit, ContactInvitationListCubit? contactInvitationListCubit, @@ -147,9 +147,9 @@ class _$PerAccountCollectionStateCopyWithImpl<$Res> : accountInfo // ignore: cast_nullable_to_non_nullable as AccountInfo, avAccountRecordState: freezed == avAccountRecordState - ? _self.avAccountRecordState! + ? _self.avAccountRecordState : avAccountRecordState // ignore: cast_nullable_to_non_nullable - as AsyncValue?, + as AsyncValue?, accountInfoCubit: freezed == accountInfoCubit ? _self.accountInfoCubit : accountInfoCubit // ignore: cast_nullable_to_non_nullable @@ -227,7 +227,7 @@ class _PerAccountCollectionState extends PerAccountCollectionState { @override final AccountInfo accountInfo; @override - final AsyncValue? avAccountRecordState; + final AsyncValue? avAccountRecordState; @override final AccountInfoCubit? accountInfoCubit; @override @@ -326,7 +326,7 @@ abstract mixin class _$PerAccountCollectionStateCopyWith<$Res> @useResult $Res call( {AccountInfo accountInfo, - AsyncValue? avAccountRecordState, + AsyncValue? avAccountRecordState, AccountInfoCubit? accountInfoCubit, AccountRecordCubit? accountRecordCubit, ContactInvitationListCubit? contactInvitationListCubit, @@ -375,7 +375,7 @@ class __$PerAccountCollectionStateCopyWithImpl<$Res> avAccountRecordState: freezed == avAccountRecordState ? _self.avAccountRecordState : avAccountRecordState // ignore: cast_nullable_to_non_nullable - as AsyncValue?, + as AsyncValue?, accountInfoCubit: freezed == accountInfoCubit ? _self.accountInfoCubit : accountInfoCubit // ignore: cast_nullable_to_non_nullable 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 c406812..914afb8 100644 --- a/lib/account_manager/models/user_login/user_login.freezed.dart +++ b/lib/account_manager/models/user_login/user_login.freezed.dart @@ -67,8 +67,8 @@ abstract mixin class $UserLoginCopyWith<$Res> { _$UserLoginCopyWithImpl; @useResult $Res call( - {Typed superIdentityRecordKey, - Typed identitySecret, + {TypedKey superIdentityRecordKey, + TypedSecret identitySecret, AccountRecordInfo accountRecordInfo, Timestamp lastActive}); @@ -94,13 +94,13 @@ class _$UserLoginCopyWithImpl<$Res> implements $UserLoginCopyWith<$Res> { }) { return _then(_self.copyWith( superIdentityRecordKey: null == superIdentityRecordKey - ? _self.superIdentityRecordKey! + ? _self.superIdentityRecordKey : superIdentityRecordKey // ignore: cast_nullable_to_non_nullable - as Typed, + as TypedKey, identitySecret: null == identitySecret - ? _self.identitySecret! + ? _self.identitySecret : identitySecret // ignore: cast_nullable_to_non_nullable - as Typed, + as TypedSecret, accountRecordInfo: null == accountRecordInfo ? _self.accountRecordInfo : accountRecordInfo // ignore: cast_nullable_to_non_nullable @@ -136,10 +136,10 @@ class _UserLogin implements UserLogin { // SuperIdentity record key for the user // used to index the local accounts table @override - final Typed superIdentityRecordKey; + final TypedKey superIdentityRecordKey; // The identity secret as unlocked from the local accounts table @override - final Typed identitySecret; + final TypedSecret identitySecret; // The account record key, owner key and secret pulled from the identity @override final AccountRecordInfo accountRecordInfo; @@ -197,8 +197,8 @@ abstract mixin class _$UserLoginCopyWith<$Res> @override @useResult $Res call( - {Typed superIdentityRecordKey, - Typed identitySecret, + {TypedKey superIdentityRecordKey, + TypedSecret identitySecret, AccountRecordInfo accountRecordInfo, Timestamp lastActive}); @@ -227,11 +227,11 @@ class __$UserLoginCopyWithImpl<$Res> implements _$UserLoginCopyWith<$Res> { superIdentityRecordKey: null == superIdentityRecordKey ? _self.superIdentityRecordKey : superIdentityRecordKey // ignore: cast_nullable_to_non_nullable - as Typed, + as TypedKey, identitySecret: null == identitySecret ? _self.identitySecret : identitySecret // ignore: cast_nullable_to_non_nullable - as Typed, + as TypedSecret, accountRecordInfo: null == accountRecordInfo ? _self.accountRecordInfo : accountRecordInfo // ignore: cast_nullable_to_non_nullable diff --git a/lib/account_manager/views/edit_account_page.dart b/lib/account_manager/views/edit_account_page.dart index 9af4719..bb14968 100644 --- a/lib/account_manager/views/edit_account_page.dart +++ b/lib/account_manager/views/edit_account_page.dart @@ -214,9 +214,9 @@ class _EditAccountPageState extends WindowSetupState { // Look up account cubit for this specific account final perAccountCollectionBlocMapCubit = context.read(); - final accountRecordCubit = await perAccountCollectionBlocMapCubit - .operate(widget.superIdentityRecordKey, - closure: (c) async => c.accountRecordCubit); + final accountRecordCubit = perAccountCollectionBlocMapCubit + .entry(widget.superIdentityRecordKey) + ?.accountRecordCubit; if (accountRecordCubit == null) { return false; } diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 0e20229..878858b 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -176,12 +176,11 @@ class SingleContactMessagesCubit extends Cubit { _remoteIdentityPublicKey, rcvdMessagesDHTLog); } - Future updateRemoteMessagesRecordKey( - TypedKey? remoteMessagesRecordKey) async { - await _initWait(); - + void updateRemoteMessagesRecordKey(TypedKey? remoteMessagesRecordKey) { _sspRemoteConversationRecordKey.updateState(remoteMessagesRecordKey, (remoteMessagesRecordKey) async { + await _initWait(); + // Don't bother if nothing is changing if (_remoteMessagesRecordKey == remoteMessagesRecordKey) { return; diff --git a/lib/chat/models/chat_component_state.freezed.dart b/lib/chat/models/chat_component_state.freezed.dart index dd5e68e..bbecc7a 100644 --- a/lib/chat/models/chat_component_state.freezed.dart +++ b/lib/chat/models/chat_component_state.freezed.dart @@ -67,9 +67,9 @@ abstract mixin class $ChatComponentStateCopyWith<$Res> { @useResult $Res call( {User? localUser, - IMap remoteUsers, - IMap historicalRemoteUsers, - IMap unknownUsers, + IMap remoteUsers, + IMap historicalRemoteUsers, + IMap unknownUsers, AsyncValue> messageWindow, String title}); @@ -103,17 +103,17 @@ class _$ChatComponentStateCopyWithImpl<$Res> : localUser // ignore: cast_nullable_to_non_nullable as User?, remoteUsers: null == remoteUsers - ? _self.remoteUsers! + ? _self.remoteUsers : remoteUsers // ignore: cast_nullable_to_non_nullable - as IMap, + as IMap, historicalRemoteUsers: null == historicalRemoteUsers - ? _self.historicalRemoteUsers! + ? _self.historicalRemoteUsers : historicalRemoteUsers // ignore: cast_nullable_to_non_nullable - as IMap, + as IMap, unknownUsers: null == unknownUsers - ? _self.unknownUsers! + ? _self.unknownUsers : unknownUsers // ignore: cast_nullable_to_non_nullable - as IMap, + as IMap, messageWindow: null == messageWindow ? _self.messageWindow : messageWindow // ignore: cast_nullable_to_non_nullable @@ -167,13 +167,13 @@ class _ChatComponentState implements ChatComponentState { final User? localUser; // Active remote users @override - final IMap remoteUsers; + final IMap remoteUsers; // Historical remote users @override - final IMap historicalRemoteUsers; + final IMap historicalRemoteUsers; // Unknown users @override - final IMap unknownUsers; + final IMap unknownUsers; // Messages state @override final AsyncValue> messageWindow; @@ -227,9 +227,9 @@ abstract mixin class _$ChatComponentStateCopyWith<$Res> @useResult $Res call( {User? localUser, - IMap remoteUsers, - IMap historicalRemoteUsers, - IMap unknownUsers, + IMap remoteUsers, + IMap historicalRemoteUsers, + IMap unknownUsers, AsyncValue> messageWindow, String title}); @@ -267,15 +267,15 @@ class __$ChatComponentStateCopyWithImpl<$Res> remoteUsers: null == remoteUsers ? _self.remoteUsers : remoteUsers // ignore: cast_nullable_to_non_nullable - as IMap, + as IMap, historicalRemoteUsers: null == historicalRemoteUsers ? _self.historicalRemoteUsers : historicalRemoteUsers // ignore: cast_nullable_to_non_nullable - as IMap, + as IMap, unknownUsers: null == unknownUsers ? _self.unknownUsers : unknownUsers // ignore: cast_nullable_to_non_nullable - as IMap, + as IMap, messageWindow: null == messageWindow ? _self.messageWindow : messageWindow // ignore: cast_nullable_to_non_nullable diff --git a/lib/chat/views/chat_component_widget.dart b/lib/chat/views/chat_component_widget.dart index 5ecf6e9..7d90d89 100644 --- a/lib/chat/views/chat_component_widget.dart +++ b/lib/chat/views/chat_component_widget.dart @@ -54,10 +54,9 @@ class ChatComponentWidget extends StatefulWidget { final contactListCubit = context.watch(); // Get the active conversation cubit - final activeConversationCubit = context - .select( - (x) => x.tryOperateSync(localConversationRecordKey, - closure: (cubit) => cubit)); + final activeConversationCubit = context.select< + ActiveConversationsBlocMapCubit, + ActiveConversationCubit?>((x) => x.entry(localConversationRecordKey)); if (activeConversationCubit == null) { return waitingPage(onCancel: onCancel); } @@ -65,8 +64,7 @@ class ChatComponentWidget extends StatefulWidget { // Get the messages cubit final messagesCubit = context.select( - (x) => x.tryOperateSync(localConversationRecordKey, - closure: (cubit) => cubit)); + (x) => x.entry(localConversationRecordKey)); if (messagesCubit == null) { return waitingPage(onCancel: onCancel); } @@ -106,10 +104,11 @@ class _ChatComponentWidgetState extends State { _textEditingController = TextEditingController(); _scrollController = ScrollController(); _chatStateProcessor = SingleStateProcessor(); + _focusNode = FocusNode(); - final _chatComponentCubit = context.read(); - _chatStateProcessor.follow(_chatComponentCubit.stream, - _chatComponentCubit.state, _updateChatState); + final chatComponentCubit = context.read(); + _chatStateProcessor.follow( + chatComponentCubit.stream, chatComponentCubit.state, _updateChatState); super.initState(); } @@ -118,6 +117,7 @@ class _ChatComponentWidgetState extends State { void dispose() { unawaited(_chatStateProcessor.close()); + _focusNode.dispose(); _chatController.dispose(); _scrollController.dispose(); _textEditingController.dispose(); @@ -281,6 +281,7 @@ class _ChatComponentWidgetState extends State { // Composer builder composerBuilder: (ctx) => VcComposerWidget( autofocus: true, + focusNode: _focusNode, textInputAction: isAnyMobile ? TextInputAction.newline : TextInputAction.send, @@ -399,6 +400,8 @@ class _ChatComponentWidgetState extends State { } void _handleSendPressed(ChatComponentCubit chatComponentCubit, String text) { + _focusNode.requestFocus(); + if (text.startsWith('/')) { chatComponentCubit.runCommand(text); return; @@ -491,4 +494,5 @@ class _ChatComponentWidgetState extends State { late final TextEditingController _textEditingController; late final ScrollController _scrollController; late final SingleStateProcessor _chatStateProcessor; + late final FocusNode _focusNode; } diff --git a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart index 714201b..198ae85 100644 --- a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart +++ b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart @@ -1,9 +1,12 @@ +import 'package:async_tools/async_tools.dart'; 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 +typedef ContactRequestInboxState = AsyncValue; + class ContactRequestInboxCubit extends DefaultDHTRecordCubit { ContactRequestInboxCubit( diff --git a/lib/contact_invitation/cubits/waiting_invitation_cubit.dart b/lib/contact_invitation/cubits/waiting_invitation_cubit.dart index 47addc2..b712546 100644 --- a/lib/contact_invitation/cubits/waiting_invitation_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitation_cubit.dart @@ -9,10 +9,62 @@ import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../conversation/conversation.dart'; import '../../proto/proto.dart' as proto; -import '../../tools/tools.dart'; import '../models/accepted_contact.dart'; import 'contact_request_inbox_cubit.dart'; +/// State of WaitingInvitationCubit +sealed class WaitingInvitationState + implements StateMachineState { + WaitingInvitationState({required this.global}); + final WaitingInvitationStateGlobal global; +} + +class WaitingInvitationStateGlobal { + WaitingInvitationStateGlobal( + {required this.accountInfo, + required this.accountRecordCubit, + required this.contactInvitationRecord}); + final AccountInfo accountInfo; + final AccountRecordCubit accountRecordCubit; + final proto.ContactInvitationRecord contactInvitationRecord; +} + +/// State of WaitingInvitationCubit: +/// Signature was invalid +class WaitingInvitationStateInvalidSignature + with StateMachineEndState + implements WaitingInvitationState { + const WaitingInvitationStateInvalidSignature({required this.global}); + + @override + final WaitingInvitationStateGlobal global; +} + +/// State of WaitingInvitationCubit: +/// Failed to initialize +class WaitingInvitationStateInitFailed + with StateMachineEndState + implements WaitingInvitationState { + const WaitingInvitationStateInitFailed( + {required this.global, required this.exception}); + + @override + final WaitingInvitationStateGlobal global; + final Exception exception; +} + +/// State of WaitingInvitationCubit: +/// Finished normally with an invitation status +class WaitingInvitationStateInvitationStatus + with StateMachineEndState + implements WaitingInvitationState { + const WaitingInvitationStateInvitationStatus( + {required this.global, required this.status}); + @override + final WaitingInvitationStateGlobal global; + final InvitationStatus status; +} + @immutable class InvitationStatus extends Equatable { const InvitationStatus({required this.acceptedContact}); @@ -22,94 +74,160 @@ class InvitationStatus extends Equatable { List get props => [acceptedContact]; } -class WaitingInvitationCubit extends AsyncTransformerCubit { - WaitingInvitationCubit( - ContactRequestInboxCubit super.input, { +/// State of WaitingInvitationCubit: +/// Waiting for the invited contact to accept/reject the invitation +class WaitingInvitationStateWaitForContactResponse + extends AsyncCubitReactorState< + WaitingInvitationState, + ContactRequestInboxState, + ContactRequestInboxCubit> implements WaitingInvitationState { + WaitingInvitationStateWaitForContactResponse(super.create, + {required this.global}) + : super(onState: (ctx) async { + final signedContactResponse = ctx.state.asData?.value; + if (signedContactResponse == null) { + return null; + } + + final contactResponse = proto.ContactResponse.fromBuffer( + signedContactResponse.contactResponse); + final contactSuperRecordKey = + contactResponse.superIdentityRecordKey.toVeilid(); + + // Fetch the remote contact's account superidentity + return WaitingInvitationStateWaitForContactSuperIdentity( + () => SuperIdentityCubit(superRecordKey: contactSuperRecordKey), + global: global, + signedContactResponse: signedContactResponse); + }); + + @override + final WaitingInvitationStateGlobal global; +} + +/// State of WaitingInvitationCubit: +/// Once an accept/reject happens, get the SuperIdentity of the recipient +class WaitingInvitationStateWaitForContactSuperIdentity + extends AsyncCubitReactorState implements WaitingInvitationState { + WaitingInvitationStateWaitForContactSuperIdentity(super.create, + {required this.global, + required proto.SignedContactResponse signedContactResponse}) + : super(onState: (ctx) async { + final contactSuperIdentity = ctx.state.asData?.value; + if (contactSuperIdentity == null) { + return null; + } + + final contactResponseBytes = + Uint8List.fromList(signedContactResponse.contactResponse); + final contactResponse = + proto.ContactResponse.fromBuffer(contactResponseBytes); + + // Verify + final idcs = await contactSuperIdentity.currentInstance.cryptoSystem; + final signature = signedContactResponse.identitySignature.toVeilid(); + if (!await idcs.verify(contactSuperIdentity.currentInstance.publicKey, + contactResponseBytes, signature)) { + // Could not verify signature of contact response + return WaitingInvitationStateInvalidSignature( + global: global, + ); + } + + // Check for rejection + if (!contactResponse.accept) { + // Rejection + return WaitingInvitationStateInvitationStatus( + global: global, + status: const InvitationStatus(acceptedContact: null), + ); + } + + // Pull profile from remote conversation key + final remoteConversationRecordKey = + contactResponse.remoteConversationRecordKey.toVeilid(); + + return WaitingInvitationStateWaitForConversation( + () => ConversationCubit( + accountInfo: global.accountInfo, + remoteIdentityPublicKey: + contactSuperIdentity.currentInstance.typedPublicKey, + remoteConversationRecordKey: remoteConversationRecordKey), + global: global, + remoteConversationRecordKey: remoteConversationRecordKey, + contactSuperIdentity: contactSuperIdentity, + ); + }); + + @override + final WaitingInvitationStateGlobal global; +} + +/// State of WaitingInvitationCubit: +/// Wait for the conversation cubit to initialize so we can return the +/// accepted invitation +class WaitingInvitationStateWaitForConversation extends AsyncCubitReactorState< + WaitingInvitationState, + AsyncValue, + ConversationCubit> implements WaitingInvitationState { + WaitingInvitationStateWaitForConversation(super.create, + {required this.global, + required TypedKey remoteConversationRecordKey, + required SuperIdentity contactSuperIdentity}) + : super(onState: (ctx) async { + final remoteConversation = ctx.state.asData?.value.remoteConversation; + final localConversation = ctx.state.asData?.value.localConversation; + if (remoteConversation == null || localConversation != null) { + return null; + } + + // Stop reacting to the conversation cubit + ctx.stop(); + + // Complete the local conversation now that we have the remote profile + final remoteProfile = remoteConversation.profile; + final localConversationRecordKey = global + .contactInvitationRecord.localConversationRecordKey + .toVeilid(); + + try { + await ctx.cubit.initLocalConversation( + profile: global.accountRecordCubit.state.asData!.value.profile, + existingConversationRecordKey: localConversationRecordKey); + } on Exception catch (e) { + return WaitingInvitationStateInitFailed( + global: global, exception: e); + } + + return WaitingInvitationStateInvitationStatus( + global: global, + status: InvitationStatus( + acceptedContact: AcceptedContact( + remoteProfile: remoteProfile, + remoteIdentity: contactSuperIdentity, + remoteConversationRecordKey: remoteConversationRecordKey, + localConversationRecordKey: localConversationRecordKey))); + }); + + @override + final WaitingInvitationStateGlobal global; +} + +/// Invitation state processor for sent invitations +class WaitingInvitationCubit extends StateMachineCubit { + WaitingInvitationCubit({ + required ContactRequestInboxCubit Function() initialStateCreate, required AccountInfo accountInfo, required AccountRecordCubit accountRecordCubit, required proto.ContactInvitationRecord contactInvitationRecord, }) : super( - transform: (signedContactResponse) => _transform( - signedContactResponse, + WaitingInvitationStateWaitForContactResponse( + initialStateCreate, + global: WaitingInvitationStateGlobal( accountInfo: accountInfo, accountRecordCubit: accountRecordCubit, - contactInvitationRecord: contactInvitationRecord)); - - static Future> _transform( - proto.SignedContactResponse? signedContactResponse, - {required AccountInfo accountInfo, - required AccountRecordCubit accountRecordCubit, - required proto.ContactInvitationRecord contactInvitationRecord}) async { - if (signedContactResponse == null) { - return const AsyncValue.loading(); - } - - final contactResponseBytes = - Uint8List.fromList(signedContactResponse.contactResponse); - final contactResponse = - proto.ContactResponse.fromBuffer(contactResponseBytes); - final contactIdentityMasterRecordKey = - contactResponse.superIdentityRecordKey.toVeilid(); - - // Fetch the remote contact's account master - final contactSuperIdentity = await SuperIdentity.open( - superRecordKey: contactIdentityMasterRecordKey); - - // Verify - final idcs = await contactSuperIdentity.currentInstance.cryptoSystem; - final signature = signedContactResponse.identitySignature.toVeilid(); - if (!await idcs.verify(contactSuperIdentity.currentInstance.publicKey, - contactResponseBytes, signature)) { - // Could not verify signature of contact response - return AsyncValue.error('Invalid signature on contact response.'); - } - - // 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( - accountInfo: accountInfo, - remoteIdentityPublicKey: - contactSuperIdentity.currentInstance.typedPublicKey, - remoteConversationRecordKey: remoteConversationRecordKey); - - // wait for remote conversation for up to 20 seconds - proto.Conversation? remoteConversation; - var retryCount = 20; - do { - await conversation.refresh(); - remoteConversation = conversation.state.asData?.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( - profile: accountRecordCubit.state.asData!.value.profile, - existingConversationRecordKey: localConversationRecordKey, - callback: (localConversation) async => AsyncValue.data(InvitationStatus( - acceptedContact: AcceptedContact( - remoteProfile: remoteProfile, - remoteIdentity: contactSuperIdentity, - remoteConversationRecordKey: remoteConversationRecordKey, - localConversationRecordKey: localConversationRecordKey)))); - } + contactInvitationRecord: contactInvitationRecord), + ), + ); } 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 197ba8c..f125e71 100644 --- a/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart @@ -10,13 +10,13 @@ import '../../proto/proto.dart' as proto; import 'cubits.dart'; typedef WaitingInvitationsBlocMapState - = BlocMapState>; + = BlocMapState; // 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, WaitingInvitationCubit> + WaitingInvitationState, WaitingInvitationCubit> with StateMapFollower, TypedKey, proto.ContactInvitationRecord> { @@ -45,13 +45,12 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit _addWaitingInvitation( - {required proto.ContactInvitationRecord - contactInvitationRecord}) async => + void _addWaitingInvitation( + {required proto.ContactInvitationRecord contactInvitationRecord}) => add( contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(), - () async => WaitingInvitationCubit( - ContactRequestInboxCubit( + () => WaitingInvitationCubit( + initialStateCreate: () => ContactRequestInboxCubit( accountInfo: _accountInfo, contactInvitationRecord: contactInvitationRecord), accountInfo: _accountInfo, @@ -63,44 +62,73 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit removeFromState(TypedKey key) => remove(key); + void removeFromState(TypedKey key) => remove(key); @override - Future updateState( - TypedKey key, - proto.ContactInvitationRecord? oldValue, - proto.ContactInvitationRecord newValue) async { - await _addWaitingInvitation(contactInvitationRecord: newValue); + void updateState(TypedKey key, proto.ContactInvitationRecord? oldValue, + proto.ContactInvitationRecord newValue) { + _addWaitingInvitation(contactInvitationRecord: newValue); } //// diff --git a/lib/contact_invitation/models/valid_contact_invitation.dart b/lib/contact_invitation/models/valid_contact_invitation.dart index f19e951..c39692c 100644 --- a/lib/contact_invitation/models/valid_contact_invitation.dart +++ b/lib/contact_invitation/models/valid_contact_invitation.dart @@ -47,36 +47,35 @@ class ValidContactInvitation { accountInfo: _accountInfo, remoteIdentityPublicKey: _contactSuperIdentity.currentInstance.typedPublicKey); - return conversation.initLocalConversation( - profile: profile, - callback: (localConversation) async { - final contactResponse = proto.ContactResponse() - ..accept = true - ..remoteConversationRecordKey = localConversation.key.toProto() - ..superIdentityRecordKey = - _accountInfo.superIdentityRecordKey.toProto(); - final contactResponseBytes = contactResponse.writeToBuffer(); + final localConversationRecordKey = + await conversation.initLocalConversation(profile: profile); - final cs = await _accountInfo.identityCryptoSystem; - final identitySignature = await cs.signWithKeyPair( - _accountInfo.identityWriter, contactResponseBytes); + final contactResponse = proto.ContactResponse() + ..accept = true + ..remoteConversationRecordKey = localConversationRecordKey.toProto() + ..superIdentityRecordKey = + _accountInfo.superIdentityRecordKey.toProto(); + final contactResponseBytes = contactResponse.writeToBuffer(); - final signedContactResponse = proto.SignedContactResponse() - ..contactResponse = contactResponseBytes - ..identitySignature = identitySignature.toProto(); + final cs = await _accountInfo.identityCryptoSystem; + final identitySignature = await cs.signWithKeyPair( + _accountInfo.identityWriter, contactResponseBytes); - // Write the acceptance to the inbox - await contactRequestInbox - .eventualWriteProtobuf(signedContactResponse, subkey: 1); + final signedContactResponse = proto.SignedContactResponse() + ..contactResponse = contactResponseBytes + ..identitySignature = identitySignature.toProto(); - return AcceptedContact( - remoteProfile: _contactRequestPrivate.profile, - remoteIdentity: _contactSuperIdentity, - remoteConversationRecordKey: - _contactRequestPrivate.chatRecordKey.toVeilid(), - localConversationRecordKey: localConversation.key, - ); - }); + // Write the acceptance to the inbox + await contactRequestInbox.eventualWriteProtobuf(signedContactResponse, + subkey: 1); + + return AcceptedContact( + remoteProfile: _contactRequestPrivate.profile, + remoteIdentity: _contactSuperIdentity, + remoteConversationRecordKey: + _contactRequestPrivate.chatRecordKey.toVeilid(), + localConversationRecordKey: localConversationRecordKey, + ); }); } on Exception catch (e) { log.debug('exception: $e', e); diff --git a/lib/contact_invitation/views/paste_invitation_dialog.dart b/lib/contact_invitation/views/paste_invitation_dialog.dart index 9cd9efc..b014fc2 100644 --- a/lib/contact_invitation/views/paste_invitation_dialog.dart +++ b/lib/contact_invitation/views/paste_invitation_dialog.dart @@ -112,8 +112,8 @@ class PasteInvitationDialogState extends State { constraints: const BoxConstraints(maxHeight: 200), child: TextField( enabled: !dialogState.isValidating, - onChanged: (text) async => - _onPasteChanged(text, validateInviteData), + autofocus: true, + onChanged: (text) => _onPasteChanged(text, validateInviteData), style: monoStyle, keyboardType: TextInputType.multiline, maxLines: null, @@ -129,14 +129,11 @@ class PasteInvitationDialogState extends State { } @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return InvitationDialog( - locator: widget._locator, - onValidationCancelled: onValidationCancelled, - onValidationSuccess: onValidationSuccess, - onValidationFailed: onValidationFailed, - inviteControlIsValid: inviteControlIsValid, - buildInviteControl: buildInviteControl); - } + Widget build(BuildContext context) => InvitationDialog( + locator: widget._locator, + onValidationCancelled: onValidationCancelled, + onValidationSuccess: onValidationSuccess, + onValidationFailed: onValidationFailed, + inviteControlIsValid: inviteControlIsValid, + buildInviteControl: buildInviteControl); } diff --git a/lib/contact_invitation/views/scan_invitation_dialog.dart b/lib/contact_invitation/views/scan_invitation_dialog.dart index fa8bba9..8fbdf5c 100644 --- a/lib/contact_invitation/views/scan_invitation_dialog.dart +++ b/lib/contact_invitation/views/scan_invitation_dialog.dart @@ -169,7 +169,7 @@ class ScanInvitationDialogState extends State { fit: BoxFit.contain, scanWindow: scanWindow, controller: cameraController, - errorBuilder: (context, error, child) => + errorBuilder: (context, error) => ScannerErrorWidget(error: error), onDetect: (c) { final barcode = c.barcodes.firstOrNull; @@ -242,6 +242,10 @@ class ScanInvitationDialogState extends State { return const Icon(Icons.camera_front); case CameraFacing.back: return const Icon(Icons.camera_rear); + case CameraFacing.external: + return const Icon(Icons.camera_alt); + case CameraFacing.unknown: + return const Icon(Icons.question_mark); } }, ), diff --git a/lib/contacts/views/contact_details_widget.dart b/lib/contacts/views/contact_details_widget.dart index 7b5416e..707dc1e 100644 --- a/lib/contacts/views/contact_details_widget.dart +++ b/lib/contacts/views/contact_details_widget.dart @@ -27,8 +27,7 @@ class ContactDetailsWidget extends StatefulWidget { final void Function(bool)? onModifiedState; } -class _ContactDetailsWidgetState extends State - with SingleTickerProviderStateMixin { +class _ContactDetailsWidgetState extends State { @override Widget build(BuildContext context) => SingleChildScrollView( child: EditContactForm( diff --git a/lib/contacts/views/contact_item_widget.dart b/lib/contacts/views/contact_item_widget.dart index 4614f27..e206570 100644 --- a/lib/contacts/views/contact_item_widget.dart +++ b/lib/contacts/views/contact_item_widget.dart @@ -23,7 +23,6 @@ class ContactItemWidget extends StatelessWidget { _onDelete = onDelete; @override - // ignore: prefer_expression_function_bodies Widget build( BuildContext context, ) { diff --git a/lib/contacts/views/contacts_browser.dart b/lib/contacts/views/contacts_browser.dart index 74cd0b5..84a2601 100644 --- a/lib/contacts/views/contacts_browser.dart +++ b/lib/contacts/views/contacts_browser.dart @@ -8,7 +8,6 @@ import 'package:searchable_listview/searchable_listview.dart'; import 'package:star_menu/star_menu.dart'; import 'package:veilid_support/veilid_support.dart'; -import '../../chat_list/chat_list.dart'; import '../../contact_invitation/contact_invitation.dart'; import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; @@ -45,6 +44,7 @@ class ContactsBrowserElement { class ContactsBrowser extends StatefulWidget { const ContactsBrowser( {required this.onContactSelected, + required this.onContactDeleted, required this.onStartChat, this.selectedContactRecordKey, super.key}); @@ -52,6 +52,7 @@ class ContactsBrowser extends StatefulWidget { State createState() => _ContactsBrowserState(); final Future Function(proto.Contact? contact) onContactSelected; + final Future Function(proto.Contact contact) onContactDeleted; final Future Function(proto.Contact contact) onStartChat; final TypedKey? selectedContactRecordKey; @@ -66,7 +67,10 @@ class ContactsBrowser extends StatefulWidget { 'onContactSelected', onContactSelected)) ..add( ObjectFlagProperty Function(proto.Contact contact)>.has( - 'onStartChat', onStartChat)); + 'onStartChat', onStartChat)) + ..add( + ObjectFlagProperty Function(proto.Contact contact)>.has( + 'onContactDeleted', onContactDeleted)); } } @@ -89,8 +93,6 @@ class _ContactsBrowserState extends State final menuParams = StarMenuParameters( shape: MenuShape.linear, centerOffset: const Offset(0, 64), - // backgroundParams: - // BackgroundParams(backgroundColor: theme.shadowColor.withAlpha(128)), boundaryBackground: BoundaryBackground( color: menuBackgroundColor, decoration: ShapeDecoration( @@ -145,7 +147,7 @@ class _ContactsBrowserState extends State return StarMenu( items: inviteMenuItems, - onItemTapped: (_index, controller) { + onItemTapped: (_, controller) { controller.closeMenu!(); }, controller: _invitationMenuController, @@ -162,16 +164,13 @@ class _ContactsBrowserState extends State Widget build(BuildContext context) { final theme = Theme.of(context); final scale = theme.extension()!; - //final scaleConfig = theme.extension()!; final cilState = context.watch().state; - //final cilBusy = cilState.busy; final contactInvitationRecordList = cilState.state.asData?.value.map((x) => x.value).toIList() ?? const IListConst([]); final ciState = context.watch().state; - //final ciBusy = ciState.busy; final contactList = ciState.state.asData?.value.map((x) => x.value).toIList(); @@ -201,8 +200,8 @@ class _ContactsBrowserState extends State contact.localConversationRecordKey.toVeilid(), disabled: false, onDoubleTap: _onStartChat, - onTap: _onSelectContact, - onDelete: _onDeleteContact) + onTap: onContactSelected, + onDelete: _onContactDeleted) .paddingLTRB(0, 4, 0, 0); case ContactsBrowserElementKind.invitation: final invitation = element.invitation!; @@ -261,7 +260,7 @@ class _ContactsBrowserState extends State ]); } - Future _onSelectContact(proto.Contact contact) async { + Future onContactSelected(proto.Contact contact) async { await widget.onContactSelected(contact); } @@ -269,20 +268,8 @@ class _ContactsBrowserState extends State await widget.onStartChat(contact); } - Future _onDeleteContact(proto.Contact contact) async { - final localConversationRecordKey = - contact.localConversationRecordKey.toVeilid(); - - final contactListCubit = context.read(); - final chatListCubit = context.read(); - - // Delete the contact itself - await contactListCubit.deleteContact( - localConversationRecordKey: localConversationRecordKey); - - // Remove any chats for this contact - await chatListCubit.deleteChat( - localConversationRecordKey: localConversationRecordKey); + Future _onContactDeleted(proto.Contact contact) async { + await widget.onContactDeleted(contact); } //////////////////////////////////////////////////////////////////////////// diff --git a/lib/contacts/views/contacts_page.dart b/lib/contacts/views/contacts_page.dart index 0f1731d..26a6f0d 100644 --- a/lib/contacts/views/contacts_page.dart +++ b/lib/contacts/views/contacts_page.dart @@ -103,6 +103,7 @@ class _ContactsPageState extends State { .toVeilid(), onContactSelected: _onContactSelected, onStartChat: _onChatStarted, + onContactDeleted: _onContactDeleted, ).paddingLTRB(4, 0, 4, 8)))), if (enableRight && enableLeft) Container( @@ -157,6 +158,40 @@ class _ContactsPageState extends State { } } + Future _onContactDeleted(proto.Contact contact) async { + if (contact == _selectedContact && _isModified) { + final ok = await showConfirmModal( + context: context, + title: translate('confirmation.discard_changes'), + text: translate('confirmation.are_you_sure_discard')); + if (!ok) { + return false; + } + } + setState(() { + _selectedContact = null; + _isModified = false; + }); + + if (mounted) { + final localConversationRecordKey = + contact.localConversationRecordKey.toVeilid(); + + final contactListCubit = context.read(); + final chatListCubit = context.read(); + + // Delete the contact itself + await contactListCubit.deleteContact( + localConversationRecordKey: localConversationRecordKey); + + // Remove any chats for this contact + await chatListCubit.deleteChat( + localConversationRecordKey: localConversationRecordKey); + } + + return true; + } + proto.Contact? _selectedContact; - bool _isModified = false; + var _isModified = false; } diff --git a/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart b/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart index 08a249f..3c00eba 100644 --- a/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart +++ b/lib/conversation/cubits/active_conversations_bloc_map_cubit.dart @@ -74,11 +74,11 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit _addDirectConversation( + void _addDirectConversation( {required TypedKey remoteIdentityPublicKey, required TypedKey localConversationRecordKey, - required TypedKey remoteConversationRecordKey}) async => - add(localConversationRecordKey, () async { + required TypedKey remoteConversationRecordKey}) => + add(localConversationRecordKey, () { // Conversation cubit the tracks the state between the local // and remote halves of a contact's relationship with this account final conversationCubit = ConversationCubit( @@ -129,11 +129,10 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit removeFromState(TypedKey key) => remove(key); + void removeFromState(TypedKey key) => remove(key); @override - Future updateState( - TypedKey key, proto.Chat? oldValue, proto.Chat newValue) async { + void updateState(TypedKey key, proto.Chat? oldValue, proto.Chat newValue) { switch (newValue.whichKind()) { case proto.Chat_Kind.notSet: throw StateError('unknown chat kind'); @@ -161,7 +160,7 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit _addConversationMessages(_SingleContactChatState state) async { - // xxx could use atomic update() function - await update(state.localConversationRecordKey, - onUpdate: (cubit) async => + void _addConversationMessages(_SingleContactChatState state) { + update(state.localConversationRecordKey, + onUpdate: (cubit) => cubit.updateRemoteMessagesRecordKey(state.remoteMessagesRecordKey), - onCreate: () async => SingleContactMessagesCubit( + onCreate: () => SingleContactMessagesCubit( accountInfo: _accountInfo, remoteIdentityPublicKey: state.remoteIdentityPublicKey, localConversationRecordKey: state.localConversationRecordKey, @@ -87,13 +84,11 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit removeFromState(TypedKey key) => remove(key); + void removeFromState(TypedKey key) => remove(key); @override - Future updateState( - TypedKey key, - AsyncValue? oldValue, - AsyncValue newValue) async { + void updateState(TypedKey key, AsyncValue? oldValue, + AsyncValue newValue) { final newState = _mapStateValue(newValue); if (oldValue != null) { final oldState = _mapStateValue(oldValue); @@ -102,14 +97,14 @@ class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit get props => [localConversation, remoteConversation]; + + @override + String toString() => 'ConversationState(' + 'localConversation: ${DynamicDebug.toDebug(localConversation)}, ' + 'remoteConversation: ${DynamicDebug.toDebug(remoteConversation)}' + ')'; } /// Represents the control channel between two contacts @@ -39,13 +45,11 @@ class ConversationCubit extends Cubit> { TypedKey? localConversationRecordKey, TypedKey? remoteConversationRecordKey}) : _accountInfo = accountInfo, - _localConversationRecordKey = localConversationRecordKey, _remoteIdentityPublicKey = remoteIdentityPublicKey, - _remoteConversationRecordKey = remoteConversationRecordKey, super(const AsyncValue.loading()) { _identityWriter = _accountInfo.identityWriter; - if (_localConversationRecordKey != null) { + if (localConversationRecordKey != null) { _initWait.add((_) async { await _setLocalConversation(() async { // Open local record key if it is specified @@ -54,7 +58,7 @@ class ConversationCubit extends Cubit> { final writer = _identityWriter; final record = await pool.openRecordWrite( - _localConversationRecordKey!, writer, + localConversationRecordKey, writer, debugName: 'ConversationCubit::LocalConversation', parent: accountInfo.accountRecordKey, crypto: crypto); @@ -64,17 +68,17 @@ class ConversationCubit extends Cubit> { }); } - if (_remoteConversationRecordKey != null) { + if (remoteConversationRecordKey != null) { _initWait.add((cancel) async { await _setRemoteConversation(() async { // Open remote record key if it is specified final pool = DHTRecordPool.instance; final crypto = await _cachedConversationCrypto(); - final record = await pool.openRecordRead(_remoteConversationRecordKey, + final record = await pool.openRecordRead(remoteConversationRecordKey, debugName: 'ConversationCubit::RemoteConversation', parent: - await pool.getParentRecordKey(_remoteConversationRecordKey) ?? + await pool.getParentRecordKey(remoteConversationRecordKey) ?? accountInfo.accountRecordKey, crypto: crypto); @@ -104,13 +108,11 @@ 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( + /// Returns the local conversation record key that was initialized + Future initLocalConversation( {required proto.Profile profile, - required FutureOr Function(DHTRecord) callback, TypedKey? existingConversationRecordKey}) async { - assert(_localConversationRecordKey == null, + assert(_localConversationCubit == null, 'must not have a local conversation yet'); final pool = DHTRecordPool.instance; @@ -138,11 +140,8 @@ class ConversationCubit extends Cubit> { schema: DHTSchema.smpl( oCnt: 0, members: [DHTSchemaMember(mKey: writer.key, mCnt: 1)])); } - final out = localConversationRecord - // ignore: prefer_expression_function_bodies - .deleteScope((localConversation) async { - // Make messages log - return _initLocalMessages( + await localConversationRecord.deleteScope((localConversation) async { + await _initLocalMessages( localConversationKey: localConversation.key, callback: (messages) async { // Create initial local conversation key contents @@ -158,36 +157,31 @@ class ConversationCubit extends Cubit> { 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)); - - return out; + // If success, save the new local conversation + // record key in this object + localConversation.ref(); + await _setLocalConversation(() async => localConversation); }); }); - // If success, save the new local conversation record key in this object - _localConversationRecordKey = localConversationRecord.key; - await _setLocalConversation(() async => localConversationRecord); - - return out; + return localConversationRecord.key; } /// Force refresh of conversation keys - Future refresh() async { - await _initWait(); + // Future refresh() async { + // await _initWait(); - final lcc = _localConversationCubit; - final rcc = _remoteConversationCubit; + // final lcc = _localConversationCubit; + // final rcc = _remoteConversationCubit; - if (lcc != null) { - await lcc.refreshDefault(); - } - if (rcc != null) { - await rcc.refreshDefault(); - } - } + // if (lcc != null) { + // await lcc.refreshDefault(); + // } + // if (rcc != null) { + // await rcc.refreshDefault(); + // } + // } /// Watch for account record changes and update the conversation void watchAccountChanges(Stream> accountStream, @@ -226,12 +220,6 @@ class ConversationCubit extends Cubit> { _incrementalState = ConversationState( localConversation: conv, remoteConversation: _incrementalState.remoteConversation); - // return loading still if state isn't complete - if (_localConversationRecordKey != null && - _incrementalState.localConversation == null) { - return const AsyncValue.loading(); - } - // local state is complete, all remote state is emitted incrementally return AsyncValue.data(_incrementalState); }, loading: AsyncValue.loading, @@ -246,12 +234,6 @@ class ConversationCubit extends Cubit> { _incrementalState = ConversationState( localConversation: _incrementalState.localConversation, remoteConversation: conv); - // return loading still if the local state isn't complete - if (_localConversationRecordKey != null && - _incrementalState.localConversation == null) { - return const AsyncValue.loading(); - } - // local state is complete, all remote state is emitted incrementally return AsyncValue.data(_incrementalState); }, loading: AsyncValue.loading, @@ -263,9 +245,12 @@ class ConversationCubit extends Cubit> { // Open local converation key Future _setLocalConversation(Future Function() open) async { assert(_localConversationCubit == null, - 'shoud not set local conversation twice'); + 'should not set local conversation twice'); _localConversationCubit = DefaultDHTRecordCubit( open: open, decodeState: proto.Conversation.fromBuffer); + + await _localConversationCubit!.ready(); + _localSubscription = _localConversationCubit!.stream.listen(_updateLocalConversationState); _updateLocalConversationState(_localConversationCubit!.state); @@ -274,9 +259,12 @@ class ConversationCubit extends Cubit> { // Open remote converation key Future _setRemoteConversation(Future Function() open) async { assert(_remoteConversationCubit == null, - 'shoud not set remote conversation twice'); + 'should not set remote conversation twice'); _remoteConversationCubit = DefaultDHTRecordCubit( open: open, decodeState: proto.Conversation.fromBuffer); + + await _remoteConversationCubit!.ready(); + _remoteSubscription = _remoteConversationCubit!.stream.listen(_updateRemoteConversationState); _updateRemoteConversationState(_remoteConversationCubit!.state); @@ -316,14 +304,12 @@ class ConversationCubit extends Cubit> { final AccountInfo _accountInfo; late final KeyPair _identityWriter; final TypedKey _remoteIdentityPublicKey; - TypedKey? _localConversationRecordKey; - final TypedKey? _remoteConversationRecordKey; DefaultDHTRecordCubit? _localConversationCubit; DefaultDHTRecordCubit? _remoteConversationCubit; StreamSubscription>? _localSubscription; StreamSubscription>? _remoteSubscription; StreamSubscription>? _accountSubscription; - ConversationState _incrementalState = const ConversationState( + var _incrementalState = const ConversationState( localConversation: null, remoteConversation: null); VeilidCrypto? _conversationCrypto; final WaitSet _initWait = WaitSet(); diff --git a/lib/tools/state_logger.dart b/lib/tools/state_logger.dart index a133346..8782662 100644 --- a/lib/tools/state_logger.dart +++ b/lib/tools/state_logger.dart @@ -17,6 +17,7 @@ const Map _blocChangeLogLevels = { 'PreferencesCubit': LogLevel.debug, 'ConversationCubit': LogLevel.debug, 'DefaultDHTRecordCubit': LogLevel.debug, + 'WaitingInvitationCubit': LogLevel.debug, }; const Map _blocCreateCloseLogLevels = {}; diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 2d40a21..beb7d0e 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -2,7 +2,8 @@ PODS: - file_saver (0.0.1): - FlutterMacOS - FlutterMacOS (1.0.0) - - mobile_scanner (6.0.2): + - mobile_scanner (7.0.0): + - Flutter - FlutterMacOS - package_info_plus (0.0.1): - FlutterMacOS @@ -33,7 +34,7 @@ PODS: DEPENDENCIES: - file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`) + - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) @@ -52,7 +53,7 @@ EXTERNAL SOURCES: FlutterMacOS: :path: Flutter/ephemeral mobile_scanner: - :path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos + :path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin package_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos pasteboard: @@ -79,7 +80,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: file_saver: e35bd97de451dde55ff8c38862ed7ad0f3418d0f FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - mobile_scanner: 0e365ed56cad24f28c0fd858ca04edefb40dfac3 + mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 package_info_plus: f0052d280d17aa382b932f399edf32507174e870 pasteboard: 278d8100149f940fb795d6b3a74f0720c890ecb7 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 diff --git a/packages/veilid_support/example/pubspec.lock b/packages/veilid_support/example/pubspec.lock index c40540e..9af9773 100644 --- a/packages/veilid_support/example/pubspec.lock +++ b/packages/veilid_support/example/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 + sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f url: "https://pub.dev" source: hosted - version: "80.0.0" + version: "82.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" + sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0" url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.4.5" args: dependency: transitive description: @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" async_tools: dependency: "direct dev" description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: bloc_advanced_tools - sha256: "7c7f294b425552c2d4831b01ad0d3e1f33f2bdf9acfb7b639caa072781d228cf" + sha256: dfb142569814952af8d93e7fe045972d847e29382471687db59913e253202f6e url: "https://pub.dev" source: hosted - version: "0.1.10" + version: "0.1.12" boolean_selector: dependency: transitive description: @@ -89,6 +89,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" clock: dependency: transitive description: @@ -117,10 +125,10 @@ packages: dependency: transitive description: name: coverage - sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43 + sha256: "802bd084fb82e55df091ec8ad1553a7331b61c08251eef19a508b6f3f3a9858d" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.13.1" crypto: dependency: transitive description: @@ -149,18 +157,18 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" fast_immutable_collections: dependency: transitive description: name: fast_immutable_collections - sha256: "95a69b9380483dff49ae2c12c9eb92e2b4e1aeff481a33c2a20883471771598a" + sha256: d1aa3d7788fab06cce7f303f4969c7a16a10c865e1bd2478291a8ebcbee084e5 url: "https://pub.dev" source: hosted - version: "11.0.3" + version: "11.0.4" ffi: dependency: transitive description: @@ -299,10 +307,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -323,10 +331,10 @@ packages: dependency: "direct dev" description: name: lint_hard - sha256: ffe7058cb49e021d244d67e650a63380445b56643c2849c6929e938246b99058 + sha256: "2073d4e83ac4e3f2b87cc615fff41abb5c2c5618e117edcd3d71f40f2186f4d5" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "6.1.1" logging: dependency: transitive description: @@ -411,10 +419,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.16" + version: "2.2.17" path_provider_foundation: dependency: transitive description: @@ -483,10 +491,10 @@ packages: dependency: transitive description: name: protobuf - sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + sha256: "579fe5557eae58e3adca2e999e38f02441d8aa908703854a9e0a0f47fa857731" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.1.0" pub_semver: dependency: transitive description: @@ -658,7 +666,7 @@ packages: path: "../../../../veilid/veilid-flutter" relative: true source: path - version: "0.4.4" + version: "0.4.6" veilid_support: dependency: "direct main" description: @@ -677,10 +685,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" watcher: dependency: transitive description: @@ -701,26 +709,26 @@ packages: dependency: transitive description: name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" webdriver: dependency: transitive description: name: webdriver - sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.1.0" webkit_inspection_protocol: dependency: transitive description: diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index 93cdcc4..6323692 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -613,7 +613,7 @@ class _DHTLogSpine { // xxx: Don't watch for local changes because this class already handles // xxx: notifying listeners and knows when it makes local changes _subscription ??= - await _spineRecord.listen(localChanges: true, _onSpineChanged); + await _spineRecord.listen(localChanges: false, _onSpineChanged); await _spineRecord.watch(subkeys: [ValueSubkeyRange.single(0)]); } on Exception { // If anything fails, try to cancel the watches diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart index e5fb513..3d396d2 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/default_dht_record_cubit.dart @@ -6,14 +6,14 @@ import '../../../veilid_support.dart'; class DefaultDHTRecordCubit extends DHTRecordCubit { DefaultDHTRecordCubit({ required super.open, - required T Function(List data) decodeState, + required T Function(Uint8List data) decodeState, }) : super( initialStateFunction: _makeInitialStateFunction(decodeState), stateFunction: _makeStateFunction(decodeState), watchFunction: _makeWatchFunction()); static InitialStateFunction _makeInitialStateFunction( - T Function(List data) decodeState) => + T Function(Uint8List data) decodeState) => (record) async { final initialData = await record.get(); if (initialData == null) { @@ -23,7 +23,7 @@ class DefaultDHTRecordCubit extends DHTRecordCubit { }; static StateFunction _makeStateFunction( - T Function(List data) decodeState) => + T Function(Uint8List data) decodeState) => (record, subkeys, updatedata) async { final defaultSubkey = record.subkeyOrDefault(-1); if (subkeys.containsSubkey(defaultSubkey)) { 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 cab5a77..eb56f7c 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 @@ -87,6 +87,10 @@ abstract class DHTRecordCubit extends Cubit> { await super.close(); } + Future ready() async { + await initWait(); + } + Future refresh(List subkeys) async { await initWait(); @@ -111,8 +115,6 @@ abstract class DHTRecordCubit extends Cubit> { } } - // DHTRecord get record => _record; - @protected final WaitSet initWait = WaitSet(); diff --git a/packages/veilid_support/lib/identity_support/identity_support.dart b/packages/veilid_support/lib/identity_support/identity_support.dart index 463be9a..68723bf 100644 --- a/packages/veilid_support/lib/identity_support/identity_support.dart +++ b/packages/veilid_support/lib/identity_support/identity_support.dart @@ -3,4 +3,5 @@ export 'exceptions.dart'; export 'identity.dart'; export 'identity_instance.dart'; export 'super_identity.dart'; +export 'super_identity_cubit.dart'; export 'writable_super_identity.dart'; diff --git a/packages/veilid_support/lib/identity_support/super_identity.dart b/packages/veilid_support/lib/identity_support/super_identity.dart index 5dc0e90..c8fd59d 100644 --- a/packages/veilid_support/lib/identity_support/super_identity.dart +++ b/packages/veilid_support/lib/identity_support/super_identity.dart @@ -64,7 +64,40 @@ sealed class SuperIdentity with _$SuperIdentity { const SuperIdentity._(); - /// Opens an existing super identity and validates it + /// Ensure a SuperIdentity is valid + Future validate({required TypedKey superRecordKey}) async { + // Validate current IdentityInstance + if (!await currentInstance.validateIdentityInstance( + superRecordKey: superRecordKey, superPublicKey: publicKey)) { + // Invalid current IdentityInstance signature(s) + throw IdentityException.invalid; + } + + // Validate deprecated IdentityInstances + for (final deprecatedInstance in deprecatedInstances) { + if (!await deprecatedInstance.validateIdentityInstance( + superRecordKey: superRecordKey, superPublicKey: publicKey)) { + // Invalid deprecated IdentityInstance signature(s) + throw IdentityException.invalid; + } + } + + // Validate SuperIdentity + final deprecatedInstancesSignatures = + deprecatedInstances.map((x) => x.signature).toList(); + if (!await _validateSuperIdentitySignature( + recordKey: recordKey, + currentInstanceSignature: currentInstance.signature, + deprecatedInstancesSignatures: deprecatedInstancesSignatures, + deprecatedSuperRecordKeys: deprecatedSuperRecordKeys, + publicKey: publicKey, + signature: signature)) { + // Invalid SuperIdentity signature + throw IdentityException.invalid; + } + } + + /// Opens an existing super identity, validates it, and returns it static Future open({required TypedKey superRecordKey}) async { final pool = DHTRecordPool.instance; @@ -75,37 +108,7 @@ sealed class SuperIdentity with _$SuperIdentity { final superIdentity = (await superRec.getJson(SuperIdentity.fromJson, refreshMode: DHTRecordRefreshMode.network))!; - // Validate current IdentityInstance - if (!await superIdentity.currentInstance.validateIdentityInstance( - superRecordKey: superRecordKey, - superPublicKey: superIdentity.publicKey)) { - // Invalid current IdentityInstance signature(s) - throw IdentityException.invalid; - } - - // Validate deprecated IdentityInstances - for (final deprecatedInstance in superIdentity.deprecatedInstances) { - if (!await deprecatedInstance.validateIdentityInstance( - superRecordKey: superRecordKey, - superPublicKey: superIdentity.publicKey)) { - // Invalid deprecated IdentityInstance signature(s) - throw IdentityException.invalid; - } - } - - // Validate SuperIdentity - final deprecatedInstancesSignatures = - superIdentity.deprecatedInstances.map((x) => x.signature).toList(); - if (!await _validateSuperIdentitySignature( - recordKey: superIdentity.recordKey, - currentInstanceSignature: superIdentity.currentInstance.signature, - deprecatedInstancesSignatures: deprecatedInstancesSignatures, - deprecatedSuperRecordKeys: superIdentity.deprecatedSuperRecordKeys, - publicKey: superIdentity.publicKey, - signature: superIdentity.signature)) { - // Invalid SuperIdentity signature - throw IdentityException.invalid; - } + await superIdentity.validate(superRecordKey: superRecordKey); return superIdentity; }); diff --git a/packages/veilid_support/lib/identity_support/super_identity_cubit.dart b/packages/veilid_support/lib/identity_support/super_identity_cubit.dart new file mode 100644 index 0000000..9de55ad --- /dev/null +++ b/packages/veilid_support/lib/identity_support/super_identity_cubit.dart @@ -0,0 +1,21 @@ +import 'package:async_tools/async_tools.dart'; + +import '../veilid_support.dart'; + +typedef SuperIdentityState = AsyncValue; + +class SuperIdentityCubit extends DefaultDHTRecordCubit { + SuperIdentityCubit({required TypedKey superRecordKey}) + : super( + open: () => _open(superRecordKey: superRecordKey), + decodeState: (buf) => jsonDecodeBytes(SuperIdentity.fromJson, buf)); + + static Future _open({required TypedKey superRecordKey}) async { + final pool = DHTRecordPool.instance; + + return pool.openRecordRead( + superRecordKey, + debugName: 'SuperIdentityCubit::_open::SuperIdentityRecord', + ); + } +} diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index d86402b..b4c7eef 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: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 + sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f url: "https://pub.dev" source: hosted - version: "80.0.0" + version: "82.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" + sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0" url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.4.5" args: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: "direct main" description: name: bloc_advanced_tools - sha256: "7c7f294b425552c2d4831b01ad0d3e1f33f2bdf9acfb7b639caa072781d228cf" + sha256: dfb142569814952af8d93e7fe045972d847e29382471687db59913e253202f6e url: "https://pub.dev" source: hosted - version: "0.1.10" + version: "0.1.12" boolean_selector: dependency: transitive description: @@ -161,6 +161,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" code_builder: dependency: transitive description: @@ -189,10 +197,10 @@ packages: dependency: transitive description: name: coverage - sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43 + sha256: "802bd084fb82e55df091ec8ad1553a7331b61c08251eef19a508b6f3f3a9858d" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.13.1" crypto: dependency: transitive description: @@ -205,10 +213,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac" + sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.0" equatable: dependency: "direct main" description: @@ -221,10 +229,10 @@ packages: dependency: "direct main" description: name: fast_immutable_collections - sha256: "95a69b9380483dff49ae2c12c9eb92e2b4e1aeff481a33c2a20883471771598a" + sha256: d1aa3d7788fab06cce7f303f4969c7a16a10c865e1bd2478291a8ebcbee084e5 url: "https://pub.dev" source: hosted - version: "11.0.3" + version: "11.0.4" ffi: dependency: transitive description: @@ -263,10 +271,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "7ed2ddaa47524976d5f2aa91432a79da36a76969edd84170777ac5bea82d797c" + sha256: "6022db4c7bfa626841b2a10f34dd1e1b68e8f8f9650db6112dcdeeca45ca793c" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.0.6" freezed_annotation: dependency: "direct main" description: @@ -311,10 +319,10 @@ packages: dependency: transitive description: name: http - sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" http_multi_server: dependency: transitive description: @@ -367,18 +375,18 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: "81f04dee10969f89f604e1249382d46b97a1ccad53872875369622b5bfc9e58a" + sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c url: "https://pub.dev" source: hosted - version: "6.9.4" + version: "6.9.5" lint_hard: dependency: "direct dev" description: name: lint_hard - sha256: ffe7058cb49e021d244d67e650a63380445b56643c2849c6929e938246b99058 + sha256: "2073d4e83ac4e3f2b87cc615fff41abb5c2c5618e117edcd3d71f40f2186f4d5" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "6.1.1" logging: dependency: transitive description: @@ -463,10 +471,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.16" + version: "2.2.17" path_provider_foundation: dependency: transitive description: @@ -527,10 +535,10 @@ packages: dependency: "direct main" description: name: protobuf - sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + sha256: "579fe5557eae58e3adca2e999e38f02441d8aa908703854a9e0a0f47fa857731" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.1.0" pub_semver: dependency: transitive description: @@ -684,26 +692,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e" + sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" url: "https://pub.dev" source: hosted - version: "1.25.15" + version: "1.26.2" test_api: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.6" test_core: dependency: transitive description: name: test_core - sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" + sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" url: "https://pub.dev" source: hosted - version: "0.6.8" + version: "0.6.11" timing: dependency: transitive description: @@ -734,15 +742,15 @@ packages: path: "../../../veilid/veilid-flutter" relative: true source: path - version: "0.4.4" + version: "0.4.6" vm_service: dependency: transitive description: name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + sha256: "6f82e9ee8e7339f5d8b699317f6f3afc17c80a68ebef1bc0d6f52a678c14b1e6" url: "https://pub.dev" source: hosted - version: "15.0.0" + version: "15.0.1" watcher: dependency: transitive description: @@ -763,18 +771,18 @@ packages: dependency: transitive description: name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" webkit_inspection_protocol: dependency: transitive description: diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index 65ba78d..8864fa6 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -9,7 +9,7 @@ environment: dependencies: async_tools: ^0.1.9 bloc: ^9.0.0 - bloc_advanced_tools: ^0.1.10 + bloc_advanced_tools: ^0.1.12 charcode: ^1.4.0 collection: ^1.19.1 convert: ^3.1.2 @@ -23,7 +23,7 @@ dependencies: path: ^1.9.1 path_provider: ^2.1.5 - protobuf: ^3.1.0 + protobuf: ^4.1.0 veilid: # veilid: ^0.0.1 path: ../../../veilid/veilid-flutter diff --git a/pubspec.lock b/pubspec.lock index cdec931..aa97499 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -157,10 +157,10 @@ packages: dependency: "direct main" description: name: bloc_advanced_tools - sha256: "7c7f294b425552c2d4831b01ad0d3e1f33f2bdf9acfb7b639caa072781d228cf" + sha256: dfb142569814952af8d93e7fe045972d847e29382471687db59913e253202f6e url: "https://pub.dev" source: hosted - version: "0.1.10" + version: "0.1.12" blurry_modal_progress_hud: dependency: "direct main" description: @@ -277,10 +277,10 @@ packages: dependency: transitive description: name: camera_android_camerax - sha256: ea7e40bd63afb8f55058e48ec529ce96562be9d08393f79631a06f781161fd0d + sha256: "9fb44e73e0fea3647a904dc26d38db24055e5b74fc68fd2b6d3abfa1bd20f536" url: "https://pub.dev" source: hosted - version: "0.6.16" + version: "0.6.17" camera_avfoundation: dependency: transitive description: @@ -437,10 +437,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac" + sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.0" diffutil_dart: dependency: transitive description: @@ -477,10 +477,10 @@ packages: dependency: "direct main" description: name: expansion_tile_group - sha256: "3be10b81d6d99d1213fe76a285993be0ea6092565ac100152deb6cdf9f5521dc" + sha256: "894c5088d94dda5d1ddde50463881935ff41b15850fe674605b9003d09716c8e" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" fast_immutable_collections: dependency: "direct main" description: @@ -554,18 +554,18 @@ packages: dependency: "direct main" description: name: flutter_chat_core - sha256: "529959634622e9df3b96a4a3764ecc61ec6f0dfa3258a52c139ae10a56ccad80" + sha256: "7875785bc4aa0b1dce56a76d2a8bd65841c130a3deb2c527878ebfdf8c54f971" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" flutter_chat_ui: dependency: "direct main" description: name: flutter_chat_ui - sha256: c63df9cd05fe86a3588b4e47f184fbb9e9c3b86153b8a97f3a789e6edb03d28e + sha256: "012aa0d9cc2898b8f89b48f66adb106de9547e466ba21ad54ccef25515f68dcc" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" flutter_form_builder: dependency: "direct main" description: @@ -631,10 +631,10 @@ packages: dependency: "direct main" description: name: flutter_sticky_header - sha256: "7f76d24d119424ca0c95c146b8627a457e8de8169b0d584f766c2c545db8f8be" + sha256: fb4fda6164ef3e5fc7ab73aba34aad253c17b7c6ecf738fa26f1a905b7d2d1e2 url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.8.0" flutter_svg: dependency: "direct main" description: @@ -724,10 +724,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3 + sha256: "0b1e06223bee260dee31a171fb1153e306907563a0b0225e8c1733211911429a" url: "https://pub.dev" source: hosted - version: "14.8.1" + version: "15.1.2" graphs: dependency: transitive description: @@ -788,10 +788,10 @@ packages: dependency: transitive description: name: idb_shim - sha256: d3dae2085f2dcc9d05b851331fddb66d57d3447ff800de9676b396795436e135 + sha256: "40e872276d79a1a97cc2c1ea0ecf046b8e34d788f16a8ea8f0da3e9b337d42da" url: "https://pub.dev" source: hosted - version: "2.6.5+1" + version: "2.6.6+1" image: dependency: "direct main" description: @@ -812,10 +812,10 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" io: dependency: transitive description: @@ -916,10 +916,10 @@ packages: dependency: "direct main" description: name: mobile_scanner - sha256: f536c5b8cadcf73d764bdce09c94744f06aa832264730f8971b21a60c5666826 + sha256: "72f06a071aa8b14acea3ab43ea7949eefe4a2469731ae210e006ba330a033a8c" url: "https://pub.dev" source: hosted - version: "6.0.10" + version: "7.0.0" nested: dependency: transitive description: @@ -964,10 +964,10 @@ packages: dependency: "direct main" description: name: pasteboard - sha256: "7bf733f3a00c7188ec1f2c6f0612854248b302cf91ef3611a2b7bb141c0f9d55" + sha256: "9ff73ada33f79a59ff91f6c01881fd4ed0a0031cfc4ae2d86c0384471525fca1" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.4.0" path: dependency: "direct main" description: @@ -1132,10 +1132,10 @@ packages: dependency: "direct main" description: name: protobuf - sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + sha256: "579fe5557eae58e3adca2e999e38f02441d8aa908703854a9e0a0f47fa857731" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.1.0" provider: dependency: "direct main" description: @@ -1317,18 +1317,18 @@ packages: dependency: "direct main" description: name: share_plus - sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da + sha256: b2961506569e28948d75ec346c28775bb111986bb69dc6a20754a457e3d97fa0 url: "https://pub.dev" source: hosted - version: "10.1.4" + version: "11.0.0" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b + sha256: "1032d392bc5d2095a77447a805aa3f804d2ae6a4d5eef5e6ebb3bd94c1bc19ef" url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "6.0.0" shared_preferences: dependency: "direct main" description: @@ -1603,10 +1603,10 @@ packages: dependency: transitive description: name: test_api - sha256: "6c7653816b1c938e121b69ff63a33c9dc68102b65a5fb0a5c0f9786256ed33e6" + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.5" + version: "0.7.6" timing: dependency: transitive description: @@ -1731,10 +1731,10 @@ packages: dependency: transitive description: name: value_layout_builder - sha256: c02511ea91ca5c643b514a33a38fa52536f74aa939ec367d02938b5ede6807fa + sha256: ab4b7d98bac8cefeb9713154d43ee0477490183f5aa23bb4ffa5103d9bbf6275 url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.5.0" vector_graphics: dependency: transitive description: @@ -1755,10 +1755,10 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" + sha256: "557a315b7d2a6dbb0aaaff84d857967ce6bdc96a63dc6ee2a57ce5a6ee5d3331" url: "https://pub.dev" source: hosted - version: "1.1.16" + version: "1.1.17" vector_math: dependency: transitive description: @@ -1887,4 +1887,4 @@ packages: version: "1.1.2" sdks: dart: ">=3.7.0 <4.0.0" - flutter: ">=3.29.0" + flutter: ">=3.32.0" diff --git a/pubspec.yaml b/pubspec.yaml index 73ec83a..519714b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ version: 0.4.7+20 environment: sdk: ">=3.2.0 <4.0.0" - flutter: ">=3.22.1" + flutter: ">=3.32.0" dependencies: accordion: ^2.6.0 @@ -21,7 +21,7 @@ dependencies: badges: ^3.1.2 basic_utils: ^5.8.2 bloc: ^9.0.0 - bloc_advanced_tools: ^0.1.10 + bloc_advanced_tools: ^0.1.12 blurry_modal_progress_hud: ^1.1.1 change_case: ^2.2.0 charcode: ^1.4.0 @@ -46,29 +46,29 @@ dependencies: flutter_native_splash: ^2.4.5 flutter_slidable: ^4.0.0 flutter_spinkit: ^5.2.1 - flutter_sticky_header: ^0.7.0 + flutter_sticky_header: ^0.8.0 flutter_svg: ^2.0.17 flutter_translate: ^4.1.0 flutter_zoom_drawer: ^3.2.0 form_builder_validators: ^11.1.2 freezed_annotation: ^3.0.0 - go_router: ^14.8.1 + go_router: ^15.1.2 image: ^4.5.3 intl: ^0.19.0 json_annotation: ^4.9.0 keyboard_avoider: ^0.2.0 loggy: ^2.0.3 meta: ^1.16.0 - mobile_scanner: ^6.0.7 + mobile_scanner: ^7.0.0 package_info_plus: ^8.3.0 - pasteboard: ^0.3.0 + pasteboard: ^0.4.0 path: ^1.9.1 path_provider: ^2.1.5 pdf: ^3.11.3 pinput: ^5.0.1 preload_page_view: ^0.2.0 printing: ^5.14.2 - protobuf: ^3.1.0 + protobuf: ^4.1.0 provider: ^6.1.2 qr_code_dart_scan: ^0.10.0 qr_flutter: ^4.1.0 @@ -81,7 +81,7 @@ dependencies: git: url: https://gitlab.com/veilid/Searchable-Listview.git ref: main - share_plus: ^10.1.4 + share_plus: ^11.0.0 shared_preferences: ^2.5.2 signal_strength_indicator: ^0.4.1 sliver_expandable: ^1.1.2 @@ -108,17 +108,18 @@ dependencies: xterm: ^4.0.0 zxing2: ^0.2.3 - # dependency_overrides: - # async_tools: - # path: ../dart_async_tools - # bloc_advanced_tools: - # path: ../bloc_advanced_tools - # searchable_listview: - # path: ../Searchable-Listview - # flutter_chat_core: - # path: ../flutter_chat_ui/packages/flutter_chat_core - # flutter_chat_ui: - # path: ../flutter_chat_ui/packages/flutter_chat_ui +dependency_overrides: + intl: ^0.20.2 # Until flutter_translate updates intl +# async_tools: +# path: ../dart_async_tools +# bloc_advanced_tools: +# path: ../bloc_advanced_tools +# searchable_listview: +# path: ../Searchable-Listview +# flutter_chat_core: +# path: ../flutter_chat_ui/packages/flutter_chat_core +# flutter_chat_ui: +# path: ../flutter_chat_ui/packages/flutter_chat_ui dev_dependencies: build_runner: ^2.4.15 From 3b1cb53b8afb912acdf4aabe19d9e57db2571f73 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 25 May 2025 23:40:52 -0400 Subject: [PATCH 250/270] Accessibility update --- CHANGELOG.md | 3 + assets/i18n/en.json | 1 + ios/Podfile.lock | 83 +------ ios/Runner.xcodeproj/project.pbxproj | 18 -- .../xcshareddata/xcschemes/Runner.xcscheme | 2 + .../views/edit_account_page.dart | 47 ++-- .../views/edit_profile_form.dart | 78 +++--- .../views/new_account_page.dart | 2 + .../views/show_recovery_key_page.dart | 7 +- lib/app.dart | 4 +- .../chat_builders/vc_composer_widget.dart | 1 + .../chat_builders/vc_text_message_widget.dart | 13 +- lib/chat/views/chat_component_widget.dart | 25 +- .../chat_single_contact_item_widget.dart | 23 +- .../views/contact_invitation_item_widget.dart | 4 +- lib/contacts/views/availability_widget.dart | 45 ++-- lib/contacts/views/contact_item_widget.dart | 26 +- lib/contacts/views/contacts_browser.dart | 31 +-- lib/contacts/views/contacts_page.dart | 10 +- lib/contacts/views/edit_contact_form.dart | 35 +-- lib/keyboard_shortcuts.dart | 136 +++++++++-- lib/layout/default_app_bar.dart | 16 +- lib/layout/home/drawer_menu/drawer_menu.dart | 31 +-- .../home/drawer_menu/menu_item_widget.dart | 4 +- lib/layout/home/home_account_ready.dart | 147 ++++++------ lib/layout/home/home_screen.dart | 59 ++--- .../views/notifications_preferences.dart | 226 +++++++----------- lib/router/views/router_shell.dart | 10 +- lib/settings/models/preferences.dart | 4 +- lib/settings/settings_page.dart | 76 +++--- lib/theme/models/contrast_generator.dart | 8 + lib/theme/models/scale_theme/scale_theme.dart | 8 + lib/theme/models/theme_preference.dart | 2 +- lib/theme/views/avatar_widget.dart | 74 ------ lib/theme/views/enter_password.dart | 3 +- .../brightness_preferences.dart | 28 +-- .../{ => preferences}/color_preferences.dart | 43 ++-- .../display_scale_preferences.dart | 109 +++++++++ lib/theme/views/preferences/preferences.dart | 4 + .../preferences/wallpaper_preferences.dart | 25 ++ lib/theme/views/responsive.dart | 4 + .../{ => styled_widgets}/styled_alert.dart | 3 +- .../views/styled_widgets/styled_avatar.dart | 77 ++++++ .../styled_button_box.dart} | 19 +- .../views/styled_widgets/styled_checkbox.dart | 63 +++++ .../{ => styled_widgets}/styled_dialog.dart | 2 +- .../views/styled_widgets/styled_dropdown.dart | 59 +++++ .../{ => styled_widgets}/styled_scaffold.dart | 2 +- .../styled_slide_tile.dart} | 50 ++-- .../views/styled_widgets/styled_slider.dart | 79 ++++++ .../views/styled_widgets/styled_widgets.dart | 8 + lib/theme/views/views.dart | 12 +- lib/theme/views/wallpaper_preferences.dart | 37 --- lib/veilid_processor/views/developer.dart | 8 +- .../views/signal_strength_meter.dart | 2 +- 55 files changed, 1089 insertions(+), 807 deletions(-) delete mode 100644 lib/theme/views/avatar_widget.dart rename lib/theme/views/{ => preferences}/brightness_preferences.dart (59%) rename lib/theme/views/{ => preferences}/color_preferences.dart (54%) create mode 100644 lib/theme/views/preferences/display_scale_preferences.dart create mode 100644 lib/theme/views/preferences/preferences.dart create mode 100644 lib/theme/views/preferences/wallpaper_preferences.dart rename lib/theme/views/{ => styled_widgets}/styled_alert.dart (99%) create mode 100644 lib/theme/views/styled_widgets/styled_avatar.dart rename lib/theme/views/{option_box.dart => styled_widgets/styled_button_box.dart} (75%) create mode 100644 lib/theme/views/styled_widgets/styled_checkbox.dart rename lib/theme/views/{ => styled_widgets}/styled_dialog.dart (98%) create mode 100644 lib/theme/views/styled_widgets/styled_dropdown.dart rename lib/theme/views/{ => styled_widgets}/styled_scaffold.dart (97%) rename lib/theme/views/{slider_tile.dart => styled_widgets/styled_slide_tile.dart} (75%) create mode 100644 lib/theme/views/styled_widgets/styled_slider.dart create mode 100644 lib/theme/views/styled_widgets/styled_widgets.dart delete mode 100644 lib/theme/views/wallpaper_preferences.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index a7b1930..8f7eefa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ - Deprecated accounts no longer crash application at startup - Simplify SingleContactMessagesCubit and MessageReconciliation - Update flutter_chat_ui to 2.0.0 +- Accessibility improvements + - Text scaling + - Keyboard shortcuts Ctrl + / Ctrl - to change font size ## v0.4.7 ## - *Community Contributions* diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 53bf461..50b0904 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -276,6 +276,7 @@ "titlebar": "Settings", "color_theme": "Color Theme", "brightness_mode": "Brightness Mode", + "display_scale": "Display Scale", "display_beta_warning": "Display beta warning on startup", "none": "None", "in_app": "In-app", diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 2528d2d..add7488 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -6,54 +6,9 @@ PODS: - Flutter (1.0.0) - flutter_native_splash (2.4.3): - Flutter - - GoogleDataTransport (10.1.0): - - nanopb (~> 3.30910.0) - - PromisesObjC (~> 2.4) - - GoogleMLKit/BarcodeScanning (7.0.0): - - GoogleMLKit/MLKitCore - - MLKitBarcodeScanning (~> 6.0.0) - - GoogleMLKit/MLKitCore (7.0.0): - - MLKitCommon (~> 12.0.0) - - GoogleToolboxForMac/Defines (4.2.1) - - GoogleToolboxForMac/Logger (4.2.1): - - GoogleToolboxForMac/Defines (= 4.2.1) - - "GoogleToolboxForMac/NSData+zlib (4.2.1)": - - GoogleToolboxForMac/Defines (= 4.2.1) - - GoogleUtilities/Environment (8.0.2): - - GoogleUtilities/Privacy - - GoogleUtilities/Logger (8.0.2): - - GoogleUtilities/Environment - - GoogleUtilities/Privacy - - GoogleUtilities/Privacy (8.0.2) - - GoogleUtilities/UserDefaults (8.0.2): - - GoogleUtilities/Logger - - GoogleUtilities/Privacy - - GTMSessionFetcher/Core (3.5.0) - - MLImage (1.0.0-beta6) - - MLKitBarcodeScanning (6.0.0): - - MLKitCommon (~> 12.0) - - MLKitVision (~> 8.0) - - MLKitCommon (12.0.0): - - GoogleDataTransport (~> 10.0) - - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) - - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" - - GoogleUtilities/Logger (~> 8.0) - - GoogleUtilities/UserDefaults (~> 8.0) - - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) - - MLKitVision (8.0.0): - - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) - - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" - - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) - - MLImage (= 1.0.0-beta6) - - MLKitCommon (~> 12.0) - - mobile_scanner (6.0.2): + - mobile_scanner (7.0.0): - Flutter - - GoogleMLKit/BarcodeScanning (~> 7.0.0) - - nanopb (3.30910.0): - - nanopb/decode (= 3.30910.0) - - nanopb/encode (= 3.30910.0) - - nanopb/decode (3.30910.0) - - nanopb/encode (3.30910.0) + - FlutterMacOS - package_info_plus (0.4.5): - Flutter - pasteboard (0.0.1): @@ -63,7 +18,6 @@ PODS: - FlutterMacOS - printing (1.0.0): - Flutter - - PromisesObjC (2.4.0) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -84,7 +38,7 @@ DEPENDENCIES: - file_saver (from `.symlinks/plugins/file_saver/ios`) - Flutter (from `Flutter`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - - mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`) + - mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - pasteboard (from `.symlinks/plugins/pasteboard/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) @@ -96,20 +50,6 @@ DEPENDENCIES: - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - veilid (from `.symlinks/plugins/veilid/ios`) -SPEC REPOS: - trunk: - - GoogleDataTransport - - GoogleMLKit - - GoogleToolboxForMac - - GoogleUtilities - - GTMSessionFetcher - - MLImage - - MLKitBarcodeScanning - - MLKitCommon - - MLKitVision - - nanopb - - PromisesObjC - EXTERNAL SOURCES: camera_avfoundation: :path: ".symlinks/plugins/camera_avfoundation/ios" @@ -120,7 +60,7 @@ EXTERNAL SOURCES: flutter_native_splash: :path: ".symlinks/plugins/flutter_native_splash/ios" mobile_scanner: - :path: ".symlinks/plugins/mobile_scanner/ios" + :path: ".symlinks/plugins/mobile_scanner/darwin" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" pasteboard: @@ -143,26 +83,15 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/veilid/ios" SPEC CHECKSUMS: - camera_avfoundation: 04b44aeb14070126c6529e5ab82cc7c9fca107cf + camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436 file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf - GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 - GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318 - GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 - GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d - GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 - MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56 - MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2 - MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d - MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e - mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036 - nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 + mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 printing: 54ff03f28fe9ba3aa93358afb80a8595a071dd07 - PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index e612191..3a96d3e 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -139,7 +139,6 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 02C44F9283ADDE9FAAA73512 /* [CP] Embed Pods Frameworks */, - 61BE8A90522682C17620991D /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -232,23 +231,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 61BE8A90522682C17620991D /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 3e31b44..a44fb7f 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> { title: translate('edit_account_page.remove_account_confirm'), child: Column(mainAxisSize: MainAxisSize.min, children: [ Text(translate('edit_account_page.remove_account_confirm_message')) - .paddingLTRB(24, 24, 24, 0), - Text(translate('confirmation.are_you_sure')).paddingAll(8), + .paddingLTRB(24.scaled(context), 24.scaled(context), + 24.scaled(context), 0), + Text(translate('confirmation.are_you_sure')) + .paddingAll(8.scaled(context)), Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ElevatedButton( onPressed: () { Navigator.of(context).pop(false); }, child: Row(mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.cancel, size: 16).paddingLTRB(0, 0, 4, 0), + Icon(Icons.cancel, size: 16.scaled(context)) + .paddingLTRB(0, 0, 4.scaled(context), 0), Text(translate('button.no')).paddingLTRB(0, 0, 4, 0) ])), ElevatedButton( @@ -89,10 +92,12 @@ class _EditAccountPageState extends WindowSetupState { Navigator.of(context).pop(true); }, child: Row(mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0), - Text(translate('button.yes')).paddingLTRB(0, 0, 4, 0) + Icon(Icons.check, size: 16.scaled(context)) + .paddingLTRB(0, 0, 4.scaled(context), 0), + Text(translate('button.yes')) + .paddingLTRB(0, 0, 4.scaled(context), 0) ])) - ]).paddingAll(24) + ]).paddingAll(24.scaled(context)) ])); if (confirmed != null && confirmed) { try { @@ -141,29 +146,36 @@ class _EditAccountPageState extends WindowSetupState { title: translate('edit_account_page.destroy_account_confirm'), child: Column(mainAxisSize: MainAxisSize.min, children: [ Text(translate('edit_account_page.destroy_account_confirm_message')) - .paddingLTRB(24, 24, 24, 0), + .paddingLTRB(24.scaled(context), 24.scaled(context), + 24.scaled(context), 0), Text(translate( 'edit_account_page.destroy_account_confirm_message_details')) - .paddingLTRB(24, 24, 24, 0), - Text(translate('confirmation.are_you_sure')).paddingAll(8), + .paddingLTRB(24.scaled(context), 24.scaled(context), + 24.scaled(context), 0), + Text(translate('confirmation.are_you_sure')) + .paddingAll(24.scaled(context)), Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ElevatedButton( onPressed: () { Navigator.of(context).pop(false); }, child: Row(mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.cancel, size: 16).paddingLTRB(0, 0, 4, 0), - Text(translate('button.no')).paddingLTRB(0, 0, 4, 0) + Icon(Icons.cancel, size: 16.scaled(context)) + .paddingLTRB(0, 0, 4.scaled(context), 0), + Text(translate('button.no')) + .paddingLTRB(0, 0, 4.scaled(context), 0) ])), ElevatedButton( onPressed: () { Navigator.of(context).pop(true); }, child: Row(mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0), - Text(translate('button.yes')).paddingLTRB(0, 0, 4, 0) + Icon(Icons.check, size: 16.scaled(context)) + .paddingLTRB(0, 0, 4.scaled(context), 0), + Text(translate('button.yes')) + .paddingLTRB(0, 0, 4.scaled(context), 0) ])) - ]).paddingAll(24) + ]).paddingAll(24.scaled(context)) ])); if (confirmed != null && confirmed) { try { @@ -250,10 +262,12 @@ class _EditAccountPageState extends WindowSetupState { return StyledScaffold( appBar: DefaultAppBar( + context: context, title: Text(translate('edit_account_page.titlebar')), leading: Navigator.canPop(context) ? IconButton( icon: const Icon(Icons.arrow_back), + iconSize: 24.scaled(context), onPressed: () { singleFuture((this, _kDoBackArrow), () async { if (_isModified) { @@ -277,6 +291,7 @@ class _EditAccountPageState extends WindowSetupState { const SignalStrengthMeterWidget(), IconButton( icon: const Icon(Icons.settings), + iconSize: 24.scaled(context), tooltip: translate('menu.settings_tooltip'), onPressed: () async { await GoRouterHelper(context).push('/settings'); @@ -285,14 +300,14 @@ class _EditAccountPageState extends WindowSetupState { body: SingleChildScrollView( child: Column(children: [ _editAccountForm(context).paddingLTRB(0, 0, 0, 32), - OptionBox( + StyledButtonBox( instructions: translate('edit_account_page.remove_account_description'), buttonIcon: Icons.person_remove_alt_1, buttonText: translate('edit_account_page.remove_account'), onClick: _onRemoveAccount, ), - OptionBox( + StyledButtonBox( instructions: translate('edit_account_page.destroy_account_description'), buttonIcon: Icons.person_off, diff --git a/lib/account_manager/views/edit_profile_form.dart b/lib/account_manager/views/edit_profile_form.dart index 4977dc7..114c7b8 100644 --- a/lib/account_manager/views/edit_profile_form.dart +++ b/lib/account_manager/views/edit_profile_form.dart @@ -53,16 +53,16 @@ class EditProfileForm extends StatefulWidget { ..add(DiagnosticsProperty('initialValue', initialValue)); } - static const String formFieldName = 'name'; - static const String formFieldPronouns = 'pronouns'; - static const String formFieldAbout = 'about'; - static const String formFieldAvailability = 'availability'; - static const String formFieldFreeMessage = 'free_message'; - static const String formFieldAwayMessage = 'away_message'; - static const String formFieldBusyMessage = 'busy_message'; - static const String formFieldAvatar = 'avatar'; - static const String formFieldAutoAway = 'auto_away'; - static const String formFieldAutoAwayTimeout = 'auto_away_timeout'; + static const formFieldName = 'name'; + static const formFieldPronouns = 'pronouns'; + static const formFieldAbout = 'about'; + static const formFieldAvailability = 'availability'; + static const formFieldFreeMessage = 'free_message'; + static const formFieldAwayMessage = 'away_message'; + static const formFieldBusyMessage = 'busy_message'; + static const formFieldAvatar = 'avatar'; + static const formFieldAutoAway = 'auto_away'; + static const formFieldAutoAwayTimeout = 'auto_away_timeout'; } class _EditProfileFormState extends State { @@ -98,6 +98,7 @@ class _EditProfileFormState extends State { name: EditProfileForm.formFieldAvailability, initialValue: initialValue, decoration: InputDecoration( + contentPadding: const EdgeInsets.all(8).scaled(context), floatingLabelBehavior: FloatingLabelBehavior.always, labelText: translate('account.form_availability'), hintText: translate('account.empty_busy_message')), @@ -110,7 +111,7 @@ class _EditProfileFormState extends State { Text(x == proto.Availability.AVAILABILITY_OFFLINE ? translate('availability.always_show_offline') : AvailabilityWidget.availabilityName(x)) - .paddingLTRB(8, 0, 0, 0), + .paddingLTRB(8.scaled(context), 0, 0, 0), ]))) .toList(), ); @@ -175,17 +176,8 @@ class _EditProfileFormState extends State { BuildContext context, ) { final theme = Theme.of(context); - final scale = theme.extension()!; - final scaleConfig = theme.extension()!; final textTheme = theme.textTheme; - late final Color border; - if (scaleConfig.useVisualIndicators && !scaleConfig.preferBorders) { - border = scale.primaryScale.elementBackground; - } else { - border = scale.primaryScale.border; - } - return FormBuilder( key: _formKey, autovalidateMode: AutovalidateMode.onUserInteraction, @@ -197,14 +189,9 @@ class _EditProfileFormState extends State { children: [ Row(children: [ const Spacer(), - AvatarWidget( + StyledAvatar( name: _currentValueName, - size: 128, - borderColor: border, - foregroundColor: scale.primaryScale.primaryText, - backgroundColor: scale.primaryScale.primary, - scaleConfig: scaleConfig, - textStyle: theme.textTheme.titleLarge!.copyWith(fontSize: 64), + size: 128.scaled(context), ).paddingLTRB(0, 0, 0, 16), const Spacer() ]), @@ -218,6 +205,7 @@ class _EditProfileFormState extends State { }); }, decoration: InputDecoration( + contentPadding: const EdgeInsets.all(8).scaled(context), floatingLabelBehavior: FloatingLabelBehavior.always, labelText: translate('account.form_name'), hintText: translate('account.empty_name')), @@ -233,6 +221,7 @@ class _EditProfileFormState extends State { initialValue: _savedValue.pronouns, maxLength: 64, decoration: InputDecoration( + contentPadding: const EdgeInsets.all(8).scaled(context), floatingLabelBehavior: FloatingLabelBehavior.always, labelText: translate('account.form_pronouns'), hintText: translate('account.empty_pronouns')), @@ -245,6 +234,7 @@ class _EditProfileFormState extends State { maxLines: 8, minLines: 1, decoration: InputDecoration( + contentPadding: const EdgeInsets.all(8).scaled(context), floatingLabelBehavior: FloatingLabelBehavior.always, labelText: translate('account.form_about'), hintText: translate('account.empty_about')), @@ -256,6 +246,7 @@ class _EditProfileFormState extends State { initialValue: _savedValue.freeMessage, maxLength: 128, decoration: InputDecoration( + contentPadding: const EdgeInsets.all(8).scaled(context), floatingLabelBehavior: FloatingLabelBehavior.always, labelText: translate('account.form_free_message'), hintText: translate('account.empty_free_message')), @@ -266,6 +257,7 @@ class _EditProfileFormState extends State { initialValue: _savedValue.awayMessage, maxLength: 128, decoration: InputDecoration( + contentPadding: const EdgeInsets.all(8).scaled(context), floatingLabelBehavior: FloatingLabelBehavior.always, labelText: translate('account.form_away_message'), hintText: translate('account.empty_away_message')), @@ -276,6 +268,7 @@ class _EditProfileFormState extends State { initialValue: _savedValue.busyMessage, maxLength: 128, decoration: InputDecoration( + contentPadding: const EdgeInsets.all(8).scaled(context), floatingLabelBehavior: FloatingLabelBehavior.always, labelText: translate('account.form_busy_message'), hintText: translate('account.empty_busy_message')), @@ -291,12 +284,13 @@ class _EditProfileFormState extends State { _currentValueAutoAway = v ?? false; }); }, - ).paddingLTRB(0, 0, 0, 16), + ).paddingLTRB(0, 0, 0, 16.scaled(context)), FormBuilderTextField( name: EditProfileForm.formFieldAutoAwayTimeout, enabled: _currentValueAutoAway, initialValue: _savedValue.autoAwayTimeout.toString(), decoration: InputDecoration( + contentPadding: const EdgeInsets.all(8).scaled(context), labelText: translate('account.form_auto_away_timeout'), ), validator: FormBuilderValidators.positiveNumber(), @@ -306,7 +300,7 @@ class _EditProfileFormState extends State { const Spacer(), Text(widget.instructions).toCenter().flexible(flex: 6), const Spacer(), - ]).paddingSymmetric(vertical: 16), + ]).paddingSymmetric(vertical: 16.scaled(context)), Row(children: [ const Spacer(), Builder(builder: (context) { @@ -319,17 +313,19 @@ class _EditProfileFormState extends State { false; return ElevatedButton( - onPressed: (networkReady && _isModified) ? _doSubmit : null, - child: Row(mainAxisSize: MainAxisSize.min, children: [ - Icon(networkReady ? Icons.check : Icons.hourglass_empty, - size: 16) - .paddingLTRB(0, 0, 4, 0), - Text(networkReady - ? widget.submitText - : widget.submitDisabledText) - .paddingLTRB(0, 0, 4, 0) - ]), - ); + onPressed: (networkReady && _isModified) ? _doSubmit : null, + child: Padding( + padding: EdgeInsetsGeometry.all(4.scaled(context)), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Icon(networkReady ? Icons.check : Icons.hourglass_empty, + size: 16.scaled(context)) + .paddingLTRB(0, 0, 4.scaled(context), 0), + Text(networkReady + ? widget.submitText + : widget.submitDisabledText) + .paddingLTRB(0, 0, 4.scaled(context), 0) + ]), + )); }), const Spacer() ]) @@ -363,5 +359,5 @@ class _EditProfileFormState extends State { late AccountSpec _savedValue; late bool _currentValueAutoAway; late String _currentValueName; - bool _isModified = false; + var _isModified = false; } diff --git a/lib/account_manager/views/new_account_page.dart b/lib/account_manager/views/new_account_page.dart index 07034df..5012527 100644 --- a/lib/account_manager/views/new_account_page.dart +++ b/lib/account_manager/views/new_account_page.dart @@ -94,6 +94,7 @@ class _NewAccountPageState extends WindowSetupState { return StyledScaffold( appBar: DefaultAppBar( + context: context, title: Text(translate('new_account_page.titlebar')), leading: GoRouterHelper(context).canPop() ? IconButton( @@ -111,6 +112,7 @@ class _NewAccountPageState extends WindowSetupState { const SignalStrengthMeterWidget(), IconButton( icon: const Icon(Icons.settings), + iconSize: 24.scaled(context), tooltip: translate('menu.settings_tooltip'), onPressed: () async { await GoRouterHelper(context).push('/settings'); diff --git a/lib/account_manager/views/show_recovery_key_page.dart b/lib/account_manager/views/show_recovery_key_page.dart index 7c971e0..5423543 100644 --- a/lib/account_manager/views/show_recovery_key_page.dart +++ b/lib/account_manager/views/show_recovery_key_page.dart @@ -164,6 +164,7 @@ class _ShowRecoveryKeyPageState extends WindowSetupState { return StyledScaffold( appBar: DefaultAppBar( + context: context, title: Text(translate('show_recovery_key_page.titlebar')), actions: [ const SignalStrengthMeterWidget(), @@ -193,7 +194,7 @@ class _ShowRecoveryKeyPageState extends WindowSetupState { textAlign: TextAlign.center, translate('show_recovery_key_page.instructions_options')) .paddingLTRB(12, 0, 12, 24), - OptionBox( + StyledButtonBox( instructions: translate('show_recovery_key_page.instructions_print'), buttonIcon: Icons.print, @@ -209,7 +210,7 @@ class _ShowRecoveryKeyPageState extends WindowSetupState { _codeHandled = true; }); }), - OptionBox( + StyledButtonBox( instructions: translate('show_recovery_key_page.instructions_view'), buttonIcon: Icons.edit_document, @@ -229,7 +230,7 @@ class _ShowRecoveryKeyPageState extends WindowSetupState { _codeHandled = true; }); }), - OptionBox( + StyledButtonBox( instructions: translate('show_recovery_key_page.instructions_share'), buttonIcon: Icons.ios_share, diff --git a/lib/app.dart b/lib/app.dart index 802b0d7..5f4d6dc 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -31,7 +31,7 @@ class VeilidChatApp extends StatelessWidget { super.key, }); - static const String name = 'VeilidChat'; + static const name = 'VeilidChat'; final ThemeData initialThemeData; @@ -125,7 +125,7 @@ class VeilidChatApp extends StatelessWidget { @override Widget build(BuildContext context) => FutureProvider( initialData: null, - create: (context) async => VeilidChatGlobalInit.initialize(), + create: (context) => VeilidChatGlobalInit.initialize(), builder: (context, __) { final globalInit = context.watch(); if (globalInit == null) { diff --git a/lib/chat/views/chat_builders/vc_composer_widget.dart b/lib/chat/views/chat_builders/vc_composer_widget.dart index b3eb1e5..f470e9b 100644 --- a/lib/chat/views/chat_builders/vc_composer_widget.dart +++ b/lib/chat/views/chat_builders/vc_composer_widget.dart @@ -355,6 +355,7 @@ class _VcComposerState extends State { borderRadius: BorderRadius.all(Radius.circular( 8 * config.borderRadiusScale))), hintText: widget.hintText, + hintMaxLines: 1, hintStyle: chatTheme.typography.bodyMedium.copyWith( color: widget.hintColor ?? chatTheme.colors.onSurface diff --git a/lib/chat/views/chat_builders/vc_text_message_widget.dart b/lib/chat/views/chat_builders/vc_text_message_widget.dart index fc1fe80..52235f6 100644 --- a/lib/chat/views/chat_builders/vc_text_message_widget.dart +++ b/lib/chat/views/chat_builders/vc_text_message_widget.dart @@ -12,7 +12,7 @@ class VcTextMessageWidget extends StatelessWidget { const VcTextMessageWidget({ required this.message, required this.index, - this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + this.padding, this.borderRadius, this.onlyEmojiFontSize, this.sentBackgroundColor, @@ -72,10 +72,6 @@ class VcTextMessageWidget extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); final scaleTheme = theme.extension()!; - final config = scaleTheme.config; - final scheme = scaleTheme.scheme; - final scale = scaleTheme.scheme.scale(ScaleKind.primary); - final textTheme = theme.textTheme; final scaleChatTheme = scaleTheme.chatTheme(); final chatTheme = scaleChatTheme.chatTheme; @@ -243,15 +239,16 @@ class TimeAndStatus extends StatelessWidget { if (showStatus && status != null) if (status == MessageStatus.sending) SizedBox( - width: 6, - height: 6, + width: 6.scaled(context), + height: 6.scaled(context), child: CircularProgressIndicator( color: textStyle?.color, strokeWidth: 2, ), ) else - Icon(getIconForStatus(status!), color: textStyle?.color, size: 12), + Icon(getIconForStatus(status!), + color: textStyle?.color, size: 12.scaled(context)), ], ); } diff --git a/lib/chat/views/chat_component_widget.dart b/lib/chat/views/chat_component_widget.dart index 7d90d89..8106a48 100644 --- a/lib/chat/views/chat_component_widget.dart +++ b/lib/chat/views/chat_component_widget.dart @@ -132,17 +132,9 @@ class _ChatComponentWidgetState extends State { final scale = scaleTheme.scheme.scale(ScaleKind.primary); final textTheme = theme.textTheme; final scaleChatTheme = scaleTheme.chatTheme(); - // final errorChatTheme = chatTheme.copyWith(color:) - // ..inputTextColor = scaleScheme.errorScale.primary - // ..sendButtonIcon = Image.asset( - // 'assets/icon-send.png', - // color: scaleScheme.errorScale.primary, - // package: 'flutter_chat_ui', - // )) - // .commit(); // Get the enclosing chat component cubit that contains our state - // (created by ChatComponentWidget.builder()) + // (created by ChatComponentWidget.singleContact()) final chatComponentCubit = context.watch(); final chatComponentState = chatComponentCubit.state; @@ -273,14 +265,19 @@ class _ChatComponentWidgetState extends State { // Text message builder textMessageBuilder: (context, message, index) => VcTextMessageWidget( - message: message, - index: index, - // showTime: true, - // showStatus: true, - ), + message: message, + index: index, + padding: const EdgeInsets.symmetric( + vertical: 12, horizontal: 16) + .scaled(context) + // showTime: true, + // showStatus: true, + ), // Composer builder composerBuilder: (ctx) => VcComposerWidget( autofocus: true, + padding: const EdgeInsets.all(4).scaled(context), + gap: 8.scaled(context), focusNode: _focusNode, textInputAction: isAnyMobile ? TextInputAction.newline 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 5191fdd..d2594c5 100644 --- a/lib/chat_list/views/chat_single_contact_item_widget.dart +++ b/lib/chat_list/views/chat_single_contact_item_widget.dart @@ -24,11 +24,9 @@ class ChatSingleContactItemWidget extends StatelessWidget { final bool _disabled; @override - // ignore: prefer_expression_function_bodies Widget build( BuildContext context, ) { - final theme = Theme.of(context); final scaleTheme = Theme.of(context).extension()!; final activeChatCubit = context.watch(); @@ -48,23 +46,12 @@ class ChatSingleContactItemWidget extends StatelessWidget { selected: selected, ); - final avatar = AvatarWidget( + final avatar = StyledAvatar( name: name, - size: 32, - borderColor: scaleTheme.config.useVisualIndicators - ? scaleTheme.scheme.primaryScale.primaryText - : scaleTheme.scheme.primaryScale.subtleBorder, - foregroundColor: _disabled - ? scaleTheme.scheme.grayScale.primaryText - : scaleTheme.scheme.primaryScale.primaryText, - backgroundColor: _disabled - ? scaleTheme.scheme.grayScale.primary - : scaleTheme.scheme.primaryScale.primary, - scaleConfig: scaleTheme.config, - textStyle: theme.textTheme.titleLarge!, + size: 32.scaled(context), ); - return SliderTile( + return StyledSlideTile( key: ValueKey(_localConversationRecordKey), disabled: _disabled, selected: selected, @@ -75,14 +62,14 @@ class ChatSingleContactItemWidget extends StatelessWidget { trailing: AvailabilityWidget( availability: availability, color: scaleTileTheme.textColor, - ).fit(fit: BoxFit.scaleDown), + ).fit(fit: BoxFit.fill), onTap: () { singleFuture(activeChatCubit, () async { activeChatCubit.setActiveChat(_localConversationRecordKey); }); }, endActions: [ - SliderTileAction( + SlideTileAction( //icon: Icons.delete, label: translate('button.delete'), actionScale: ScaleKind.tertiary, diff --git a/lib/contact_invitation/views/contact_invitation_item_widget.dart b/lib/contact_invitation/views/contact_invitation_item_widget.dart index 544e5db..b86a833 100644 --- a/lib/contact_invitation/views/contact_invitation_item_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_item_widget.dart @@ -44,7 +44,7 @@ class ContactInvitationItemWidget extends StatelessWidget { title = contactInvitationRecord.message; } - return SliderTile( + return StyledSlideTile( key: ObjectKey(contactInvitationRecord), disabled: tileDisabled, selected: selected, @@ -67,7 +67,7 @@ class ContactInvitationItemWidget extends StatelessWidget { ))); }, endActions: [ - SliderTileAction( + SlideTileAction( // icon: Icons.delete, label: translate('button.delete'), actionScale: ScaleKind.tertiary, diff --git a/lib/contacts/views/availability_widget.dart b/lib/contacts/views/availability_widget.dart index 75ef0a8..55fac39 100644 --- a/lib/contacts/views/availability_widget.dart +++ b/lib/contacts/views/availability_widget.dart @@ -1,37 +1,34 @@ -import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_translate/flutter_translate.dart'; import '../../proto/proto.dart' as proto; -import '../../theme/theme.dart'; class AvailabilityWidget extends StatelessWidget { const AvailabilityWidget( {required this.availability, required this.color, this.vertical = true, - this.size = 32, super.key}); - static Widget availabilityIcon(proto.Availability availability, Color color, - {double size = 24}) { + static Widget availabilityIcon( + proto.Availability availability, + Color color, + ) { late final Widget icon; switch (availability) { case proto.Availability.AVAILABILITY_AWAY: icon = SvgPicture.asset('assets/images/toilet.svg', - width: size, - height: size, colorFilter: ColorFilter.mode(color, BlendMode.srcATop)); case proto.Availability.AVAILABILITY_BUSY: - icon = Icon(Icons.event_busy, size: size); + icon = const Icon(Icons.event_busy, applyTextScaling: true); case proto.Availability.AVAILABILITY_FREE: - icon = Icon(Icons.event_available, size: size); + icon = const Icon(Icons.event_available, applyTextScaling: true); case proto.Availability.AVAILABILITY_OFFLINE: - icon = Icon(Icons.cloud_off, size: size); + icon = const Icon(Icons.cloud_off, applyTextScaling: true); case proto.Availability.AVAILABILITY_UNSPECIFIED: - icon = Icon(Icons.question_mark, size: size); + icon = const Icon(Icons.question_mark, applyTextScaling: true); } return icon; } @@ -59,23 +56,17 @@ class AvailabilityWidget extends StatelessWidget { final textTheme = theme.textTheme; final name = availabilityName(availability); - final icon = availabilityIcon(availability, color, size: size * 2 / 3); + final icon = availabilityIcon(availability, color); return vertical - ? ConstrainedBox( - constraints: BoxConstraints.tightFor(width: size), - child: Column(mainAxisSize: MainAxisSize.min, children: [ - icon, - Text(name, style: textTheme.labelSmall!.copyWith(color: color)) - .fit(fit: BoxFit.scaleDown) - ])) - : ConstrainedBox( - constraints: BoxConstraints.tightFor(height: size), - child: Row(mainAxisSize: MainAxisSize.min, children: [ - icon, - Text(name, style: textTheme.labelLarge!.copyWith(color: color)) - .paddingLTRB(size / 4, 0, 0, 0) - ])); + ? Column(mainAxisSize: MainAxisSize.min, children: [ + icon, + Text(name, style: textTheme.labelSmall!.copyWith(color: color)) + ]) + : Row(mainAxisSize: MainAxisSize.min, children: [ + icon, + Text(' $name', style: textTheme.labelLarge!.copyWith(color: color)) + ]); } //////////////////////////////////////////////////////////////////////////// @@ -83,7 +74,6 @@ class AvailabilityWidget extends StatelessWidget { final proto.Availability availability; final Color color; final bool vertical; - final double size; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -92,7 +82,6 @@ class AvailabilityWidget extends StatelessWidget { ..add( DiagnosticsProperty('availability', availability)) ..add(DiagnosticsProperty('vertical', vertical)) - ..add(DoubleProperty('size', size)) ..add(ColorProperty('color', color)); } } diff --git a/lib/contacts/views/contact_item_widget.dart b/lib/contacts/views/contact_item_widget.dart index e206570..9a76be5 100644 --- a/lib/contacts/views/contact_item_widget.dart +++ b/lib/contacts/views/contact_item_widget.dart @@ -26,30 +26,16 @@ class ContactItemWidget extends StatelessWidget { Widget build( BuildContext context, ) { - final theme = Theme.of(context); - final scale = theme.extension()!; - final scaleConfig = theme.extension()!; - final name = _contact.nameOrNickname; final title = _contact.displayName; final subtitle = _contact.profile.status; - final avatar = AvatarWidget( + final avatar = StyledAvatar( name: name, - size: 34, - borderColor: _disabled - ? scale.grayScale.primaryText - : scale.primaryScale.subtleBorder, - foregroundColor: _disabled - ? scale.grayScale.primaryText - : scale.primaryScale.primaryText, - backgroundColor: - _disabled ? scale.grayScale.primary : scale.primaryScale.primary, - scaleConfig: scaleConfig, - textStyle: theme.textTheme.titleLarge!, + size: 34.scaled(context), ); - return SliderTile( + return StyledSlideTile( key: ObjectKey(_contact), disabled: _disabled, selected: _selected, @@ -69,7 +55,7 @@ class ContactItemWidget extends StatelessWidget { }), startActions: [ if (_onDoubleTap != null) - SliderTileAction( + SlideTileAction( //icon: Icons.edit, label: translate('button.chat'), actionScale: ScaleKind.secondary, @@ -81,7 +67,7 @@ class ContactItemWidget extends StatelessWidget { ], endActions: [ if (_onTap != null) - SliderTileAction( + SlideTileAction( //icon: Icons.edit, label: translate('button.edit'), actionScale: ScaleKind.secondary, @@ -91,7 +77,7 @@ class ContactItemWidget extends StatelessWidget { }), ), if (_onDelete != null) - SliderTileAction( + SlideTileAction( //icon: Icons.delete, label: translate('button.delete'), actionScale: ScaleKind.tertiary, diff --git a/lib/contacts/views/contacts_browser.dart b/lib/contacts/views/contacts_browser.dart index 84a2601..0504a7a 100644 --- a/lib/contacts/views/contacts_browser.dart +++ b/lib/contacts/views/contacts_browser.dart @@ -92,7 +92,7 @@ class _ContactsBrowserState extends State final menuParams = StarMenuParameters( shape: MenuShape.linear, - centerOffset: const Offset(0, 64), + centerOffset: Offset(0, 64.scaled(context)), boundaryBackground: BoundaryBackground( color: menuBackgroundColor, decoration: ShapeDecoration( @@ -113,13 +113,14 @@ class _ContactsBrowserState extends State onPressed: onPressed, icon: Icon( iconData, - size: 32, - ).paddingSTEB(0, 8, 0, 8), + size: 32.scaled(context), + ).paddingSTEB(0, 8.scaled(context), 0, 8.scaled(context)), label: Text( text, + textScaler: MediaQuery.of(context).textScaler, maxLines: 2, textAlign: TextAlign.center, - ).paddingSTEB(0, 8, 0, 8)); + ).paddingSTEB(0, 8.scaled(context), 0, 8.scaled(context))); final inviteMenuItems = [ makeMenuButton( @@ -135,14 +136,14 @@ class _ContactsBrowserState extends State onPressed: () async { _invitationMenuController.closeMenu!(); await ScanInvitationDialog.show(context); - }).paddingLTRB(0, 0, 0, 8), + }).paddingLTRB(0, 0, 0, 8.scaled(context)), makeMenuButton( iconData: Icons.contact_page, text: translate('add_contact_sheet.create_invite'), onPressed: () async { _invitationMenuController.closeMenu!(); await CreateInvitationDialog.show(context); - }).paddingLTRB(0, 0, 0, 8), + }).paddingLTRB(0, 0, 0, 8.scaled(context)), ]; return StarMenu( @@ -154,7 +155,7 @@ class _ContactsBrowserState extends State params: menuParams, child: IconButton( onPressed: () {}, - iconSize: 24, + iconSize: 24.scaled(context), icon: Icon(Icons.person_add, color: menuIconColor), tooltip: translate('add_contact_sheet.add_contact')), ); @@ -202,13 +203,13 @@ class _ContactsBrowserState extends State onDoubleTap: _onStartChat, onTap: onContactSelected, onDelete: _onContactDeleted) - .paddingLTRB(0, 4, 0, 0); + .paddingLTRB(0, 4.scaled(context), 0, 0); case ContactsBrowserElementKind.invitation: final invitation = element.invitation!; return ContactInvitationItemWidget( contactInvitationRecord: invitation, disabled: false) - .paddingLTRB(0, 4, 0, 0); + .paddingLTRB(0, 4.scaled(context), 0, 0); } }, filter: (value) { @@ -242,9 +243,11 @@ class _ContactsBrowserState extends State } return filtered; }, - searchFieldHeight: 40, - listViewPadding: const EdgeInsets.fromLTRB(4, 0, 4, 4), - searchFieldPadding: const EdgeInsets.fromLTRB(4, 8, 4, 4), + searchFieldHeight: 40.scaled(context), + listViewPadding: + const EdgeInsets.fromLTRB(4, 0, 4, 4).scaled(context), + searchFieldPadding: + const EdgeInsets.fromLTRB(4, 8, 4, 4).scaled(context), emptyWidget: contactList == null ? waitingPage( text: translate('contact_list.loading_contacts')) @@ -254,8 +257,8 @@ class _ContactsBrowserState extends State searchFieldEnabled: contactList != null, inputDecoration: InputDecoration(labelText: translate('contact_list.search')), - secondaryWidget: - buildInvitationButton(context).paddingLTRB(4, 0, 0, 0)) + secondaryWidget: buildInvitationButton(context) + .paddingLTRB(4.scaled(context), 0, 0, 0)) .expanded() ]); } diff --git a/lib/contacts/views/contacts_page.dart b/lib/contacts/views/contacts_page.dart index 26a6f0d..c984b57 100644 --- a/lib/contacts/views/contacts_page.dart +++ b/lib/contacts/views/contacts_page.dart @@ -40,6 +40,7 @@ class _ContactsPageState extends State { return StyledScaffold( appBar: DefaultAppBar( + context: context, title: Text( !enableSplit && enableRight ? translate('contacts_dialog.edit_contact') @@ -47,6 +48,7 @@ class _ContactsPageState extends State { ), leading: IconButton( icon: const Icon(Icons.arrow_back), + iconSize: 24.scaled(context), onPressed: () { singleFuture((this, _kDoBackArrow), () async { final confirmed = await _onContactSelected(null); @@ -65,21 +67,21 @@ class _ContactsPageState extends State { if (_selectedContact != null) IconButton( icon: const Icon(Icons.chat_bubble), - iconSize: 24, + iconSize: 24.scaled(context), color: appBarTheme.iconColor, tooltip: translate('contacts_dialog.new_chat'), onPressed: () async { await _onChatStarted(_selectedContact!); - }).paddingLTRB(8, 0, 8, 0), + }), if (enableSplit && _selectedContact != null) IconButton( icon: const Icon(Icons.close), - iconSize: 24, + iconSize: 24.scaled(context), color: appBarTheme.iconColor, tooltip: translate('contacts_dialog.close_contact'), onPressed: () async { await _onContactSelected(null); - }).paddingLTRB(8, 0, 8, 0), + }), ]), body: LayoutBuilder(builder: (context, constraint) { final maxWidth = constraint.maxWidth; diff --git a/lib/contacts/views/edit_contact_form.dart b/lib/contacts/views/edit_contact_form.dart index 514f019..7ab6019 100644 --- a/lib/contacts/views/edit_contact_form.dart +++ b/lib/contacts/views/edit_contact_form.dart @@ -92,16 +92,8 @@ class _EditContactFormState extends State { Widget _editContactForm(BuildContext context) { final theme = Theme.of(context); final scale = theme.extension()!; - final scaleConfig = theme.extension()!; final textTheme = theme.textTheme; - late final Color border; - if (scaleConfig.useVisualIndicators && !scaleConfig.preferBorders) { - border = scale.primaryScale.elementBackground; - } else { - border = scale.primaryScale.subtleBorder; - } - return FormBuilder( key: _formKey, autovalidateMode: AutovalidateMode.onUserInteraction, @@ -116,18 +108,12 @@ class _EditContactFormState extends State { children: [ Row(children: [ const Spacer(), - AvatarWidget( - name: _currentValueNickname.isNotEmpty - ? _currentValueNickname - : widget.contact.profile.name, - size: 128, - borderColor: border, - foregroundColor: scale.primaryScale.primaryText, - backgroundColor: scale.primaryScale.primary, - scaleConfig: scaleConfig, - textStyle: theme.textTheme.titleLarge! - .copyWith(fontSize: 64), - ).paddingLTRB(0, 0, 0, 16), + StyledAvatar( + name: _currentValueNickname.isNotEmpty + ? _currentValueNickname + : widget.contact.profile.name, + size: 128) + .paddingLTRB(0, 0, 0, 16), const Spacer() ]), SelectableText(widget.contact.profile.name, @@ -211,10 +197,11 @@ class _EditContactFormState extends State { ElevatedButton( onPressed: _isModified ? _doSubmit : null, child: Row(mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0), - Text(widget.submitText).paddingLTRB(0, 0, 4, 0) - ]), - ).paddingSymmetric(vertical: 4).alignAtCenter(), + Icon(Icons.check, size: 24.scaled(context)) + .paddingLTRB(0, 0, 4, 0), + Text(widget.submitText).paddingLTRB(0, 0, 4.scaled(context), 0) + ]).paddingAll(4.scaled(context)), + ).paddingSymmetric(vertical: 4.scaled(context)).alignAtCenter(), ], ), ); diff --git a/lib/keyboard_shortcuts.dart b/lib/keyboard_shortcuts.dart index 6708d72..7b952c8 100644 --- a/lib/keyboard_shortcuts.dart +++ b/lib/keyboard_shortcuts.dart @@ -32,6 +32,14 @@ class DeveloperPageIntent extends Intent { const DeveloperPageIntent(); } +class DisplayScaleUpIntent extends Intent { + const DisplayScaleUpIntent(); +} + +class DisplayScaleDownIntent extends Intent { + const DisplayScaleDownIntent(); +} + class KeyboardShortcuts extends StatelessWidget { const KeyboardShortcuts({required this.child, super.key}); @@ -57,7 +65,7 @@ class KeyboardShortcuts extends StatelessWidget { }); } - void changeBrightness(BuildContext context) { + void _changeBrightness(BuildContext context) { singleFuture(this, () async { final prefs = PreferencesRepository.instance.value; @@ -79,7 +87,7 @@ class KeyboardShortcuts extends StatelessWidget { }); } - void changeColor(BuildContext context) { + void _changeColor(BuildContext context) { singleFuture(this, () async { final prefs = PreferencesRepository.instance.value; final oldColor = prefs.themePreference.colorPreference; @@ -100,6 +108,54 @@ class KeyboardShortcuts extends StatelessWidget { }); } + void _displayScaleUp(BuildContext context) { + singleFuture(this, () async { + final prefs = PreferencesRepository.instance.value; + final oldIndex = displayScaleToIndex(prefs.themePreference.displayScale); + if (oldIndex == maxDisplayScaleIndex) { + return; + } + final newIndex = oldIndex + 1; + final newDisplayScaleName = indexToDisplayScaleName(newIndex); + + log.info('Changing display scale to $newDisplayScaleName'); + + final newPrefs = prefs.copyWith( + themePreference: prefs.themePreference + .copyWith(displayScale: indexToDisplayScale(newIndex))); + await PreferencesRepository.instance.set(newPrefs); + + if (context.mounted) { + ThemeSwitcher.of(context) + .changeTheme(theme: newPrefs.themePreference.themeData()); + } + }); + } + + void _displayScaleDown(BuildContext context) { + singleFuture(this, () async { + final prefs = PreferencesRepository.instance.value; + final oldIndex = displayScaleToIndex(prefs.themePreference.displayScale); + if (oldIndex == 0) { + return; + } + final newIndex = oldIndex - 1; + final newDisplayScaleName = indexToDisplayScaleName(newIndex); + + log.info('Changing display scale to $newDisplayScaleName'); + + final newPrefs = prefs.copyWith( + themePreference: prefs.themePreference + .copyWith(displayScale: indexToDisplayScale(newIndex))); + await PreferencesRepository.instance.set(newPrefs); + + if (context.mounted) { + ThemeSwitcher.of(context) + .changeTheme(theme: newPrefs.themePreference.themeData()); + } + }); + } + void _attachDetach(BuildContext context) { singleFuture(this, () async { if (ProcessorRepository.instance.processorConnectionState.isAttached) { @@ -125,44 +181,88 @@ class KeyboardShortcuts extends StatelessWidget { @override Widget build(BuildContext context) => ThemeSwitcher( builder: (context) => Shortcuts( - shortcuts: const { - SingleActivator( + shortcuts: { + ////////////////////////// Reload Theme + const SingleActivator( LogicalKeyboardKey.keyR, control: true, alt: true, - ): ReloadThemeIntent(), - SingleActivator( + ): const ReloadThemeIntent(), + ////////////////////////// Switch Brightness + const SingleActivator( LogicalKeyboardKey.keyB, control: true, alt: true, - ): ChangeBrightnessIntent(), - SingleActivator( + ): const ChangeBrightnessIntent(), + ////////////////////////// Change Color + const SingleActivator( LogicalKeyboardKey.keyC, control: true, alt: true, - ): ChangeColorIntent(), - SingleActivator( - LogicalKeyboardKey.keyA, - control: true, - alt: true, - ): AttachDetachIntent(), - SingleActivator( + ): const ChangeColorIntent(), + ////////////////////////// Attach/Detach + if (kIsDebugMode) + const SingleActivator( + LogicalKeyboardKey.keyA, + control: true, + alt: true, + ): const AttachDetachIntent(), + ////////////////////////// Show Developer Page + const SingleActivator( LogicalKeyboardKey.keyD, control: true, alt: true, - ): DeveloperPageIntent(), + ): const DeveloperPageIntent(), + ////////////////////////// Display Scale Up + SingleActivator( + LogicalKeyboardKey.equal, + meta: isMac || isiOS, + control: !(isMac || isiOS), + ): const DisplayScaleUpIntent(), + SingleActivator( + LogicalKeyboardKey.equal, + shift: true, + meta: isMac || isiOS, + control: !(isMac || isiOS), + ): const DisplayScaleUpIntent(), + SingleActivator( + LogicalKeyboardKey.add, + shift: true, + meta: isMac || isiOS, + control: !(isMac || isiOS), + ): const DisplayScaleUpIntent(), + SingleActivator( + LogicalKeyboardKey.numpadAdd, + meta: isMac || isiOS, + control: !(isMac || isiOS), + ): const DisplayScaleUpIntent(), + ////////////////////////// Display Scale Down + SingleActivator( + LogicalKeyboardKey.minus, + meta: isMac || isiOS, + control: !(isMac || isiOS), + ): const DisplayScaleDownIntent(), + SingleActivator( + LogicalKeyboardKey.numpadSubtract, + meta: isMac || isiOS, + control: !(isMac || isiOS), + ): const DisplayScaleDownIntent(), }, child: Actions(actions: >{ ReloadThemeIntent: CallbackAction( onInvoke: (intent) => reloadTheme(context)), ChangeBrightnessIntent: CallbackAction( - onInvoke: (intent) => changeBrightness(context)), + onInvoke: (intent) => _changeBrightness(context)), ChangeColorIntent: CallbackAction( - onInvoke: (intent) => changeColor(context)), + onInvoke: (intent) => _changeColor(context)), AttachDetachIntent: CallbackAction( onInvoke: (intent) => _attachDetach(context)), DeveloperPageIntent: CallbackAction( onInvoke: (intent) => _developerPage(context)), + DisplayScaleUpIntent: CallbackAction( + onInvoke: (intent) => _displayScaleUp(context)), + DisplayScaleDownIntent: CallbackAction( + onInvoke: (intent) => _displayScaleDown(context)), }, child: Focus(autofocus: true, child: child)))); ///////////////////////////////////////////////////////// diff --git a/lib/layout/default_app_bar.dart b/lib/layout/default_app_bar.dart index b9c0b41..7742dad 100644 --- a/lib/layout/default_app_bar.dart +++ b/lib/layout/default_app_bar.dart @@ -2,21 +2,27 @@ import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import '../theme/theme.dart'; + class DefaultAppBar extends AppBar { DefaultAppBar( - {super.title, + {required BuildContext context, + super.title, super.flexibleSpace, super.key, Widget? leading, super.actions}) : super( + toolbarHeight: 40.scaled(context), + leadingWidth: 40.scaled(context), leading: leading ?? Container( - margin: const EdgeInsets.all(4), + margin: const EdgeInsets.all(4).scaled(context), decoration: BoxDecoration( color: Colors.black.withAlpha(32), shape: BoxShape.circle), - child: - SvgPicture.asset('assets/images/vlogo.svg', height: 24) - .paddingAll(4))); + child: SvgPicture.asset('assets/images/vlogo.svg', + width: 24.scaled(context), + height: 24.scaled(context)) + .paddingAll(4.scaled(context)))); } diff --git a/lib/layout/home/drawer_menu/drawer_menu.dart b/lib/layout/home/drawer_menu/drawer_menu.dart index 0863b1f..33f2bc2 100644 --- a/lib/layout/home/drawer_menu/drawer_menu.dart +++ b/lib/layout/home/drawer_menu/drawer_menu.dart @@ -95,19 +95,15 @@ class _DrawerMenuState extends State { activeBorder = scale.primary; } - final avatar = AvatarWidget( + final avatar = StyledAvatar( name: name, - size: 34, - borderColor: border, - foregroundColor: loggedIn ? scale.primaryText : scale.subtleText, - backgroundColor: loggedIn ? scale.primary : scale.elementBackground, - scaleConfig: scaleConfig, - textStyle: theme.textTheme.titleLarge!, + size: 34.scaled(context), ); return AnimatedPadding( padding: EdgeInsets.fromLTRB(selected ? 0 : 8, selected ? 0 : 2, - selected ? 0 : 8, selected ? 0 : 2), + selected ? 0 : 8, selected ? 0 : 2) + .scaled(context), duration: const Duration(milliseconds: 50), child: MenuItemWidget( title: name, @@ -144,7 +140,7 @@ class _DrawerMenuState extends State { (scaleConfig.preferBorders || scaleConfig.useVisualIndicators) ? null : activeBorder, - minHeight: 48, + minHeight: 48.scaled(context), )); } @@ -196,7 +192,8 @@ class _DrawerMenuState extends State { color: scaleScheme.errorScale.subtleBorder, borderRadius: 12 * scaleConfig.borderRadiusScale), ); - loggedInAccounts.add(loggedInAccount.paddingLTRB(0, 0, 0, 8)); + loggedInAccounts + .add(loggedInAccount.paddingLTRB(0, 0, 0, 8.scaled(context))); } else { // Account is not logged in final scale = theme.extension()!.grayScale; @@ -246,8 +243,8 @@ class _DrawerMenuState extends State { } return IconButton( icon: icon, + padding: const EdgeInsets.all(12), color: border, - constraints: const BoxConstraints.expand(height: 48, width: 48), style: ButtonStyle( backgroundColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.hovered)) { @@ -286,7 +283,10 @@ class _DrawerMenuState extends State { final scale = scaleScheme.scale(_scaleKind); final settingsButton = _getButton( - icon: const Icon(Icons.settings), + icon: const Icon( + Icons.settings, + applyTextScaling: true, + ), tooltip: translate('menu.settings_tooltip'), scale: scale, scaleConfig: scaleConfig, @@ -295,7 +295,10 @@ class _DrawerMenuState extends State { }).paddingLTRB(0, 0, 16, 0); final addButton = _getButton( - icon: const Icon(Icons.add), + icon: const Icon( + Icons.add, + applyTextScaling: true, + ), tooltip: translate('menu.add_account_tooltip'), scale: scale, scaleConfig: scaleConfig, @@ -364,7 +367,7 @@ class _DrawerMenuState extends State { // : null) // .paddingLTRB(0, 0, 16, 0), GestureDetector( - onLongPress: () async { + onLongPress: () { context .findAncestorWidgetOfExactType()! .reloadTheme(context); diff --git a/lib/layout/home/drawer_menu/menu_item_widget.dart b/lib/layout/home/drawer_menu/menu_item_widget.dart index 1255458..80e466b 100644 --- a/lib/layout/home/drawer_menu/menu_item_widget.dart +++ b/lib/layout/home/drawer_menu/menu_item_widget.dart @@ -2,6 +2,8 @@ import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import '../../../theme/views/preferences/preferences.dart'; + class MenuItemWidget extends StatelessWidget { const MenuItemWidget({ required this.title, @@ -81,7 +83,7 @@ class MenuItemWidget extends StatelessWidget { hoverColor: footerButtonIconHoverColor, icon: Icon( footerButtonIcon, - size: 24, + size: 24.scaled(context), ), onPressed: footerCallback), ], diff --git a/lib/layout/home/home_account_ready.dart b/lib/layout/home/home_account_ready.dart index a2966a6..5ae1180 100644 --- a/lib/layout/home/home_account_ready.dart +++ b/lib/layout/home/home_account_ready.dart @@ -28,73 +28,81 @@ class _HomeAccountReadyState extends State { final theme = Theme.of(context); final scale = theme.extension()!; final scaleConfig = theme.extension()!; - return IconButton( - icon: const Icon(Icons.menu), - color: scaleConfig.preferBorders - ? scale.primaryScale.border - : scale.primaryScale.borderText, - constraints: const BoxConstraints.expand(height: 40, width: 40), - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - scaleConfig.preferBorders - ? scale.primaryScale.hoverElementBackground - : scale.primaryScale.hoverBorder), - shape: WidgetStateProperty.all( - RoundedRectangleBorder( - side: !scaleConfig.useVisualIndicators - ? BorderSide.none - : BorderSide( - strokeAlign: BorderSide.strokeAlignCenter, - color: scaleConfig.preferBorders - ? scale.primaryScale.border - : scale.primaryScale.borderText, - width: 2), - borderRadius: BorderRadius.all( - Radius.circular(8 * scaleConfig.borderRadiusScale))), - )), - tooltip: translate('menu.accounts_menu_tooltip'), - onPressed: () async { - final ctrl = context.read(); - await ctrl.toggle?.call(); - }); + return AspectRatio( + aspectRatio: 1, + child: IconButton( + icon: const Icon( + Icons.menu, + applyTextScaling: true, + ), + color: scaleConfig.preferBorders + ? scale.primaryScale.border + : scale.primaryScale.borderText, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + scaleConfig.preferBorders + ? scale.primaryScale.hoverElementBackground + : scale.primaryScale.hoverBorder), + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + side: !scaleConfig.useVisualIndicators + ? BorderSide.none + : BorderSide( + strokeAlign: BorderSide.strokeAlignCenter, + color: scaleConfig.preferBorders + ? scale.primaryScale.border + : scale.primaryScale.borderText, + width: 2), + borderRadius: BorderRadius.all(Radius.circular( + 8 * scaleConfig.borderRadiusScale))), + )), + tooltip: translate('menu.accounts_menu_tooltip'), + onPressed: () async { + final ctrl = context.read(); + await ctrl.toggle?.call(); + })); }); Widget buildContactsButton() => Builder(builder: (context) { final theme = Theme.of(context); final scale = theme.extension()!; final scaleConfig = theme.extension()!; - return IconButton( - icon: const Icon(Icons.contacts), - color: scaleConfig.preferBorders - ? scale.primaryScale.border - : scale.primaryScale.borderText, - constraints: const BoxConstraints.expand(height: 40, width: 40), - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - scaleConfig.preferBorders - ? scale.primaryScale.hoverElementBackground - : scale.primaryScale.hoverBorder), - shape: WidgetStateProperty.all( - RoundedRectangleBorder( - side: !scaleConfig.useVisualIndicators - ? BorderSide.none - : BorderSide( - strokeAlign: BorderSide.strokeAlignCenter, - color: scaleConfig.preferBorders - ? scale.primaryScale.border - : scale.primaryScale.borderText, - width: 2), - borderRadius: BorderRadius.all( - Radius.circular(8 * scaleConfig.borderRadiusScale))), - )), - tooltip: translate('menu.contacts_tooltip'), - onPressed: () async { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => const ContactsPage(), + return AspectRatio( + aspectRatio: 1, + child: IconButton( + icon: const Icon( + Icons.contacts, + applyTextScaling: true, ), - ); - }); + color: scaleConfig.preferBorders + ? scale.primaryScale.border + : scale.primaryScale.borderText, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + scaleConfig.preferBorders + ? scale.primaryScale.hoverElementBackground + : scale.primaryScale.hoverBorder), + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + side: !scaleConfig.useVisualIndicators + ? BorderSide.none + : BorderSide( + strokeAlign: BorderSide.strokeAlignCenter, + color: scaleConfig.preferBorders + ? scale.primaryScale.border + : scale.primaryScale.borderText, + width: 2), + borderRadius: BorderRadius.all(Radius.circular( + 8 * scaleConfig.borderRadiusScale))), + )), + tooltip: translate('menu.contacts_tooltip'), + onPressed: () async { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const ContactsPage(), + ), + ); + })); }); Widget buildLeftPane(BuildContext context) => Builder( @@ -112,14 +120,17 @@ class _HomeAccountReadyState extends State { ? scale.primaryScale.subtleBackground : scale.primaryScale.subtleBorder, child: Column(children: [ - Row(children: [ - buildMenuButton().paddingLTRB(0, 0, 8, 0), - ProfileWidget( - profile: profile, - showPronouns: false, - ).expanded(), - buildContactsButton().paddingLTRB(8, 0, 0, 0), - ]).paddingAll(8), + IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + buildMenuButton().paddingLTRB(0, 0, 8, 0), + ProfileWidget( + profile: profile, + showPronouns: false, + ).expanded(), + buildContactsButton().paddingLTRB(8, 0, 0, 0), + ])).paddingAll(8), const ChatListWidget().expanded() ])); }))); diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index b4c3b58..e774790 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -71,8 +71,9 @@ class HomeScreenState extends State context: context, title: translate('splash.beta_title'), child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(Icons.warning, size: 64), + Icon(Icons.warning, size: 64.scaled(context)), RichText( + textScaler: MediaQuery.of(context).textScaler, textAlign: TextAlign.center, text: TextSpan( children: [ @@ -206,34 +207,36 @@ class HomeScreenState extends State .indexWhere((x) => x.superIdentity.recordKey == activeLocalAccount); final canClose = activeIndex != -1; + final drawer = ZoomDrawer( + controller: _zoomDrawerController, + menuScreen: Builder(builder: (context) { + final zoomDrawer = ZoomDrawer.of(context); + zoomDrawer!.stateNotifier.addListener(() { + if (zoomDrawer.isOpen()) { + FocusManager.instance.primaryFocus?.unfocus(); + } + }); + return const DrawerMenu(); + }), + mainScreen: Provider.value( + value: _zoomDrawerController, + child: Builder(builder: _buildAccountPageView)), + borderRadius: 0, + angle: 0, + openCurve: Curves.fastEaseInToSlowEaseOut, + closeCurve: Curves.fastEaseInToSlowEaseOut, + menuScreenTapClose: canClose, + mainScreenTapClose: canClose, + disableDragGesture: !canClose, + mainScreenScale: .25, + slideWidth: min(360, MediaQuery.of(context).size.width * 0.9), + ); + + final drawerWithAvoider = + isWeb ? drawer : KeyboardAvoider(curve: Curves.ease, child: drawer); + return DefaultTextStyle( - style: theme.textTheme.bodySmall!, - child: KeyboardAvoider( - curve: Curves.ease, - child: ZoomDrawer( - controller: _zoomDrawerController, - menuScreen: Builder(builder: (context) { - final zoomDrawer = ZoomDrawer.of(context); - zoomDrawer!.stateNotifier.addListener(() { - if (zoomDrawer.isOpen()) { - FocusManager.instance.primaryFocus?.unfocus(); - } - }); - return const DrawerMenu(); - }), - mainScreen: Provider.value( - value: _zoomDrawerController, - child: Builder(builder: _buildAccountPageView)), - borderRadius: 0, - angle: 0, - openCurve: Curves.fastEaseInToSlowEaseOut, - closeCurve: Curves.fastEaseInToSlowEaseOut, - menuScreenTapClose: canClose, - mainScreenTapClose: canClose, - disableDragGesture: !canClose, - mainScreenScale: .25, - slideWidth: min(360, MediaQuery.of(context).size.width * 0.9), - ))); + style: theme.textTheme.bodySmall!, child: drawerWithAvoider); } //////////////////////////////////////////////////////////////////////////// diff --git a/lib/notifications/views/notifications_preferences.dart b/lib/notifications/views/notifications_preferences.dart index 82f3555..8918fa9 100644 --- a/lib/notifications/views/notifications_preferences.dart +++ b/lib/notifications/views/notifications_preferences.dart @@ -1,24 +1,13 @@ import 'package:awesome_extensions/awesome_extensions.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 '../../theme/theme.dart'; import '../notifications.dart'; -const String formFieldDisplayBetaWarning = 'displayBetaWarning'; -const String formFieldEnableBadge = 'enableBadge'; -const String formFieldEnableNotifications = 'enableNotifications'; -const String formFieldMessageNotificationContent = 'messageNotificationContent'; -const String formFieldInvitationAcceptMode = 'invitationAcceptMode'; -const String formFieldInvitationAcceptSound = 'invitationAcceptSound'; -const String formFieldMessageReceivedMode = 'messageReceivedMode'; -const String formFieldMessageReceivedSound = 'messageReceivedSound'; -const String formFieldMessageSentSound = 'messageSentSound'; - Widget buildSettingsPageNotificationPreferences( - {required BuildContext context, required void Function() onChanged}) { + {required BuildContext context}) { final theme = Theme.of(context); final scale = theme.extension()!; final scaleConfig = theme.extension()!; @@ -33,7 +22,6 @@ Widget buildSettingsPageNotificationPreferences( final newPrefs = preferencesRepository.value .copyWith(notificationsPreference: newNotificationsPreference); await preferencesRepository.set(newPrefs); - onChanged(); } List> notificationModeItems() { @@ -54,9 +42,10 @@ Widget buildSettingsPageNotificationPreferences( enabled: x.$2, child: Text( x.$3, - style: textTheme.labelSmall, + softWrap: false, + style: textTheme.labelMedium, textAlign: TextAlign.center, - ))); + ).fit(fit: BoxFit.scaleDown))); } return out; } @@ -77,7 +66,8 @@ Widget buildSettingsPageNotificationPreferences( enabled: x.$2, child: Text( x.$3, - style: textTheme.labelSmall, + softWrap: false, + style: textTheme.labelMedium, textAlign: TextAlign.center, ))); } @@ -110,7 +100,8 @@ Widget buildSettingsPageNotificationPreferences( enabled: x.$2, child: Text( x.$3, - style: textTheme.labelSmall, + softWrap: false, + style: textTheme.labelMedium, textAlign: TextAlign.center, ))); } @@ -127,66 +118,45 @@ Widget buildSettingsPageNotificationPreferences( ), child: Column(mainAxisSize: MainAxisSize.min, children: [ // Display Beta Warning - FormBuilderCheckbox( - name: formFieldDisplayBetaWarning, - title: Text(translate('settings_page.display_beta_warning'), - style: textTheme.labelMedium), - initialValue: notificationsPreference.displayBetaWarning, + StyledCheckbox( + label: translate('settings_page.display_beta_warning'), + value: notificationsPreference.displayBetaWarning, onChanged: (value) async { - if (value == null) { - return; - } final newNotificationsPreference = notificationsPreference.copyWith(displayBetaWarning: value); await updatePreferences(newNotificationsPreference); }), // Enable Badge - FormBuilderCheckbox( - name: formFieldEnableBadge, - title: Text(translate('settings_page.enable_badge'), - style: textTheme.labelMedium), - initialValue: notificationsPreference.enableBadge, + StyledCheckbox( + label: translate('settings_page.enable_badge'), + value: notificationsPreference.enableBadge, onChanged: (value) async { - if (value == null) { - return; - } final newNotificationsPreference = notificationsPreference.copyWith(enableBadge: value); await updatePreferences(newNotificationsPreference); }), // Enable Notifications - FormBuilderCheckbox( - name: formFieldEnableNotifications, - title: Text(translate('settings_page.enable_notifications'), - style: textTheme.labelMedium), - initialValue: notificationsPreference.enableNotifications, + StyledCheckbox( + label: translate('settings_page.enable_notifications'), + value: notificationsPreference.enableNotifications, onChanged: (value) async { - if (value == null) { - return; - } final newNotificationsPreference = notificationsPreference.copyWith(enableNotifications: value); await updatePreferences(newNotificationsPreference); }), - - FormBuilderDropdown( - name: formFieldMessageNotificationContent, - isDense: false, - decoration: InputDecoration( - labelText: translate('settings_page.message_notification_content')), - enabled: notificationsPreference.enableNotifications, - initialValue: notificationsPreference.messageNotificationContent, - onChanged: (value) async { - if (value == null) { - return; - } - final newNotificationsPreference = notificationsPreference.copyWith( - messageNotificationContent: value); - await updatePreferences(newNotificationsPreference); - }, + StyledDropdown( items: messageNotificationContentItems(), - ).paddingLTRB(0, 4, 0, 4), + value: notificationsPreference.messageNotificationContent, + decoratorLabel: translate('settings_page.message_notification_content'), + onChanged: !notificationsPreference.enableNotifications + ? null + : (value) async { + final newNotificationsPreference = notificationsPreference + .copyWith(messageNotificationContent: value); + await updatePreferences(newNotificationsPreference); + }, + ).paddingLTRB(0, 4.scaled(context), 0, 4.scaled(context)), // Notifications Table( @@ -199,95 +169,85 @@ Widget buildSettingsPageNotificationPreferences( color: scale.primaryScale.border, decorationColor: scale.primaryScale.border, decoration: TextDecoration.underline)) - .paddingAll(8), + .paddingAll(8.scaled(context)), Text(translate('settings_page.delivery'), textAlign: TextAlign.center, style: textTheme.titleMedium!.copyWith( color: scale.primaryScale.border, decorationColor: scale.primaryScale.border, decoration: TextDecoration.underline)) - .paddingAll(8), + .paddingAll(8.scaled(context)), Text(translate('settings_page.sound'), textAlign: TextAlign.center, style: textTheme.titleMedium!.copyWith( color: scale.primaryScale.border, decorationColor: scale.primaryScale.border, decoration: TextDecoration.underline)) - .paddingAll(8), + .paddingAll(8.scaled(context)), ]), TableRow(children: [ // Invitation accepted Text( textAlign: TextAlign.right, translate('settings_page.invitation_accepted')) - .paddingAll(8), - FormBuilderDropdown( - name: formFieldInvitationAcceptMode, - isDense: false, - enabled: notificationsPreference.enableNotifications, - initialValue: notificationsPreference.onInvitationAcceptedMode, - onChanged: (value) async { - if (value == null) { - return; - } - final newNotificationsPreference = notificationsPreference - .copyWith(onInvitationAcceptedMode: value); - await updatePreferences(newNotificationsPreference); - }, + .paddingAll(4.scaled(context)), + StyledDropdown( items: notificationModeItems(), - ).paddingAll(4), - FormBuilderDropdown( - name: formFieldInvitationAcceptSound, - isDense: false, - enabled: notificationsPreference.enableNotifications, - initialValue: notificationsPreference.onInvitationAcceptedSound, - onChanged: (value) async { - if (value == null) { - return; - } - final newNotificationsPreference = notificationsPreference - .copyWith(onInvitationAcceptedSound: value); - await updatePreferences(newNotificationsPreference); - }, + value: notificationsPreference.onInvitationAcceptedMode, + onChanged: !notificationsPreference.enableNotifications + ? null + : (value) async { + final newNotificationsPreference = + notificationsPreference.copyWith( + onInvitationAcceptedMode: value); + await updatePreferences(newNotificationsPreference); + }, + ).paddingAll(4.scaled(context)), + StyledDropdown( items: soundEffectItems(), - ).paddingLTRB(4, 4, 0, 4) + value: notificationsPreference.onInvitationAcceptedSound, + onChanged: !notificationsPreference.enableNotifications + ? null + : (value) async { + final newNotificationsPreference = + notificationsPreference.copyWith( + onInvitationAcceptedSound: value); + await updatePreferences(newNotificationsPreference); + }, + ).paddingLTRB( + 4.scaled(context), 4.scaled(context), 0, 4.scaled(context)) ]), // Message received TableRow(children: [ Text( textAlign: TextAlign.right, translate('settings_page.message_received')) - .paddingAll(8), - FormBuilderDropdown( - name: formFieldMessageReceivedMode, - isDense: false, - enabled: notificationsPreference.enableNotifications, - initialValue: notificationsPreference.onMessageReceivedMode, - onChanged: (value) async { - if (value == null) { - return; - } - final newNotificationsPreference = notificationsPreference - .copyWith(onMessageReceivedMode: value); - await updatePreferences(newNotificationsPreference); - }, + .paddingAll(4.scaled(context)), + StyledDropdown( items: notificationModeItems(), + value: notificationsPreference.onMessageReceivedMode, + onChanged: !notificationsPreference.enableNotifications + ? null + : (value) async { + final newNotificationsPreference = + notificationsPreference.copyWith( + onMessageReceivedMode: value); + await updatePreferences(newNotificationsPreference); + }, ).paddingAll(4), - FormBuilderDropdown( - name: formFieldMessageReceivedSound, - isDense: false, - enabled: notificationsPreference.enableNotifications, - initialValue: notificationsPreference.onMessageReceivedSound, - onChanged: (value) async { - if (value == null) { - return; - } - final newNotificationsPreference = notificationsPreference - .copyWith(onMessageReceivedSound: value); - await updatePreferences(newNotificationsPreference); - }, + StyledDropdown( items: soundEffectItems(), - ).paddingLTRB(4, 4, 0, 4) + value: notificationsPreference.onMessageReceivedSound, + onChanged: !notificationsPreference.enableNotifications + ? null + : (value) async { + final newNotificationsPreference = + notificationsPreference.copyWith( + onMessageReceivedSound: value); + await updatePreferences(newNotificationsPreference); + }, + ).paddingLTRB( + 4.scaled(context), 4.scaled(context), 0, 4.scaled(context)) ]), // Message sent @@ -295,25 +255,23 @@ Widget buildSettingsPageNotificationPreferences( Text( textAlign: TextAlign.right, translate('settings_page.message_sent')) - .paddingAll(8), + .paddingAll(4.scaled(context)), const SizedBox.shrink(), - FormBuilderDropdown( - name: formFieldMessageSentSound, - isDense: false, - enabled: notificationsPreference.enableNotifications, - initialValue: notificationsPreference.onMessageSentSound, - onChanged: (value) async { - if (value == null) { - return; - } - final newNotificationsPreference = notificationsPreference - .copyWith(onMessageSentSound: value); - await updatePreferences(newNotificationsPreference); - }, + StyledDropdown( items: soundEffectItems(), - ).paddingLTRB(4, 4, 0, 4) + value: notificationsPreference.onMessageSentSound, + onChanged: !notificationsPreference.enableNotifications + ? null + : (value) async { + final newNotificationsPreference = + notificationsPreference.copyWith( + onMessageSentSound: value); + await updatePreferences(newNotificationsPreference); + }, + ).paddingLTRB( + 4.scaled(context), 4.scaled(context), 0, 4.scaled(context)) ]), ]) - ]).paddingAll(8), + ]).paddingAll(8.scaled(context)), ); } diff --git a/lib/router/views/router_shell.dart b/lib/router/views/router_shell.dart index 164c452..d22129f 100644 --- a/lib/router/views/router_shell.dart +++ b/lib/router/views/router_shell.dart @@ -1,7 +1,9 @@ +import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import '../../keyboard_shortcuts.dart'; import '../../notifications/notifications.dart'; +import '../../settings/settings.dart'; import '../../theme/theme.dart'; class RouterShell extends StatelessWidget { @@ -10,7 +12,13 @@ class RouterShell extends StatelessWidget { @override Widget build(BuildContext context) => PopControl( dismissible: false, - child: NotificationsWidget(child: KeyboardShortcuts(child: _child))); + child: AsyncBlocBuilder( + builder: (context, state) => MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaler: + TextScaler.linear(state.themePreference.displayScale)), + child: NotificationsWidget( + child: KeyboardShortcuts(child: _child))))); final Widget _child; } diff --git a/lib/settings/models/preferences.dart b/lib/settings/models/preferences.dart index 3ef683e..a7432d6 100644 --- a/lib/settings/models/preferences.dart +++ b/lib/settings/models/preferences.dart @@ -20,7 +20,7 @@ sealed class LockPreference with _$LockPreference { factory LockPreference.fromJson(dynamic json) => _$LockPreferenceFromJson(json as Map); - static const LockPreference defaults = LockPreference(); + static const defaults = LockPreference(); } // Theme supports multiple translations @@ -49,5 +49,5 @@ sealed class Preferences with _$Preferences { factory Preferences.fromJson(dynamic json) => _$PreferencesFromJson(json as Map); - static const Preferences defaults = Preferences(); + static const defaults = Preferences(); } diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart index 2a05f08..810b20e 100644 --- a/lib/settings/settings_page.dart +++ b/lib/settings/settings_page.dart @@ -1,7 +1,6 @@ import 'package:animated_theme_switcher/animated_theme_switcher.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:go_router/go_router.dart'; @@ -11,62 +10,53 @@ import '../theme/theme.dart'; import '../veilid_processor/veilid_processor.dart'; import 'settings.dart'; -class SettingsPage extends StatefulWidget { +class SettingsPage extends StatelessWidget { 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(); - } - @override Widget build(BuildContext context) => AsyncBlocBuilder( builder: (context, state) => ThemeSwitcher.withTheme( - builder: (_, switcher, theme) => StyledScaffold( + builder: (_, switcher, theme) => StyledScaffold( appBar: DefaultAppBar( + context: context, title: Text(translate('settings_page.titlebar')), leading: IconButton( - icon: const Icon(Icons.arrow_back), + iconSize: 24.scaled(context), + icon: Icon(Icons.arrow_back), onPressed: () => GoRouterHelper(context).pop(), ), actions: [ const SignalStrengthMeterWidget() .paddingLTRB(16, 0, 16, 0), ]), - body: ThemeSwitchingArea( - child: FormBuilder( - key: _formKey, - child: ListView( - padding: const EdgeInsets.all(8), - children: [ - buildSettingsPageColorPreferences( - context: context, - switcher: switcher, - onChanged: () => setState(() {})) - .paddingLTRB(0, 8, 0, 0), - buildSettingsPageBrightnessPreferences( - context: context, - switcher: switcher, - onChanged: () => setState(() {})), - buildSettingsPageWallpaperPreferences( - context: context, - switcher: switcher, - onChanged: () => setState(() {})), - buildSettingsPageNotificationPreferences( - context: context, - onChanged: () => setState(() {})), - ].map((x) => x.paddingLTRB(0, 0, 0, 8)).toList(), + body: ListView( + padding: const EdgeInsets.all(8).scaled(context), + children: [ + buildSettingsPageColorPreferences( + context: context, + switcher: switcher, ), - ).paddingSymmetric(horizontal: 8, vertical: 8), - )))); + buildSettingsPageBrightnessPreferences( + context: context, + switcher: switcher, + ), + buildSettingsPageDisplayScalePreferences( + context: context, + switcher: switcher, + ), + buildSettingsPageWallpaperPreferences( + context: context, + switcher: switcher, + ), + buildSettingsPageNotificationPreferences( + context: context, + ), + ] + .map((x) => x.paddingLTRB(0, 0, 0, 8.scaled(context))) + .toList(), + ).paddingSymmetric(vertical: 4.scaled(context)), + ).paddingSymmetric( + horizontal: 8.scaled(context), vertical: 8.scaled(context)), + )); } diff --git a/lib/theme/models/contrast_generator.dart b/lib/theme/models/contrast_generator.dart index 314e28a..05c5f55 100644 --- a/lib/theme/models/contrast_generator.dart +++ b/lib/theme/models/contrast_generator.dart @@ -308,6 +308,13 @@ ThemeData contrastGenerator({ side: elementBorderWidgetStateProperty(), backgroundColor: elementBackgroundWidgetStateProperty())); + final sliderTheme = SliderThemeData.fromPrimaryColors( + primaryColor: scheme.primaryScale.borderText, + primaryColorDark: scheme.primaryScale.border, + primaryColorLight: scheme.primaryScale.border, + valueIndicatorTextStyle: textTheme.labelMedium! + .copyWith(color: scheme.primaryScale.borderText)); + final themeData = baseThemeData.copyWith( // chipTheme: baseThemeData.chipTheme.copyWith( // backgroundColor: scaleScheme.primaryScale.elementBackground, @@ -316,6 +323,7 @@ ThemeData contrastGenerator({ // checkmarkColor: scaleScheme.primaryScale.border, // side: BorderSide(color: scaleScheme.primaryScale.border)), elevatedButtonTheme: elevatedButtonTheme, + sliderTheme: sliderTheme, textSelectionTheme: TextSelectionThemeData( cursorColor: scheme.primaryScale.appText, selectionColor: scheme.primaryScale.appText.withAlpha(0x7F), diff --git a/lib/theme/models/scale_theme/scale_theme.dart b/lib/theme/models/scale_theme/scale_theme.dart index c3217ea..755bd54 100644 --- a/lib/theme/models/scale_theme/scale_theme.dart +++ b/lib/theme/models/scale_theme/scale_theme.dart @@ -132,6 +132,13 @@ class ScaleTheme extends ThemeExtension { iconColor: elementColorWidgetStateProperty(), )); + final sliderTheme = SliderThemeData.fromPrimaryColors( + primaryColor: scheme.primaryScale.hoverBorder, + primaryColorDark: scheme.primaryScale.border, + primaryColorLight: scheme.primaryScale.border, + valueIndicatorTextStyle: textTheme.labelMedium! + .copyWith(color: scheme.primaryScale.borderText)); + final themeData = baseThemeData.copyWith( scrollbarTheme: baseThemeData.scrollbarTheme.copyWith( thumbColor: WidgetStateProperty.resolveWith((states) { @@ -183,6 +190,7 @@ class ScaleTheme extends ThemeExtension { elevatedButtonTheme: elevatedButtonTheme, inputDecorationTheme: ScaleInputDecoratorTheme(scheme, config, textTheme), + sliderTheme: sliderTheme, extensions: >[scheme, config, this]); return themeData; diff --git a/lib/theme/models/theme_preference.dart b/lib/theme/models/theme_preference.dart index aaad52d..44d06d8 100644 --- a/lib/theme/models/theme_preference.dart +++ b/lib/theme/models/theme_preference.dart @@ -61,7 +61,7 @@ sealed class ThemePreferences with _$ThemePreferences { factory ThemePreferences.fromJson(dynamic json) => _$ThemePreferencesFromJson(json as Map); - static const ThemePreferences defaults = ThemePreferences(); + static const defaults = ThemePreferences(); } extension ThemePreferencesExt on ThemePreferences { diff --git a/lib/theme/views/avatar_widget.dart b/lib/theme/views/avatar_widget.dart deleted file mode 100644 index 42bea11..0000000 --- a/lib/theme/views/avatar_widget.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:awesome_extensions/awesome_extensions.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; - -import '../theme.dart'; - -class AvatarWidget extends StatelessWidget { - const AvatarWidget({ - required String name, - required double size, - required Color borderColor, - required Color foregroundColor, - required Color backgroundColor, - required ScaleConfig scaleConfig, - required TextStyle textStyle, - super.key, - ImageProvider? imageProvider, - }) : _name = name, - _size = size, - _borderColor = borderColor, - _foregroundColor = foregroundColor, - _backgroundColor = backgroundColor, - _scaleConfig = scaleConfig, - _textStyle = textStyle, - _imageProvider = imageProvider; - - @override - Widget build(BuildContext context) { - final abbrev = _name.split(' ').map((s) => s.isEmpty ? '' : s[0]).join(); - late final String shortname; - if (abbrev.length >= 3) { - shortname = abbrev[0] + abbrev[1] + abbrev[abbrev.length - 1]; - } else { - shortname = abbrev; - } - - return Container( - height: _size, - width: _size, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: _borderColor, - width: 1 * (_size ~/ 32 + 1), - strokeAlign: BorderSide.strokeAlignOutside)), - child: AvatarImage( - //size: 32, - backgroundImage: _imageProvider, - backgroundColor: - _scaleConfig.useVisualIndicators && !_scaleConfig.preferBorders - ? _foregroundColor - : _backgroundColor, - child: Text( - shortname.isNotEmpty ? shortname : '?', - softWrap: false, - style: _textStyle.copyWith( - color: _scaleConfig.useVisualIndicators && - !_scaleConfig.preferBorders - ? _backgroundColor - : _foregroundColor, - ), - ).fit().paddingAll(_size / 16))); - } - - //////////////////////////////////////////////////////////////////////////// - final String _name; - final double _size; - final Color _borderColor; - final Color _foregroundColor; - final Color _backgroundColor; - final ScaleConfig _scaleConfig; - final TextStyle _textStyle; - final ImageProvider? _imageProvider; -} diff --git a/lib/theme/views/enter_password.dart b/lib/theme/views/enter_password.dart index fc876da..f28b69e 100644 --- a/lib/theme/views/enter_password.dart +++ b/lib/theme/views/enter_password.dart @@ -32,7 +32,7 @@ class _EnterPasswordDialogState extends State { final passwordController = TextEditingController(); final focusNode = FocusNode(); final formKey = GlobalKey(); - bool _passwordVisible = false; + var _passwordVisible = false; @override void initState() { @@ -47,7 +47,6 @@ class _EnterPasswordDialogState extends State { } @override - // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { final theme = Theme.of(context); final scale = theme.extension()!; diff --git a/lib/theme/views/brightness_preferences.dart b/lib/theme/views/preferences/brightness_preferences.dart similarity index 59% rename from lib/theme/views/brightness_preferences.dart rename to lib/theme/views/preferences/brightness_preferences.dart index 7a1bb1d..a149483 100644 --- a/lib/theme/views/brightness_preferences.dart +++ b/lib/theme/views/preferences/brightness_preferences.dart @@ -1,14 +1,12 @@ 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'; +import '../../../settings/settings.dart'; +import '../../models/models.dart'; +import '../views.dart'; -const String formFieldBrightness = 'brightness'; - -List> _getBrightnessDropdownItems() { +List> _getBrightnessDropdownItems() { const brightnessPrefs = BrightnessPreference.values; final brightnessNames = { BrightnessPreference.system: translate('brightness.system'), @@ -22,25 +20,21 @@ List> _getBrightnessDropdownItems() { } Widget buildSettingsPageBrightnessPreferences( - {required BuildContext context, - required void Function() onChanged, - required ThemeSwitcherState switcher}) { + {required BuildContext context, required ThemeSwitcherState switcher}) { final preferencesRepository = PreferencesRepository.instance; final themePreferences = preferencesRepository.value.themePreference; - return FormBuilderDropdown( - name: formFieldBrightness, - decoration: InputDecoration( - label: Text(translate('settings_page.brightness_mode'))), + + return StyledDropdown( items: _getBrightnessDropdownItems(), - initialValue: themePreferences.brightnessPreference, + value: themePreferences.brightnessPreference, + decoratorLabel: translate('settings_page.brightness_mode'), onChanged: (value) async { - final newThemePrefs = themePreferences.copyWith( - brightnessPreference: value as BrightnessPreference); + final newThemePrefs = + themePreferences.copyWith(brightnessPreference: value); final newPrefs = preferencesRepository.value .copyWith(themePreference: newThemePrefs); await preferencesRepository.set(newPrefs); switcher.changeTheme(theme: newThemePrefs.themeData()); - onChanged(); }); } diff --git a/lib/theme/views/color_preferences.dart b/lib/theme/views/preferences/color_preferences.dart similarity index 54% rename from lib/theme/views/color_preferences.dart rename to lib/theme/views/preferences/color_preferences.dart index a9a8841..2c14a93 100644 --- a/lib/theme/views/color_preferences.dart +++ b/lib/theme/views/preferences/color_preferences.dart @@ -1,16 +1,12 @@ import 'package:animated_theme_switcher/animated_theme_switcher.dart'; -import 'package:async_tools/async_tools.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'; +import '../../../settings/settings.dart'; +import '../../models/models.dart'; +import '../views.dart'; -const String formFieldTheme = 'theme'; -const String _kSwitchTheme = 'switchTheme'; - -List> _getThemeDropdownItems() { +List> _getThemeDropdownItems() { const colorPrefs = ColorPreference.values; final colorNames = { ColorPreference.scarlet: translate('themes.scarlet'), @@ -34,27 +30,20 @@ List> _getThemeDropdownItems() { } Widget buildSettingsPageColorPreferences( - {required BuildContext context, - required void Function() onChanged, - required ThemeSwitcherState switcher}) { + {required BuildContext context, required ThemeSwitcherState switcher}) { final preferencesRepository = PreferencesRepository.instance; final themePreferences = preferencesRepository.value.themePreference; - return FormBuilderDropdown( - name: formFieldTheme, - decoration: - InputDecoration(label: Text(translate('settings_page.color_theme'))), - items: _getThemeDropdownItems(), - initialValue: themePreferences.colorPreference, - onChanged: (value) { - singleFuture(_kSwitchTheme, () async { - final newThemePrefs = themePreferences.copyWith( - colorPreference: value as ColorPreference); - final newPrefs = preferencesRepository.value - .copyWith(themePreference: newThemePrefs); - await preferencesRepository.set(newPrefs); - switcher.changeTheme(theme: newThemePrefs.themeData()); - onChanged(); - }); + return StyledDropdown( + items: _getThemeDropdownItems(), + value: themePreferences.colorPreference, + decoratorLabel: translate('settings_page.color_theme'), + onChanged: (value) async { + final newThemePrefs = themePreferences.copyWith(colorPreference: value); + final newPrefs = preferencesRepository.value + .copyWith(themePreference: newThemePrefs); + + await preferencesRepository.set(newPrefs); + switcher.changeTheme(theme: newThemePrefs.themeData()); }); } diff --git a/lib/theme/views/preferences/display_scale_preferences.dart b/lib/theme/views/preferences/display_scale_preferences.dart new file mode 100644 index 0000000..84bf97d --- /dev/null +++ b/lib/theme/views/preferences/display_scale_preferences.dart @@ -0,0 +1,109 @@ +import 'dart:math'; + +import 'package:animated_theme_switcher/animated_theme_switcher.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_translate/flutter_translate.dart'; + +import '../../../settings/settings.dart'; +import '../../models/models.dart'; +import '../views.dart'; + +const _scales = [ + 1 / (1 + 1 / 2), + 1 / (1 + 1 / 3), + 1 / (1 + 1 / 4), + 1, + 1 + (1 / 4), + 1 + (1 / 2), + 1 + (1 / 1), +]; +const _scaleNames = [ + '-3', + '-2', + '-1', + '0', + '1', + '2', + '3', +]; + +const _scaleNumMult = [ + 1 / (1 + 1 / 2), + 1 / (1 + 1 / 3), + 1 / (1 + 1 / 4), + 1, + 1 + 1 / 4, + 1 + 1 / 2, + 1 + 1 / 1, +]; + +int displayScaleToIndex(double displayScale) { + final idx = _scales.indexWhere((elem) => elem > displayScale); + final currentScaleIdx = idx == -1 ? _scales.length - 1 : max(0, idx - 1); + return currentScaleIdx; +} + +double indexToDisplayScale(int scaleIdx) { + final displayScale = + _scales[max(min(scaleIdx, _scales.length - 1), 0)].toDouble(); + return displayScale; +} + +String indexToDisplayScaleName(int scaleIdx) => + _scaleNames[max(min(scaleIdx, _scales.length - 1), 0)]; + +final maxDisplayScaleIndex = _scales.length - 1; + +Widget buildSettingsPageDisplayScalePreferences( + {required BuildContext context, required ThemeSwitcherState switcher}) { + final preferencesRepository = PreferencesRepository.instance; + final themePreferences = preferencesRepository.value.themePreference; + + final currentScaleIdx = displayScaleToIndex(themePreferences.displayScale); + final currentScaleName = indexToDisplayScaleName(currentScaleIdx); + + return StyledSlider( + value: currentScaleIdx.toDouble(), + label: currentScaleName, + decoratorLabel: translate('settings_page.display_scale'), + max: _scales.length - 1.toDouble(), + divisions: _scales.length - 1, + leftWidget: const Icon(Icons.text_decrease), + rightWidget: const Icon(Icons.text_increase), + onChanged: (value) async { + final scaleIdx = value.toInt(); + final displayScale = indexToDisplayScale(scaleIdx); + final newThemePrefs = + themePreferences.copyWith(displayScale: displayScale); + final newPrefs = preferencesRepository.value + .copyWith(themePreference: newThemePrefs); + + await preferencesRepository.set(newPrefs); + switcher.changeTheme(theme: newThemePrefs.themeData()); + }); +} + +extension DisplayScaledNum on num { + double scaled(BuildContext context) { + final prefs = context.watch().state.asData?.value ?? + PreferencesRepository.instance.value; + final currentScaleIdx = + displayScaleToIndex(prefs.themePreference.displayScale); + return this * _scaleNumMult[currentScaleIdx]; + } +} + +extension DisplayScaledEdgeInsets on EdgeInsets { + EdgeInsets scaled(BuildContext context) { + final prefs = context.watch().state.asData?.value ?? + PreferencesRepository.instance.value; + final currentScaleIdx = + displayScaleToIndex(prefs.themePreference.displayScale); + return EdgeInsets.fromLTRB( + left * _scaleNumMult[currentScaleIdx], + top * _scaleNumMult[currentScaleIdx], + right * _scaleNumMult[currentScaleIdx], + bottom * _scaleNumMult[currentScaleIdx]); + } +} diff --git a/lib/theme/views/preferences/preferences.dart b/lib/theme/views/preferences/preferences.dart new file mode 100644 index 0000000..ddac4c1 --- /dev/null +++ b/lib/theme/views/preferences/preferences.dart @@ -0,0 +1,4 @@ +export 'brightness_preferences.dart'; +export 'color_preferences.dart'; +export 'display_scale_preferences.dart'; +export 'wallpaper_preferences.dart'; diff --git a/lib/theme/views/preferences/wallpaper_preferences.dart b/lib/theme/views/preferences/wallpaper_preferences.dart new file mode 100644 index 0000000..050e294 --- /dev/null +++ b/lib/theme/views/preferences/wallpaper_preferences.dart @@ -0,0 +1,25 @@ +import 'package:animated_theme_switcher/animated_theme_switcher.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_translate/flutter_translate.dart'; + +import '../../../settings/settings.dart'; +import '../../models/models.dart'; +import '../views.dart'; + +Widget buildSettingsPageWallpaperPreferences( + {required BuildContext context, required ThemeSwitcherState switcher}) { + final preferencesRepository = PreferencesRepository.instance; + final themePreferences = preferencesRepository.value.themePreference; + + return StyledCheckbox( + value: themePreferences.enableWallpaper, + label: translate('settings_page.enable_wallpaper'), + onChanged: (value) async { + final newThemePrefs = themePreferences.copyWith(enableWallpaper: value); + final newPrefs = preferencesRepository.value + .copyWith(themePreference: newThemePrefs); + + await preferencesRepository.set(newPrefs); + switcher.changeTheme(theme: newThemePrefs.themeData()); + }); +} diff --git a/lib/theme/views/responsive.dart b/lib/theme/views/responsive.dart index 0182c9f..4ce42d8 100644 --- a/lib/theme/views/responsive.dart +++ b/lib/theme/views/responsive.dart @@ -3,6 +3,10 @@ import 'package:flutter/material.dart'; final isAndroid = !kIsWeb && defaultTargetPlatform == TargetPlatform.android; final isiOS = !kIsWeb && defaultTargetPlatform == TargetPlatform.iOS; +final isMac = !kIsWeb && defaultTargetPlatform == TargetPlatform.macOS; +final isWindows = !kIsWeb && defaultTargetPlatform == TargetPlatform.windows; +final isLinux = !kIsWeb && defaultTargetPlatform == TargetPlatform.linux; + final isMobile = !kIsWeb && (defaultTargetPlatform == TargetPlatform.iOS || defaultTargetPlatform == TargetPlatform.android); diff --git a/lib/theme/views/styled_alert.dart b/lib/theme/views/styled_widgets/styled_alert.dart similarity index 99% rename from lib/theme/views/styled_alert.dart rename to lib/theme/views/styled_widgets/styled_alert.dart index 1215c84..4dec616 100644 --- a/lib/theme/views/styled_alert.dart +++ b/lib/theme/views/styled_widgets/styled_alert.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:rflutter_alert/rflutter_alert.dart'; -import '../theme.dart'; +import '../../theme.dart'; AlertStyle _alertStyle(BuildContext context) { final theme = Theme.of(context); @@ -186,6 +186,7 @@ Future showAlertWidgetModal( child: Text( translate('button.ok'), style: _buttonTextStyle(context), + softWrap: true, ), ) ], diff --git a/lib/theme/views/styled_widgets/styled_avatar.dart b/lib/theme/views/styled_widgets/styled_avatar.dart new file mode 100644 index 0000000..dde39f2 --- /dev/null +++ b/lib/theme/views/styled_widgets/styled_avatar.dart @@ -0,0 +1,77 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +import '../../theme.dart'; + +class StyledAvatar extends StatelessWidget { + const StyledAvatar({ + required String name, + required double size, + bool enabled = true, + super.key, + ImageProvider? imageProvider, + }) : _name = name, + _size = size, + _imageProvider = imageProvider, + _enabled = enabled; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scaleTheme = Theme.of(context).extension()!; + + final borderColor = scaleTheme.config.useVisualIndicators + ? scaleTheme.scheme.primaryScale.primaryText + : scaleTheme.scheme.primaryScale.subtleBorder; + final foregroundColor = !_enabled + ? scaleTheme.scheme.grayScale.primaryText + : scaleTheme.scheme.primaryScale.calloutText; + final backgroundColor = !_enabled + ? scaleTheme.scheme.grayScale.primary + : scaleTheme.scheme.primaryScale.calloutBackground; + final scaleConfig = scaleTheme.config; + final textStyle = theme.textTheme.titleLarge!.copyWith(fontSize: _size / 2); + + final abbrev = _name.split(' ').map((s) => s.isEmpty ? '' : s[0]).join(); + late final String shortname; + if (abbrev.length >= 3) { + shortname = abbrev[0] + abbrev[1] + abbrev[abbrev.length - 1]; + } else { + shortname = abbrev; + } + + return Container( + height: _size, + width: _size, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: !scaleConfig.useVisualIndicators + ? null + : Border.all( + color: borderColor, + width: 1 * (_size ~/ 16 + 1), + strokeAlign: BorderSide.strokeAlignOutside)), + child: AvatarImage( + backgroundImage: _imageProvider, + backgroundColor: scaleConfig.useVisualIndicators + ? foregroundColor + : backgroundColor, + child: Text( + shortname.isNotEmpty ? shortname : '?', + softWrap: false, + textScaler: MediaQuery.of(context).textScaler, + style: textStyle.copyWith( + color: scaleConfig.useVisualIndicators + ? backgroundColor + : foregroundColor, + ), + ).paddingAll(4.scaled(context)).fit(fit: BoxFit.scaleDown))); + } + + //////////////////////////////////////////////////////////////////////////// + final String _name; + final double _size; + final ImageProvider? _imageProvider; + final bool _enabled; +} diff --git a/lib/theme/views/option_box.dart b/lib/theme/views/styled_widgets/styled_button_box.dart similarity index 75% rename from lib/theme/views/option_box.dart rename to lib/theme/views/styled_widgets/styled_button_box.dart index 06a3293..811e01c 100644 --- a/lib/theme/views/option_box.dart +++ b/lib/theme/views/styled_widgets/styled_button_box.dart @@ -1,10 +1,10 @@ import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/material.dart'; -import '../theme.dart'; +import '../../theme.dart'; -class OptionBox extends StatelessWidget { - const OptionBox( +class StyledButtonBox extends StatelessWidget { + const StyledButtonBox( {required String instructions, required IconData buttonIcon, required String buttonText, @@ -41,12 +41,15 @@ class OptionBox extends StatelessWidget { onPressed: _onClick, child: Row(mainAxisSize: MainAxisSize.min, children: [ Icon(_buttonIcon, - size: 24, color: scale.primaryScale.appText) - .paddingLTRB(0, 8, 8, 8), + size: 24.scaled(context), + color: scale.primaryScale.appText) + .paddingLTRB(0, 8.scaled(context), + 8.scaled(context), 8.scaled(context)), Text(textAlign: TextAlign.center, _buttonText) - ])).paddingLTRB(0, 12, 0, 0).toCenter() - ]).paddingAll(12)) - .paddingLTRB(24, 0, 24, 12); + ])).paddingLTRB(0, 12.scaled(context), 0, 0).toCenter() + ]).paddingAll(12.scaled(context))) + .paddingLTRB( + 24.scaled(context), 0, 24.scaled(context), 12.scaled(context)); } final String _instructions; diff --git a/lib/theme/views/styled_widgets/styled_checkbox.dart b/lib/theme/views/styled_widgets/styled_checkbox.dart new file mode 100644 index 0000000..7eb3649 --- /dev/null +++ b/lib/theme/views/styled_widgets/styled_checkbox.dart @@ -0,0 +1,63 @@ +import 'package:async_tools/async_tools.dart'; +import 'package:awesome_extensions/awesome_extensions_flutter.dart'; +import 'package:flutter/material.dart'; + +import '../views.dart'; + +const _kStyledCheckboxChanged = 'kStyledCheckboxChanged'; + +class StyledCheckbox extends StatelessWidget { + const StyledCheckbox( + {required bool value, + required String label, + String? decoratorLabel, + Future Function(bool)? onChanged, + super.key}) + : _value = value, + _onChanged = onChanged, + _label = label, + _decoratorLabel = decoratorLabel; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + + var textStyle = textTheme.labelLarge!; + if (_onChanged == null) { + textStyle = textStyle.copyWith(color: textStyle.color!.withAlpha(127)); + } + + Widget ctrl = Row(children: [ + Transform.scale( + scale: 1.scaled(context), + child: Checkbox( + value: _value, + onChanged: _onChanged == null + ? null + : (value) { + if (value == null) { + return; + } + singleFuture((this, _kStyledCheckboxChanged), () async { + await _onChanged(value); + }); + })), + Text(_label, style: textStyle).paddingAll(4.scaled(context)), + ]); + + if (_decoratorLabel != null) { + ctrl = ctrl + .paddingLTRB(4.scaled(context), 4.scaled(context), 4.scaled(context), + 4.scaled(context)) + .decoratorLabel(context, _decoratorLabel); + } + + return ctrl; + } + + final String _label; + final String? _decoratorLabel; + final Future Function(bool)? _onChanged; + final bool _value; +} diff --git a/lib/theme/views/styled_dialog.dart b/lib/theme/views/styled_widgets/styled_dialog.dart similarity index 98% rename from lib/theme/views/styled_dialog.dart rename to lib/theme/views/styled_widgets/styled_dialog.dart index 75a0f6b..4106f1d 100644 --- a/lib/theme/views/styled_dialog.dart +++ b/lib/theme/views/styled_widgets/styled_dialog.dart @@ -2,7 +2,7 @@ import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import '../theme.dart'; +import '../../theme.dart'; class StyledDialog extends StatelessWidget { const StyledDialog({required this.title, required this.child, super.key}); diff --git a/lib/theme/views/styled_widgets/styled_dropdown.dart b/lib/theme/views/styled_widgets/styled_dropdown.dart new file mode 100644 index 0000000..3af6424 --- /dev/null +++ b/lib/theme/views/styled_widgets/styled_dropdown.dart @@ -0,0 +1,59 @@ +import 'package:async_tools/async_tools.dart'; +import 'package:awesome_extensions/awesome_extensions_flutter.dart'; +import 'package:flutter/material.dart'; + +import '../../models/models.dart'; +import '../views.dart'; + +const _kStyledDropdownChanged = 'kStyledDropdownChanged'; + +class StyledDropdown extends StatelessWidget { + const StyledDropdown( + {required List> items, + required T value, + String? decoratorLabel, + Future Function(T)? onChanged, + super.key}) + : _items = items, + _onChanged = onChanged, + _decoratorLabel = decoratorLabel, + _value = value; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scheme = theme.extension()!; + + Widget ctrl = DropdownButton( + isExpanded: true, + padding: const EdgeInsets.fromLTRB(4, 0, 4, 0).scaled(context), + focusColor: theme.focusColor, + dropdownColor: scheme.primaryScale.elementBackground, + iconEnabledColor: scheme.primaryScale.appText, + iconDisabledColor: scheme.primaryScale.appText.withAlpha(127), + items: _items, + value: _value, + style: theme.textTheme.labelLarge, + onChanged: _onChanged == null + ? null + : (value) { + if (value == null) { + return; + } + singleFuture((this, _kStyledDropdownChanged), () async { + await _onChanged(value); + }); + }); + if (_decoratorLabel != null) { + ctrl = ctrl + .paddingLTRB(0, 4.scaled(context), 0, 4.scaled(context)) + .decoratorLabel(context, _decoratorLabel); + } + return ctrl; + } + + final List> _items; + final String? _decoratorLabel; + final Future Function(T)? _onChanged; + final T _value; +} diff --git a/lib/theme/views/styled_scaffold.dart b/lib/theme/views/styled_widgets/styled_scaffold.dart similarity index 97% rename from lib/theme/views/styled_scaffold.dart rename to lib/theme/views/styled_widgets/styled_scaffold.dart index 4fc803f..82f27f5 100644 --- a/lib/theme/views/styled_scaffold.dart +++ b/lib/theme/views/styled_widgets/styled_scaffold.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../theme.dart'; +import '../../theme.dart'; class StyledScaffold extends StatelessWidget { const StyledScaffold({required this.appBar, required this.body, super.key}); diff --git a/lib/theme/views/slider_tile.dart b/lib/theme/views/styled_widgets/styled_slide_tile.dart similarity index 75% rename from lib/theme/views/slider_tile.dart rename to lib/theme/views/styled_widgets/styled_slide_tile.dart index 8e5f178..43b3fd8 100644 --- a/lib/theme/views/slider_tile.dart +++ b/lib/theme/views/styled_widgets/styled_slide_tile.dart @@ -2,10 +2,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; -import '../theme.dart'; +import '../../theme.dart'; -class SliderTileAction { - const SliderTileAction({ +class SlideTileAction { + const SlideTileAction({ required this.actionScale, required this.onPressed, this.key, @@ -20,8 +20,8 @@ class SliderTileAction { final SlidableActionCallback? onPressed; } -class SliderTile extends StatelessWidget { - const SliderTile( +class StyledSlideTile extends StatelessWidget { + const StyledSlideTile( {required this.disabled, required this.selected, required this.tileScale, @@ -38,8 +38,8 @@ class SliderTile extends StatelessWidget { final bool disabled; final bool selected; final ScaleKind tileScale; - final List endActions; - final List startActions; + final List endActions; + final List startActions; final GestureTapCallback? onTap; final GestureTapCallback? onDoubleTap; final Widget? leading; @@ -54,8 +54,8 @@ class SliderTile extends StatelessWidget { ..add(DiagnosticsProperty('disabled', disabled)) ..add(DiagnosticsProperty('selected', selected)) ..add(DiagnosticsProperty('tileScale', tileScale)) - ..add(IterableProperty('endActions', endActions)) - ..add(IterableProperty('startActions', startActions)) + ..add(IterableProperty('endActions', endActions)) + ..add(IterableProperty('startActions', startActions)) ..add(ObjectFlagProperty.has('onTap', onTap)) ..add(DiagnosticsProperty('leading', leading)) ..add(StringProperty('title', title)) @@ -66,7 +66,6 @@ class SliderTile extends StatelessWidget { } @override - // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { final theme = Theme.of(context); final scaleTheme = theme.extension()!; @@ -91,12 +90,13 @@ class SliderTile extends StatelessWidget { selected: true, scaleKind: a.actionScale); return SlidableAction( - onPressed: disabled ? null : a.onPressed, - backgroundColor: scaleActionTheme.backgroundColor, - foregroundColor: scaleActionTheme.textColor, - icon: subtitle.isEmpty ? a.icon : null, - label: a.label, - padding: const EdgeInsets.all(2)); + onPressed: disabled ? null : a.onPressed, + backgroundColor: scaleActionTheme.backgroundColor, + foregroundColor: scaleActionTheme.textColor, + icon: subtitle.isEmpty ? a.icon : null, + label: a.label, + padding: const EdgeInsets.all(2).scaled(context), + ); }).toList()), startActionPane: startActions.isEmpty ? null @@ -109,17 +109,18 @@ class SliderTile extends StatelessWidget { scaleKind: a.actionScale); return SlidableAction( - onPressed: disabled ? null : a.onPressed, - backgroundColor: scaleActionTheme.backgroundColor, - foregroundColor: scaleActionTheme.textColor, - icon: subtitle.isEmpty ? a.icon : null, - label: a.label, - padding: const EdgeInsets.all(2)); + onPressed: disabled ? null : a.onPressed, + backgroundColor: scaleActionTheme.backgroundColor, + foregroundColor: scaleActionTheme.textColor, + icon: subtitle.isEmpty ? a.icon : null, + label: a.label, + padding: const EdgeInsets.all(2).scaled(context), + ); }).toList()), child: Padding( padding: scaleTheme.config.useVisualIndicators ? EdgeInsets.zero - : const EdgeInsets.fromLTRB(0, 2, 0, 2), + : const EdgeInsets.fromLTRB(0, 2, 0, 2).scaled(context), child: GestureDetector( onDoubleTap: onDoubleTap, child: ListTile( @@ -131,7 +132,8 @@ class SliderTile extends StatelessWidget { softWrap: false, ), subtitle: subtitle.isNotEmpty ? Text(subtitle) : null, - minTileHeight: 52, + contentPadding: const EdgeInsets.fromLTRB(8, 4, 8, 4) + .scaled(context), iconColor: scaleTileTheme.textColor, textColor: scaleTileTheme.textColor, leading: diff --git a/lib/theme/views/styled_widgets/styled_slider.dart b/lib/theme/views/styled_widgets/styled_slider.dart new file mode 100644 index 0000000..a0c7259 --- /dev/null +++ b/lib/theme/views/styled_widgets/styled_slider.dart @@ -0,0 +1,79 @@ +import 'package:async_tools/async_tools.dart'; +import 'package:awesome_extensions/awesome_extensions_flutter.dart'; +import 'package:flutter/material.dart'; + +import '../../models/models.dart'; +import '../views.dart'; + +const _kStyledSliderChanged = 'kStyledSliderChanged'; + +class StyledSlider extends StatelessWidget { + const StyledSlider( + {required double value, + String? label, + String? decoratorLabel, + Future Function(double)? onChanged, + Widget? leftWidget, + Widget? rightWidget, + double min = 0, + double max = 1, + int? divisions, + super.key}) + : _value = value, + _onChanged = onChanged, + _leftWidget = leftWidget, + _rightWidget = rightWidget, + _min = min, + _max = max, + _divisions = divisions, + _label = label, + _decoratorLabel = decoratorLabel; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + + Widget ctrl = Row(children: [ + if (_leftWidget != null) _leftWidget, + Slider( + activeColor: scale.scheme.primaryScale.border, + inactiveColor: scale.scheme.primaryScale.subtleBorder, + secondaryActiveColor: scale.scheme.secondaryScale.border, + value: _value, + min: _min, + max: _max, + divisions: _divisions, + label: _label, + thumbColor: scale.scheme.primaryScale.appText, + overlayColor: + WidgetStateColor.resolveWith((ws) => theme.focusColor), + onChanged: _onChanged == null + ? null + : (value) { + singleFuture((this, _kStyledSliderChanged), () async { + await _onChanged(value); + }); + }) + .expanded(), + if (_rightWidget != null) _rightWidget, + ]); + if (_decoratorLabel != null) { + ctrl = ctrl + .paddingLTRB(4.scaled(context), 4.scaled(context), 4.scaled(context), + 4.scaled(context)) + .decoratorLabel(context, _decoratorLabel); + } + return ctrl; + } + + final String? _label; + final String? _decoratorLabel; + final Future Function(double)? _onChanged; + final double _value; + final Widget? _leftWidget; + final Widget? _rightWidget; + final double _min; + final double _max; + final int? _divisions; +} diff --git a/lib/theme/views/styled_widgets/styled_widgets.dart b/lib/theme/views/styled_widgets/styled_widgets.dart new file mode 100644 index 0000000..ae45d59 --- /dev/null +++ b/lib/theme/views/styled_widgets/styled_widgets.dart @@ -0,0 +1,8 @@ +export 'styled_alert.dart'; +export 'styled_avatar.dart'; +export 'styled_checkbox.dart'; +export 'styled_dialog.dart'; +export 'styled_dropdown.dart'; +export 'styled_scaffold.dart'; +export 'styled_slide_tile.dart'; +export 'styled_slider.dart'; diff --git a/lib/theme/views/views.dart b/lib/theme/views/views.dart index 88f4a4a..1144440 100644 --- a/lib/theme/views/views.dart +++ b/lib/theme/views/views.dart @@ -1,16 +1,10 @@ -export 'avatar_widget.dart'; -export 'brightness_preferences.dart'; -export 'color_preferences.dart'; export 'enter_password.dart'; export 'enter_pin.dart'; -export 'option_box.dart'; export 'pop_control.dart'; +export 'preferences/preferences.dart'; export 'recovery_key_widget.dart'; export 'responsive.dart'; export 'scanner_error_widget.dart'; -export 'slider_tile.dart'; -export 'styled_alert.dart'; -export 'styled_dialog.dart'; -export 'styled_scaffold.dart'; -export 'wallpaper_preferences.dart'; +export 'styled_widgets/styled_button_box.dart'; +export 'styled_widgets/styled_widgets.dart'; export 'widget_helpers.dart'; diff --git a/lib/theme/views/wallpaper_preferences.dart b/lib/theme/views/wallpaper_preferences.dart deleted file mode 100644 index f9ae94c..0000000 --- a/lib/theme/views/wallpaper_preferences.dart +++ /dev/null @@ -1,37 +0,0 @@ -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 formFieldEnableWallpaper = 'enable_wallpaper'; - -Widget buildSettingsPageWallpaperPreferences( - {required BuildContext context, - required void Function() onChanged, - required ThemeSwitcherState switcher}) { - final preferencesRepository = PreferencesRepository.instance; - final themePreferences = preferencesRepository.value.themePreference; - final theme = Theme.of(context); - final textTheme = theme.textTheme; - - return FormBuilderCheckbox( - name: formFieldEnableWallpaper, - title: Text(translate('settings_page.enable_wallpaper'), - style: textTheme.labelMedium), - initialValue: themePreferences.enableWallpaper, - onChanged: (value) async { - if (value != null) { - final newThemePrefs = - themePreferences.copyWith(enableWallpaper: value); - final newPrefs = preferencesRepository.value - .copyWith(themePreference: newThemePrefs); - - await preferencesRepository.set(newPrefs); - switcher.changeTheme(theme: newThemePrefs.themeData()); - onChanged(); - } - }); -} diff --git a/lib/veilid_processor/views/developer.dart b/lib/veilid_processor/views/developer.dart index d749a9c..c03b6bf 100644 --- a/lib/veilid_processor/views/developer.dart +++ b/lib/veilid_processor/views/developer.dart @@ -222,13 +222,16 @@ class _DeveloperPageState extends State { return Scaffold( backgroundColor: scale.primaryScale.border, appBar: DefaultAppBar( + context: context, title: Text(translate('developer.title')), leading: IconButton( + iconSize: 24.scaled(context), icon: Icon(Icons.arrow_back, color: scale.primaryScale.borderText), onPressed: () => GoRouterHelper(context).pop(), ), actions: [ IconButton( + iconSize: 24.scaled(context), icon: const Icon(Icons.copy), color: scale.primaryScale.borderText, disabledColor: scale.primaryScale.borderText.withAlpha(0x3F), @@ -238,6 +241,7 @@ class _DeveloperPageState extends State { await copySelection(context); }), IconButton( + iconSize: 24.scaled(context), icon: const Icon(Icons.copy_all), color: scale.primaryScale.borderText, disabledColor: scale.primaryScale.borderText.withAlpha(0x3F), @@ -245,6 +249,7 @@ class _DeveloperPageState extends State { await copyAll(context); }), IconButton( + iconSize: 24.scaled(context), icon: const Icon(Icons.clear_all), color: scale.primaryScale.borderText, disabledColor: scale.primaryScale.borderText.withAlpha(0x3F), @@ -259,7 +264,7 @@ class _DeveloperPageState extends State { } }), SizedBox.fromSize( - size: const Size(140, 48), + size: Size(140.scaled(context), 48), child: CustomDropdown( items: _logLevelDropdownItems, initialItem: _logLevelDropdownItems @@ -300,6 +305,7 @@ class _DeveloperPageState extends State { Image.asset('assets/images/ellet.png'), TerminalView(globalDebugTerminal, textStyle: kDefaultTerminalStyle, + textScaler: TextScaler.noScaling, controller: _terminalController, keyboardType: TextInputType.none, backgroundOpacity: _showEllet ? 0.75 : 1.0, diff --git a/lib/veilid_processor/views/signal_strength_meter.dart b/lib/veilid_processor/views/signal_strength_meter.dart index 5385bb1..44e3cb6 100644 --- a/lib/veilid_processor/views/signal_strength_meter.dart +++ b/lib/veilid_processor/views/signal_strength_meter.dart @@ -19,7 +19,7 @@ class SignalStrengthMeterWidget extends StatelessWidget { final theme = Theme.of(context); final scale = theme.extension()!; - const iconSize = 16.0; + final iconSize = 16.0.scaled(context); return BlocBuilder>(builder: (context, state) { From 68e8d7fd390b4b0ed32ce886250a67a072e64f42 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 27 May 2025 16:43:38 -0400 Subject: [PATCH 251/270] More UI Cleanup --- lib/account_manager/views/profile_widget.dart | 5 +- lib/chat/views/chat_component_widget.dart | 4 +- lib/chat_list/views/chat_list_widget.dart | 50 +-- lib/contacts/views/contacts_browser.dart | 104 +++--- lib/layout/default_app_bar.dart | 2 +- lib/layout/home/home_account_ready.dart | 8 +- lib/layout/home/home_screen.dart | 57 ++-- .../views/notifications_preferences.dart | 303 ++++++++---------- lib/settings/settings_page.dart | 5 +- lib/theme/models/scale_theme/scale_theme.dart | 6 + .../display_scale_preferences.dart | 18 ++ .../views/styled_widgets/styled_checkbox.dart | 6 +- .../views/styled_widgets/styled_scaffold.dart | 2 +- .../styled_widgets/styled_slide_tile.dart | 2 +- lib/theme/views/widget_helpers.dart | 1 + pubspec.lock | 8 - pubspec.yaml | 1 - 17 files changed, 281 insertions(+), 301 deletions(-) diff --git a/lib/account_manager/views/profile_widget.dart b/lib/account_manager/views/profile_widget.dart index c026856..af7cf30 100644 --- a/lib/account_manager/views/profile_widget.dart +++ b/lib/account_manager/views/profile_widget.dart @@ -20,7 +20,6 @@ class ProfileWidget extends StatelessWidget { // @override - // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { final theme = Theme.of(context); final scale = theme.extension()!; @@ -54,7 +53,7 @@ class ProfileWidget extends StatelessWidget { ? scale.primaryScale.border : scale.primaryScale.borderText), textAlign: TextAlign.left, - ).paddingAll(8), + ).paddingAll(8.scaled(context)), if (_profile.pronouns.isNotEmpty && _showPronouns) Text('(${_profile.pronouns})', textAlign: TextAlign.right, @@ -62,7 +61,7 @@ class ProfileWidget extends StatelessWidget { color: scaleConfig.preferBorders ? scale.primaryScale.border : scale.primaryScale.primary)) - .paddingAll(8), + .paddingAll(8.scaled(context)), const Spacer() ]), ); diff --git a/lib/chat/views/chat_component_widget.dart b/lib/chat/views/chat_component_widget.dart index 8106a48..8cb4edc 100644 --- a/lib/chat/views/chat_component_widget.dart +++ b/lib/chat/views/chat_component_widget.dart @@ -161,7 +161,7 @@ class _ChatComponentWidgetState extends State { return Column( children: [ Container( - height: 48, + height: 40.scaledNoShrink(context), decoration: BoxDecoration( color: scale.border, ), @@ -177,7 +177,7 @@ class _ChatComponentWidgetState extends State { )), const Spacer(), IconButton( - iconSize: 24, + iconSize: 24.scaledNoShrink(context), icon: Icon(Icons.close, color: scale.borderText), onPressed: widget._onClose) .paddingLTRB(0, 0, 8, 0) diff --git a/lib/chat_list/views/chat_list_widget.dart b/lib/chat_list/views/chat_list_widget.dart index cce416c..04720e8 100644 --- a/lib/chat_list/views/chat_list_widget.dart +++ b/lib/chat_list/views/chat_list_widget.dart @@ -63,30 +63,34 @@ class ChatListWidget extends StatelessWidget { title: translate('chat_list.chats'), child: (chatList.isEmpty) ? const SizedBox.expand(child: EmptyChatListWidget()) - : SearchableList( - initialList: chatList.map((x) => x.value).toList(), - itemBuilder: (c) { - switch (c.whichKind()) { - case proto.Chat_Kind.direct: - return _itemBuilderDirect( - c.direct, - contactMap, - ); - case proto.Chat_Kind.group: - return const Text( - 'group chats not yet supported!'); - case proto.Chat_Kind.notSet: - throw StateError('unknown chat kind'); - } + : TapRegion( + onTapOutside: (_) { + FocusScope.of(context).unfocus(); }, - filter: (value) => - _itemFilter(contactMap, chatList, value), - searchFieldPadding: - const EdgeInsets.fromLTRB(0, 0, 0, 4), - inputDecoration: InputDecoration( - labelText: translate('chat_list.search'), - ), - ).paddingAll(8), + child: SearchableList( + initialList: chatList.map((x) => x.value).toList(), + itemBuilder: (c) { + switch (c.whichKind()) { + case proto.Chat_Kind.direct: + return _itemBuilderDirect( + c.direct, + contactMap, + ); + case proto.Chat_Kind.group: + return const Text( + 'group chats not yet supported!'); + case proto.Chat_Kind.notSet: + throw StateError('unknown chat kind'); + } + }, + filter: (value) => + _itemFilter(contactMap, chatList, value), + searchFieldPadding: + const EdgeInsets.fromLTRB(0, 0, 0, 4), + inputDecoration: InputDecoration( + labelText: translate('chat_list.search'), + ), + )).paddingAll(8), ))) .paddingLTRB(8, 0, 8, 8); }); diff --git a/lib/contacts/views/contacts_browser.dart b/lib/contacts/views/contacts_browser.dart index 0504a7a..10b207f 100644 --- a/lib/contacts/views/contacts_browser.dart +++ b/lib/contacts/views/contacts_browser.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:searchable_listview/searchable_listview.dart'; -import 'package:star_menu/star_menu.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../contact_invitation/contact_invitation.dart'; @@ -90,75 +89,62 @@ class _ContactsBrowserState extends State final menuBorderColor = scaleScheme.primaryScale.hoverBorder; - final menuParams = StarMenuParameters( - shape: MenuShape.linear, - centerOffset: Offset(0, 64.scaled(context)), - boundaryBackground: BoundaryBackground( - color: menuBackgroundColor, - decoration: ShapeDecoration( - color: menuBackgroundColor, - shape: RoundedRectangleBorder( - side: scaleConfig.useVisualIndicators - ? BorderSide( - width: 2, color: menuBorderColor, strokeAlign: 0) - : BorderSide.none, - borderRadius: BorderRadius.circular( - 8 * scaleConfig.borderRadiusScale))))); - - ElevatedButton makeMenuButton( + PopupMenuEntry makeMenuButton( {required IconData iconData, required String text, - required void Function()? onPressed}) => - ElevatedButton.icon( - onPressed: onPressed, - icon: Icon( - iconData, - size: 32.scaled(context), - ).paddingSTEB(0, 8.scaled(context), 0, 8.scaled(context)), - label: Text( - text, - textScaler: MediaQuery.of(context).textScaler, - maxLines: 2, - textAlign: TextAlign.center, - ).paddingSTEB(0, 8.scaled(context), 0, 8.scaled(context))); + void Function()? onTap}) => + PopupMenuItem( + onTap: onTap, + child: DecoratedBox( + decoration: ShapeDecoration( + color: menuBackgroundColor, + shape: RoundedRectangleBorder( + side: scaleConfig.useVisualIndicators + ? BorderSide( + width: 2, + color: menuBorderColor, + strokeAlign: 0) + : BorderSide.none, + borderRadius: BorderRadius.circular( + 8 * scaleConfig.borderRadiusScale))), + child: Row(spacing: 4.scaled(context), children: [ + Icon(iconData, size: 32.scaled(context)), + Text( + text, + textScaler: MediaQuery.of(context).textScaler, + maxLines: 2, + textAlign: TextAlign.center, + ) + ]).paddingAll(4.scaled(context))) + .paddingLTRB(0, 2.scaled(context), 0, 2.scaled(context))); final inviteMenuItems = [ makeMenuButton( - iconData: Icons.paste, - text: translate('add_contact_sheet.paste_invite'), - onPressed: () async { - _invitationMenuController.closeMenu!(); - await PasteInvitationDialog.show(context); + iconData: Icons.contact_page, + text: translate('add_contact_sheet.create_invite'), + onTap: () async { + await CreateInvitationDialog.show(context); }), makeMenuButton( iconData: Icons.qr_code_scanner, text: translate('add_contact_sheet.scan_invite'), - onPressed: () async { - _invitationMenuController.closeMenu!(); + onTap: () async { await ScanInvitationDialog.show(context); - }).paddingLTRB(0, 0, 0, 8.scaled(context)), + }), makeMenuButton( - iconData: Icons.contact_page, - text: translate('add_contact_sheet.create_invite'), - onPressed: () async { - _invitationMenuController.closeMenu!(); - await CreateInvitationDialog.show(context); - }).paddingLTRB(0, 0, 0, 8.scaled(context)), + iconData: Icons.paste, + text: translate('add_contact_sheet.paste_invite'), + onTap: () async { + await PasteInvitationDialog.show(context); + }), ]; - return StarMenu( - items: inviteMenuItems, - onItemTapped: (_, controller) { - controller.closeMenu!(); - }, - controller: _invitationMenuController, - params: menuParams, - child: IconButton( - onPressed: () {}, - iconSize: 24.scaled(context), - icon: Icon(Icons.person_add, color: menuIconColor), - tooltip: translate('add_contact_sheet.add_contact')), - ); + return PopupMenuButton( + itemBuilder: (_) => inviteMenuItems, + menuPadding: const EdgeInsets.symmetric(vertical: 4).scaled(context), + tooltip: translate('add_contact_sheet.add_contact'), + child: Icon( + size: 32.scaled(context), Icons.person_add, color: menuIconColor)); } @override @@ -253,12 +239,11 @@ class _ContactsBrowserState extends State text: translate('contact_list.loading_contacts')) : const EmptyContactListWidget(), defaultSuffixIconColor: scale.primaryScale.border, - closeKeyboardWhenScrolling: true, searchFieldEnabled: contactList != null, inputDecoration: InputDecoration(labelText: translate('contact_list.search')), secondaryWidget: buildInvitationButton(context) - .paddingLTRB(4.scaled(context), 0, 0, 0)) + .paddingLTRB(8.scaled(context), 0, 0, 0)) .expanded() ]); } @@ -276,5 +261,4 @@ class _ContactsBrowserState extends State } //////////////////////////////////////////////////////////////////////////// - final _invitationMenuController = StarMenuController(); } diff --git a/lib/layout/default_app_bar.dart b/lib/layout/default_app_bar.dart index 7742dad..d13fec9 100644 --- a/lib/layout/default_app_bar.dart +++ b/lib/layout/default_app_bar.dart @@ -13,7 +13,7 @@ class DefaultAppBar extends AppBar { Widget? leading, super.actions}) : super( - toolbarHeight: 40.scaled(context), + toolbarHeight: 48.scaled(context), leadingWidth: 40.scaled(context), leading: leading ?? Container( diff --git a/lib/layout/home/home_account_ready.dart b/lib/layout/home/home_account_ready.dart index 5ae1180..50a43c8 100644 --- a/lib/layout/home/home_account_ready.dart +++ b/lib/layout/home/home_account_ready.dart @@ -31,9 +31,9 @@ class _HomeAccountReadyState extends State { return AspectRatio( aspectRatio: 1, child: IconButton( - icon: const Icon( + icon: Icon( + size: 32.scaled(context), Icons.menu, - applyTextScaling: true, ), color: scaleConfig.preferBorders ? scale.primaryScale.border @@ -70,9 +70,9 @@ class _HomeAccountReadyState extends State { return AspectRatio( aspectRatio: 1, child: IconButton( - icon: const Icon( + icon: Icon( + size: 32.scaled(context), Icons.contacts, - applyTextScaling: true, ), color: scaleConfig.preferBorders ? scale.primaryScale.border diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index e774790..c9f1ecb 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -5,7 +5,6 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart'; -import 'package:keyboard_avoider/keyboard_avoider.dart'; import 'package:provider/provider.dart'; import 'package:transitioned_indexed_stack/transitioned_indexed_stack.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -207,36 +206,34 @@ class HomeScreenState extends State .indexWhere((x) => x.superIdentity.recordKey == activeLocalAccount); final canClose = activeIndex != -1; - final drawer = ZoomDrawer( - controller: _zoomDrawerController, - menuScreen: Builder(builder: (context) { - final zoomDrawer = ZoomDrawer.of(context); - zoomDrawer!.stateNotifier.addListener(() { - if (zoomDrawer.isOpen()) { - FocusManager.instance.primaryFocus?.unfocus(); - } - }); - return const DrawerMenu(); - }), - mainScreen: Provider.value( - value: _zoomDrawerController, - child: Builder(builder: _buildAccountPageView)), - borderRadius: 0, - angle: 0, - openCurve: Curves.fastEaseInToSlowEaseOut, - closeCurve: Curves.fastEaseInToSlowEaseOut, - menuScreenTapClose: canClose, - mainScreenTapClose: canClose, - disableDragGesture: !canClose, - mainScreenScale: .25, - slideWidth: min(360, MediaQuery.of(context).size.width * 0.9), - ); + final drawer = Scaffold( + backgroundColor: Colors.transparent, + body: ZoomDrawer( + controller: _zoomDrawerController, + menuScreen: Builder(builder: (context) { + final zoomDrawer = ZoomDrawer.of(context); + zoomDrawer!.stateNotifier.addListener(() { + if (zoomDrawer.isOpen()) { + FocusManager.instance.primaryFocus?.unfocus(); + } + }); + return const DrawerMenu(); + }), + mainScreen: Provider.value( + value: _zoomDrawerController, + child: Builder(builder: _buildAccountPageView)), + borderRadius: 0, + angle: 0, + openCurve: Curves.fastEaseInToSlowEaseOut, + closeCurve: Curves.fastEaseInToSlowEaseOut, + menuScreenTapClose: canClose, + mainScreenTapClose: canClose, + disableDragGesture: !canClose, + mainScreenScale: .25, + slideWidth: min(360, MediaQuery.of(context).size.width * 0.9), + )); - final drawerWithAvoider = - isWeb ? drawer : KeyboardAvoider(curve: Curves.ease, child: drawer); - - return DefaultTextStyle( - style: theme.textTheme.bodySmall!, child: drawerWithAvoider); + return DefaultTextStyle(style: theme.textTheme.bodySmall!, child: drawer); } //////////////////////////////////////////////////////////////////////////// diff --git a/lib/notifications/views/notifications_preferences.dart b/lib/notifications/views/notifications_preferences.dart index 8918fa9..ba06699 100644 --- a/lib/notifications/views/notifications_preferences.dart +++ b/lib/notifications/views/notifications_preferences.dart @@ -69,7 +69,7 @@ Widget buildSettingsPageNotificationPreferences( softWrap: false, style: textTheme.labelMedium, textAlign: TextAlign.center, - ))); + ).fit(fit: BoxFit.scaleDown))); } return out; } @@ -108,170 +108,147 @@ Widget buildSettingsPageNotificationPreferences( return out; } - return InputDecorator( - decoration: InputDecoration( - labelText: translate('settings_page.notifications'), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8 * scaleConfig.borderRadiusScale), - borderSide: BorderSide(width: 2, color: scale.primaryScale.border), - ), - ), - child: Column(mainAxisSize: MainAxisSize.min, children: [ - // Display Beta Warning - StyledCheckbox( - label: translate('settings_page.display_beta_warning'), - value: notificationsPreference.displayBetaWarning, - onChanged: (value) async { - final newNotificationsPreference = - notificationsPreference.copyWith(displayBetaWarning: value); - - await updatePreferences(newNotificationsPreference); - }), - // Enable Badge - StyledCheckbox( - label: translate('settings_page.enable_badge'), - value: notificationsPreference.enableBadge, - onChanged: (value) async { - final newNotificationsPreference = - notificationsPreference.copyWith(enableBadge: value); - await updatePreferences(newNotificationsPreference); - }), - // Enable Notifications - StyledCheckbox( - label: translate('settings_page.enable_notifications'), - value: notificationsPreference.enableNotifications, - onChanged: (value) async { - final newNotificationsPreference = - notificationsPreference.copyWith(enableNotifications: value); - await updatePreferences(newNotificationsPreference); - }), - StyledDropdown( - items: messageNotificationContentItems(), - value: notificationsPreference.messageNotificationContent, - decoratorLabel: translate('settings_page.message_notification_content'), - onChanged: !notificationsPreference.enableNotifications - ? null - : (value) async { - final newNotificationsPreference = notificationsPreference - .copyWith(messageNotificationContent: value); - await updatePreferences(newNotificationsPreference); - }, - ).paddingLTRB(0, 4.scaled(context), 0, 4.scaled(context)), - - // Notifications - Table( - defaultVerticalAlignment: TableCellVerticalAlignment.middle, + // Invitation accepted + Widget notificationSettingsItem( + {required String title, + required bool notificationsEnabled, + NotificationMode? deliveryValue, + SoundEffect? soundValue, + Future Function(NotificationMode)? onNotificationModeChanged, + Future Function(SoundEffect)? onSoundChanged}) => + Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.scaled(context), children: [ - TableRow(children: [ - Text(translate('settings_page.event'), - textAlign: TextAlign.center, - style: textTheme.titleMedium!.copyWith( - color: scale.primaryScale.border, - decorationColor: scale.primaryScale.border, - decoration: TextDecoration.underline)) - .paddingAll(8.scaled(context)), - Text(translate('settings_page.delivery'), - textAlign: TextAlign.center, - style: textTheme.titleMedium!.copyWith( - color: scale.primaryScale.border, - decorationColor: scale.primaryScale.border, - decoration: TextDecoration.underline)) - .paddingAll(8.scaled(context)), - Text(translate('settings_page.sound'), - textAlign: TextAlign.center, - style: textTheme.titleMedium!.copyWith( - color: scale.primaryScale.border, - decorationColor: scale.primaryScale.border, - decoration: TextDecoration.underline)) - .paddingAll(8.scaled(context)), - ]), - TableRow(children: [ - // Invitation accepted - Text( - textAlign: TextAlign.right, - translate('settings_page.invitation_accepted')) - .paddingAll(4.scaled(context)), - StyledDropdown( - items: notificationModeItems(), - value: notificationsPreference.onInvitationAcceptedMode, - onChanged: !notificationsPreference.enableNotifications - ? null - : (value) async { - final newNotificationsPreference = - notificationsPreference.copyWith( - onInvitationAcceptedMode: value); - await updatePreferences(newNotificationsPreference); - }, - ).paddingAll(4.scaled(context)), - StyledDropdown( - items: soundEffectItems(), - value: notificationsPreference.onInvitationAcceptedSound, - onChanged: !notificationsPreference.enableNotifications - ? null - : (value) async { - final newNotificationsPreference = - notificationsPreference.copyWith( - onInvitationAcceptedSound: value); - await updatePreferences(newNotificationsPreference); - }, - ).paddingLTRB( - 4.scaled(context), 4.scaled(context), 0, 4.scaled(context)) - ]), + Text('$title:', style: textTheme.titleMedium), + Wrap( + spacing: 8.scaled(context), // gap between adjacent chips + runSpacing: 8.scaled(context), // gap between lines + children: [ + if (deliveryValue != null) + IntrinsicWidth( + child: StyledDropdown( + decoratorLabel: translate('settings_page.delivery'), + items: notificationModeItems(), + value: deliveryValue, + onChanged: !notificationsEnabled + ? null + : onNotificationModeChanged, + )), + if (soundValue != null) + IntrinsicWidth( + child: StyledDropdown( + decoratorLabel: translate('settings_page.sound'), + items: soundEffectItems(), + value: soundValue, + onChanged: !notificationsEnabled ? null : onSoundChanged, + )) + ]) + ]).paddingAll(4.scaled(context)); + + return InputDecorator( + decoration: InputDecoration( + labelText: translate('settings_page.notifications'), + border: OutlineInputBorder( + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale), + borderSide: BorderSide(width: 2, color: scale.primaryScale.border), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.scaled(context), + children: [ + // Display Beta Warning + StyledCheckbox( + label: translate('settings_page.display_beta_warning'), + value: notificationsPreference.displayBetaWarning, + onChanged: (value) async { + final newNotificationsPreference = notificationsPreference + .copyWith(displayBetaWarning: value); + + await updatePreferences(newNotificationsPreference); + }), + // Enable Badge + StyledCheckbox( + label: translate('settings_page.enable_badge'), + value: notificationsPreference.enableBadge, + onChanged: (value) async { + final newNotificationsPreference = + notificationsPreference.copyWith(enableBadge: value); + await updatePreferences(newNotificationsPreference); + }), + // Enable Notifications + StyledCheckbox( + label: translate('settings_page.enable_notifications'), + value: notificationsPreference.enableNotifications, + onChanged: (value) async { + final newNotificationsPreference = notificationsPreference + .copyWith(enableNotifications: value); + await updatePreferences(newNotificationsPreference); + }), + StyledDropdown( + items: messageNotificationContentItems(), + value: notificationsPreference.messageNotificationContent, + decoratorLabel: + translate('settings_page.message_notification_content'), + onChanged: !notificationsPreference.enableNotifications + ? null + : (value) async { + final newNotificationsPreference = notificationsPreference + .copyWith(messageNotificationContent: value); + await updatePreferences(newNotificationsPreference); + }, + ).paddingAll(4.scaled(context)), + + // Notifications + + // Invitation accepted + notificationSettingsItem( + title: translate('settings_page.invitation_accepted'), + notificationsEnabled: + notificationsPreference.enableNotifications, + deliveryValue: notificationsPreference.onInvitationAcceptedMode, + soundValue: notificationsPreference.onInvitationAcceptedSound, + onNotificationModeChanged: (value) async { + final newNotificationsPreference = notificationsPreference + .copyWith(onInvitationAcceptedMode: value); + await updatePreferences(newNotificationsPreference); + }, + onSoundChanged: (value) async { + final newNotificationsPreference = notificationsPreference + .copyWith(onInvitationAcceptedSound: value); + await updatePreferences(newNotificationsPreference); + }), + // Message received - TableRow(children: [ - Text( - textAlign: TextAlign.right, - translate('settings_page.message_received')) - .paddingAll(4.scaled(context)), - StyledDropdown( - items: notificationModeItems(), - value: notificationsPreference.onMessageReceivedMode, - onChanged: !notificationsPreference.enableNotifications - ? null - : (value) async { - final newNotificationsPreference = - notificationsPreference.copyWith( - onMessageReceivedMode: value); - await updatePreferences(newNotificationsPreference); - }, - ).paddingAll(4), - StyledDropdown( - items: soundEffectItems(), - value: notificationsPreference.onMessageReceivedSound, - onChanged: !notificationsPreference.enableNotifications - ? null - : (value) async { - final newNotificationsPreference = - notificationsPreference.copyWith( - onMessageReceivedSound: value); - await updatePreferences(newNotificationsPreference); - }, - ).paddingLTRB( - 4.scaled(context), 4.scaled(context), 0, 4.scaled(context)) - ]), + notificationSettingsItem( + title: translate('settings_page.message_received'), + notificationsEnabled: + notificationsPreference.enableNotifications, + deliveryValue: notificationsPreference.onMessageReceivedMode, + soundValue: notificationsPreference.onMessageReceivedSound, + onNotificationModeChanged: (value) async { + final newNotificationsPreference = notificationsPreference + .copyWith(onMessageReceivedMode: value); + await updatePreferences(newNotificationsPreference); + }, + onSoundChanged: (value) async { + final newNotificationsPreference = notificationsPreference + .copyWith(onMessageReceivedSound: value); + await updatePreferences(newNotificationsPreference); + }), // Message sent - TableRow(children: [ - Text( - textAlign: TextAlign.right, - translate('settings_page.message_sent')) - .paddingAll(4.scaled(context)), - const SizedBox.shrink(), - StyledDropdown( - items: soundEffectItems(), - value: notificationsPreference.onMessageSentSound, - onChanged: !notificationsPreference.enableNotifications - ? null - : (value) async { - final newNotificationsPreference = - notificationsPreference.copyWith( - onMessageSentSound: value); - await updatePreferences(newNotificationsPreference); - }, - ).paddingLTRB( - 4.scaled(context), 4.scaled(context), 0, 4.scaled(context)) - ]), - ]) - ]).paddingAll(8.scaled(context)), - ); + notificationSettingsItem( + title: translate('settings_page.message_sent'), + notificationsEnabled: + notificationsPreference.enableNotifications, + soundValue: notificationsPreference.onMessageSentSound, + onSoundChanged: (value) async { + final newNotificationsPreference = notificationsPreference + .copyWith(onMessageSentSound: value); + await updatePreferences(newNotificationsPreference); + }), + ]).paddingAll(4.scaled(context))); } diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart index 810b20e..bb6ee3e 100644 --- a/lib/settings/settings_page.dart +++ b/lib/settings/settings_page.dart @@ -23,7 +23,7 @@ class SettingsPage extends StatelessWidget { title: Text(translate('settings_page.titlebar')), leading: IconButton( iconSize: 24.scaled(context), - icon: Icon(Icons.arrow_back), + icon: const Icon(Icons.arrow_back), onPressed: () => GoRouterHelper(context).pop(), ), actions: [ @@ -56,7 +56,6 @@ class SettingsPage extends StatelessWidget { .map((x) => x.paddingLTRB(0, 0, 0, 8.scaled(context))) .toList(), ).paddingSymmetric(vertical: 4.scaled(context)), - ).paddingSymmetric( - horizontal: 8.scaled(context), vertical: 8.scaled(context)), + ), )); } diff --git a/lib/theme/models/scale_theme/scale_theme.dart b/lib/theme/models/scale_theme/scale_theme.dart index 755bd54..a428c2c 100644 --- a/lib/theme/models/scale_theme/scale_theme.dart +++ b/lib/theme/models/scale_theme/scale_theme.dart @@ -191,6 +191,12 @@ class ScaleTheme extends ThemeExtension { inputDecorationTheme: ScaleInputDecoratorTheme(scheme, config, textTheme), sliderTheme: sliderTheme, + popupMenuTheme: PopupMenuThemeData( + color: scheme.primaryScale.subtleBackground, + shadowColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(8 * config.borderRadiusScale))), extensions: >[scheme, config, this]); return themeData; diff --git a/lib/theme/views/preferences/display_scale_preferences.dart b/lib/theme/views/preferences/display_scale_preferences.dart index 84bf97d..c056280 100644 --- a/lib/theme/views/preferences/display_scale_preferences.dart +++ b/lib/theme/views/preferences/display_scale_preferences.dart @@ -38,6 +38,16 @@ const _scaleNumMult = [ 1 + 1 / 1, ]; +const _scaleNumMultNoShrink = [ + 1, + 1, + 1, + 1, + 1 + 1 / 4, + 1 + 1 / 2, + 1 + 1 / 1, +]; + int displayScaleToIndex(double displayScale) { final idx = _scales.indexWhere((elem) => elem > displayScale); final currentScaleIdx = idx == -1 ? _scales.length - 1 : max(0, idx - 1); @@ -92,6 +102,14 @@ extension DisplayScaledNum on num { displayScaleToIndex(prefs.themePreference.displayScale); return this * _scaleNumMult[currentScaleIdx]; } + + double scaledNoShrink(BuildContext context) { + final prefs = context.watch().state.asData?.value ?? + PreferencesRepository.instance.value; + final currentScaleIdx = + displayScaleToIndex(prefs.themePreference.displayScale); + return this * _scaleNumMultNoShrink[currentScaleIdx]; + } } extension DisplayScaledEdgeInsets on EdgeInsets { diff --git a/lib/theme/views/styled_widgets/styled_checkbox.dart b/lib/theme/views/styled_widgets/styled_checkbox.dart index 7eb3649..0213db2 100644 --- a/lib/theme/views/styled_widgets/styled_checkbox.dart +++ b/lib/theme/views/styled_widgets/styled_checkbox.dart @@ -43,7 +43,11 @@ class StyledCheckbox extends StatelessWidget { await _onChanged(value); }); })), - Text(_label, style: textStyle).paddingAll(4.scaled(context)), + Text( + _label, + style: textStyle, + overflow: TextOverflow.clip, + ).paddingLTRB(4.scaled(context), 0, 0, 0).flexible(), ]); if (_decoratorLabel != null) { diff --git a/lib/theme/views/styled_widgets/styled_scaffold.dart b/lib/theme/views/styled_widgets/styled_scaffold.dart index 82f27f5..9a32640 100644 --- a/lib/theme/views/styled_widgets/styled_scaffold.dart +++ b/lib/theme/views/styled_widgets/styled_scaffold.dart @@ -27,7 +27,7 @@ class StyledScaffold extends StatelessWidget { return GestureDetector( onTap: () => FocusManager.instance.primaryFocus?.unfocus(), - child: scaffold /*.paddingAll(enableBorder ? 32 : 0) */); + child: scaffold); } //////////////////////////////////////////////////////////////////////////// diff --git a/lib/theme/views/styled_widgets/styled_slide_tile.dart b/lib/theme/views/styled_widgets/styled_slide_tile.dart index 43b3fd8..59f8a7b 100644 --- a/lib/theme/views/styled_widgets/styled_slide_tile.dart +++ b/lib/theme/views/styled_widgets/styled_slide_tile.dart @@ -132,7 +132,7 @@ class StyledSlideTile extends StatelessWidget { softWrap: false, ), subtitle: subtitle.isNotEmpty ? Text(subtitle) : null, - contentPadding: const EdgeInsets.fromLTRB(8, 4, 8, 4) + contentPadding: const EdgeInsets.fromLTRB(8, 2, 8, 2) .scaled(context), iconColor: scaleTileTheme.textColor, textColor: scaleTileTheme.textColor, diff --git a/lib/theme/views/widget_helpers.dart b/lib/theme/views/widget_helpers.dart index 910074e..a02f207 100644 --- a/lib/theme/views/widget_helpers.dart +++ b/lib/theme/views/widget_helpers.dart @@ -571,6 +571,7 @@ Container clipBorder({ required Color borderColor, required Widget child, }) => + // We want to return a container here // ignore: avoid_unnecessary_containers, use_decorated_box Container( decoration: ShapeDecoration( diff --git a/pubspec.lock b/pubspec.lock index aa97499..ad7857c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -848,14 +848,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.9.5" - keyboard_avoider: - dependency: "direct main" - description: - name: keyboard_avoider - sha256: d2917bd52c6612bf8d1ff97f74049ddf3592a81d44e814f0e7b07dcfd245b75c - url: "https://pub.dev" - source: hosted - version: "0.2.0" lint_hard: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 519714b..e933661 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,7 +56,6 @@ dependencies: image: ^4.5.3 intl: ^0.19.0 json_annotation: ^4.9.0 - keyboard_avoider: ^0.2.0 loggy: ^2.0.3 meta: ^1.16.0 mobile_scanner: ^7.0.0 From 5a6b57e8ec0983524471f834db5f54e7701ee499 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 27 May 2025 19:04:17 -0500 Subject: [PATCH 252/270] textscale dialogs --- lib/account_manager/views/profile_widget.dart | 62 ++++++++++--------- .../views/contact_invitation_display.dart | 2 +- .../views/create_invitation_dialog.dart | 28 ++++----- .../views/invitation_dialog.dart | 11 ++-- lib/layout/home/home_account_ready.dart | 3 +- .../views/styled_widgets/styled_dialog.dart | 13 ++-- .../styled_widgets/styled_slide_tile.dart | 2 +- 7 files changed, 62 insertions(+), 59 deletions(-) diff --git a/lib/account_manager/views/profile_widget.dart b/lib/account_manager/views/profile_widget.dart index af7cf30..8217414 100644 --- a/lib/account_manager/views/profile_widget.dart +++ b/lib/account_manager/views/profile_widget.dart @@ -7,17 +7,10 @@ import '../../theme/theme.dart'; class ProfileWidget extends StatelessWidget { const ProfileWidget({ required proto.Profile profile, - required bool showPronouns, + String? byline, super.key, }) : _profile = profile, - _showPronouns = showPronouns; - - // - - final proto.Profile _profile; - final bool _showPronouns; - - // + _byline = byline; @override Widget build(BuildContext context) { @@ -44,26 +37,37 @@ class ProfileWidget extends StatelessWidget { borderRadius: BorderRadius.all( Radius.circular(8 * scaleConfig.borderRadiusScale))), ), - child: Row(children: [ - const Spacer(), - Text( - _profile.name, - style: textTheme.titleMedium!.copyWith( - color: scaleConfig.preferBorders - ? scale.primaryScale.border - : scale.primaryScale.borderText), - textAlign: TextAlign.left, - ).paddingAll(8.scaled(context)), - if (_profile.pronouns.isNotEmpty && _showPronouns) - Text('(${_profile.pronouns})', - textAlign: TextAlign.right, - style: textTheme.bodySmall!.copyWith( - color: scaleConfig.preferBorders - ? scale.primaryScale.border - : scale.primaryScale.primary)) - .paddingAll(8.scaled(context)), - const Spacer() - ]), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 8.scaled(context), + children: [ + Text( + _profile.name, + style: textTheme.titleMedium!.copyWith( + color: scaleConfig.preferBorders + ? scale.primaryScale.border + : scale.primaryScale.borderText), + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + maxLines: 1, + ), + if (_byline != null) + Text( + _byline, + style: textTheme.bodySmall!.copyWith( + color: scaleConfig.preferBorders + ? scale.primaryScale.border + : scale.primaryScale.primary), + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + maxLines: 1, + ), + ]).paddingAll(8.scaled(context)), ); } + + //////////////////////////////////////////////////////////////////////////// + + final proto.Profile _profile; + final String? _byline; } diff --git a/lib/contact_invitation/views/contact_invitation_display.dart b/lib/contact_invitation/views/contact_invitation_display.dart index b3f048a..4ab840d 100644 --- a/lib/contact_invitation/views/contact_invitation_display.dart +++ b/lib/contact_invitation/views/contact_invitation_display.dart @@ -58,7 +58,6 @@ class ContactInvitationDisplayDialog extends StatelessWidget { } @override - // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { final theme = Theme.of(context); final textTheme = theme.textTheme; @@ -131,6 +130,7 @@ class ContactInvitationDisplayDialog extends StatelessWidget { if (message.isNotEmpty) Text(message, softWrap: true, + textAlign: TextAlign.center, maxLines: 2, style: textTheme.labelMedium! .copyWith(color: Colors.black)) diff --git a/lib/contact_invitation/views/create_invitation_dialog.dart b/lib/contact_invitation/views/create_invitation_dialog.dart index 581e8d6..41e6162 100644 --- a/lib/contact_invitation/views/create_invitation_dialog.dart +++ b/lib/contact_invitation/views/create_invitation_dialog.dart @@ -41,7 +41,7 @@ class _CreateInvitationDialogState extends State { late final TextEditingController _recipientTextController; EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none; - String _encryptionKey = ''; + var _encryptionKey = ''; Timestamp? _expiration; @override @@ -171,24 +171,23 @@ class _CreateInvitationDialogState extends State { } @override - // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { final windowSize = MediaQuery.of(context).size; final maxDialogWidth = min(windowSize.width - 64.0, 800.0 - 64.0); final maxDialogHeight = windowSize.height - 64.0; final theme = Theme.of(context); - //final scale = theme.extension()!; final textTheme = theme.textTheme; return ConstrainedBox( constraints: BoxConstraints(maxHeight: maxDialogHeight, maxWidth: maxDialogWidth), child: SingleChildScrollView( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.all(8).scaled(context), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, + spacing: 16.scaled(context), children: [ TextField( autofocus: true, @@ -200,30 +199,29 @@ class _CreateInvitationDialogState extends State { LengthLimitingTextInputFormatter(128), ], decoration: InputDecoration( + contentPadding: const EdgeInsets.all(8).scaled(context), hintText: translate('create_invitation_dialog.recipient_hint'), labelText: translate('create_invitation_dialog.recipient_name'), helperText: translate('create_invitation_dialog.recipient_helper')), - ).paddingAll(8), - const SizedBox(height: 10), + ), TextField( controller: _messageTextController, inputFormatters: [ LengthLimitingTextInputFormatter(128), ], decoration: InputDecoration( + contentPadding: const EdgeInsets.all(8).scaled(context), hintText: translate('create_invitation_dialog.message_hint'), labelText: translate('create_invitation_dialog.message_label'), helperText: translate('create_invitation_dialog.message_helper')), - ).paddingAll(8), - const SizedBox(height: 10), + ), Text(translate('create_invitation_dialog.protect_this_invitation'), - style: textTheme.labelLarge) - .paddingAll(8), + style: textTheme.labelLarge), Wrap( alignment: WrapAlignment.center, runAlignment: WrapAlignment.center, @@ -245,23 +243,23 @@ class _CreateInvitationDialogState extends State { selected: _encryptionKeyType == EncryptionKeyType.password, onSelected: _onPasswordEncryptionSelected, ) - ]).paddingAll(8).toCenter(), + ]).toCenter(), Container( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.all(8).scaled(context), child: ElevatedButton( onPressed: _recipientTextController.text.isNotEmpty ? _onGenerateButtonPressed : null, child: Text( translate('create_invitation_dialog.generate'), - ).paddingAll(16), + ).paddingAll(16.scaled(context)), ), ).toCenter(), - Text(translate('create_invitation_dialog.note')).paddingAll(8), + Text(translate('create_invitation_dialog.note')), Text( translate('create_invitation_dialog.note_text'), style: Theme.of(context).textTheme.bodySmall, - ).paddingAll(8), + ), ], ), ), diff --git a/lib/contact_invitation/views/invitation_dialog.dart b/lib/contact_invitation/views/invitation_dialog.dart index 385cbcb..3cd0bfb 100644 --- a/lib/contact_invitation/views/invitation_dialog.dart +++ b/lib/contact_invitation/views/invitation_dialog.dart @@ -292,13 +292,10 @@ class InvitationDialogState extends State { ]).toCenter(), if (_validInvitation != null && !_isValidating) Column(children: [ - Container( - constraints: const BoxConstraints(maxHeight: 64), - width: double.infinity, - child: ProfileWidget( - profile: _validInvitation!.remoteProfile, - showPronouns: true, - )).paddingLTRB(0, 0, 0, 16), + ProfileWidget( + profile: _validInvitation!.remoteProfile, + byline: _validInvitation!.remoteProfile.pronouns, + ).paddingLTRB(0, 0, 0, 16), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ diff --git a/lib/layout/home/home_account_ready.dart b/lib/layout/home/home_account_ready.dart index 50a43c8..a829248 100644 --- a/lib/layout/home/home_account_ready.dart +++ b/lib/layout/home/home_account_ready.dart @@ -127,7 +127,6 @@ class _HomeAccountReadyState extends State { buildMenuButton().paddingLTRB(0, 0, 8, 0), ProfileWidget( profile: profile, - showPronouns: false, ).expanded(), buildContactsButton().paddingLTRB(8, 0, 0, 0), ])).paddingAll(8), @@ -169,7 +168,7 @@ class _HomeAccountReadyState extends State { final hasActiveChat = activeChat != null; return LayoutBuilder(builder: (context, constraints) { - const leftColumnSize = 300.0; + const leftColumnSize = 320.0; late final bool visibleLeft; late final bool visibleRight; diff --git a/lib/theme/views/styled_widgets/styled_dialog.dart b/lib/theme/views/styled_widgets/styled_dialog.dart index 4106f1d..54431b2 100644 --- a/lib/theme/views/styled_widgets/styled_dialog.dart +++ b/lib/theme/views/styled_widgets/styled_dialog.dart @@ -1,7 +1,7 @@ -import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import '../../../settings/settings.dart'; import '../../theme.dart'; class StyledDialog extends StatelessWidget { @@ -41,17 +41,22 @@ class StyledDialog extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( 12 * scaleConfig.borderRadiusScale))), - child: child.paddingAll(0)))); + child: child))); } static Future show( {required BuildContext context, required String title, - required Widget child}) async => + required Widget child}) => showDialog( context: context, useRootNavigator: false, - builder: (context) => StyledDialog(title: title, child: child)); + builder: (context) => AsyncBlocBuilder( + builder: (context, state) => MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaler: TextScaler.linear( + state.themePreference.displayScale)), + child: StyledDialog(title: title, child: child)))); final String title; final Widget child; diff --git a/lib/theme/views/styled_widgets/styled_slide_tile.dart b/lib/theme/views/styled_widgets/styled_slide_tile.dart index 59f8a7b..e4a0e27 100644 --- a/lib/theme/views/styled_widgets/styled_slide_tile.dart +++ b/lib/theme/views/styled_widgets/styled_slide_tile.dart @@ -120,7 +120,7 @@ class StyledSlideTile extends StatelessWidget { child: Padding( padding: scaleTheme.config.useVisualIndicators ? EdgeInsets.zero - : const EdgeInsets.fromLTRB(0, 2, 0, 2).scaled(context), + : const EdgeInsets.fromLTRB(0, 4, 0, 4).scaled(context), child: GestureDetector( onDoubleTap: onDoubleTap, child: ListTile( From b8eca1161edb4cf87093206d1613a7da23722c7a Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 27 May 2025 19:35:36 -0500 Subject: [PATCH 253/270] fix giant toilet --- lib/account_manager/views/edit_profile_form.dart | 10 +++++----- lib/contacts/views/availability_widget.dart | 14 +++++++++----- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/account_manager/views/edit_profile_form.dart b/lib/account_manager/views/edit_profile_form.dart index 114c7b8..c4f84da 100644 --- a/lib/account_manager/views/edit_profile_form.dart +++ b/lib/account_manager/views/edit_profile_form.dart @@ -103,14 +103,14 @@ class _EditProfileFormState extends State { labelText: translate('account.form_availability'), hintText: translate('account.empty_busy_message')), items: availabilities - .map((x) => DropdownMenuItem( - value: x, + .map((availability) => DropdownMenuItem( + value: availability, child: Row(mainAxisSize: MainAxisSize.min, children: [ AvailabilityWidget.availabilityIcon( - x, scale.primaryScale.appText), - Text(x == proto.Availability.AVAILABILITY_OFFLINE + context, availability, scale.primaryScale.appText), + Text(availability == proto.Availability.AVAILABILITY_OFFLINE ? translate('availability.always_show_offline') - : AvailabilityWidget.availabilityName(x)) + : AvailabilityWidget.availabilityName(availability)) .paddingLTRB(8.scaled(context), 0, 0, 0), ]))) .toList(), diff --git a/lib/contacts/views/availability_widget.dart b/lib/contacts/views/availability_widget.dart index 55fac39..5ef6080 100644 --- a/lib/contacts/views/availability_widget.dart +++ b/lib/contacts/views/availability_widget.dart @@ -4,6 +4,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_translate/flutter_translate.dart'; import '../../proto/proto.dart' as proto; +import '../../theme/theme.dart'; class AvailabilityWidget extends StatelessWidget { const AvailabilityWidget( @@ -13,6 +14,7 @@ class AvailabilityWidget extends StatelessWidget { super.key}); static Widget availabilityIcon( + BuildContext context, proto.Availability availability, Color color, ) { @@ -20,15 +22,17 @@ class AvailabilityWidget extends StatelessWidget { switch (availability) { case proto.Availability.AVAILABILITY_AWAY: icon = SvgPicture.asset('assets/images/toilet.svg', + width: 24.scaled(context), + height: 24.scaled(context), colorFilter: ColorFilter.mode(color, BlendMode.srcATop)); case proto.Availability.AVAILABILITY_BUSY: - icon = const Icon(Icons.event_busy, applyTextScaling: true); + icon = Icon(size: 24.scaled(context), Icons.event_busy); case proto.Availability.AVAILABILITY_FREE: - icon = const Icon(Icons.event_available, applyTextScaling: true); + icon = Icon(size: 24.scaled(context), Icons.event_available); case proto.Availability.AVAILABILITY_OFFLINE: - icon = const Icon(Icons.cloud_off, applyTextScaling: true); + icon = Icon(size: 24.scaled(context), Icons.cloud_off); case proto.Availability.AVAILABILITY_UNSPECIFIED: - icon = const Icon(Icons.question_mark, applyTextScaling: true); + icon = Icon(size: 24.scaled(context), Icons.question_mark); } return icon; } @@ -56,7 +60,7 @@ class AvailabilityWidget extends StatelessWidget { final textTheme = theme.textTheme; final name = availabilityName(availability); - final icon = availabilityIcon(availability, color); + final icon = availabilityIcon(context, availability, color); return vertical ? Column(mainAxisSize: MainAxisSize.min, children: [ From 8aaca62ea75444787702d481a2001b94ab78e231 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 28 May 2025 13:34:57 -0500 Subject: [PATCH 254/270] better time and status position --- .../chat_builders/vc_text_message_widget.dart | 91 +++++-------------- lib/chat/views/chat_component_widget.dart | 38 ++++++-- lib/chat_list/views/chat_list_widget.dart | 69 +++++++------- .../models/scale_theme/scale_chat_theme.dart | 5 +- lib/theme/views/widget_helpers.dart | 32 +++++++ 5 files changed, 119 insertions(+), 116 deletions(-) diff --git a/lib/chat/views/chat_builders/vc_text_message_widget.dart b/lib/chat/views/chat_builders/vc_text_message_widget.dart index 52235f6..7ea2957 100644 --- a/lib/chat/views/chat_builders/vc_text_message_widget.dart +++ b/lib/chat/views/chat_builders/vc_text_message_widget.dart @@ -22,7 +22,6 @@ class VcTextMessageWidget extends StatelessWidget { this.timeStyle, this.showTime = true, this.showStatus = true, - this.timeAndStatusPosition = TimeAndStatusPosition.end, super.key, }); @@ -63,9 +62,6 @@ class VcTextMessageWidget extends StatelessWidget { /// for sent messages. final bool showStatus; - /// Position of the timestamp and status indicator relative to the text. - final TimeAndStatusPosition timeAndStatusPosition; - bool get _isOnlyEmoji => message.metadata?['isOnlyEmoji'] == true; @override @@ -98,62 +94,26 @@ class VcTextMessageWidget extends StatelessWidget { : textStyle, ); - return Container( - padding: _isOnlyEmoji - ? EdgeInsets.symmetric( - horizontal: (padding?.horizontal ?? 0) / 2, - // vertical: 0, - ) - : padding, - decoration: _isOnlyEmoji - ? null - : BoxDecoration( - color: backgroundColor, - borderRadius: borderRadius ?? chatTheme.shape, - ), - child: _buildContentBasedOnPosition( - context: context, - textContent: textContent, - timeAndStatus: timeAndStatus, - textStyle: textStyle, - ), - ); - } - - Widget _buildContentBasedOnPosition({ - required BuildContext context, - required Widget textContent, - TimeAndStatus? timeAndStatus, - TextStyle? textStyle, - }) { - if (timeAndStatus == null) { - return textContent; - } - - switch (timeAndStatusPosition) { - case TimeAndStatusPosition.start: - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [textContent, timeAndStatus], - ); - case TimeAndStatusPosition.inline: - return Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Flexible(child: textContent), - const SizedBox(width: 4), - timeAndStatus, - ], - ); - case TimeAndStatusPosition.end: - return Column( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [textContent, timeAndStatus], - ); - } + return Column( + crossAxisAlignment: + isSentByMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, + children: [ + Container( + padding: _isOnlyEmoji + ? EdgeInsets.symmetric( + horizontal: (padding?.horizontal ?? 0) / 2, + // vertical: 0, + ) + : padding, + decoration: _isOnlyEmoji + ? null + : BoxDecoration( + color: backgroundColor, + borderRadius: borderRadius ?? chatTheme.shape, + ), + child: textContent), + if (timeAndStatus != null) timeAndStatus, + ]); } Color _resolveBackgroundColor(bool isSentByMe, ScaleChatTheme theme) { @@ -170,11 +130,8 @@ class VcTextMessageWidget extends StatelessWidget { return receivedTextStyle ?? theme.receivedMessageBodyTextStyle; } - TextStyle _resolveTimeStyle(bool isSentByMe, ScaleChatTheme theme) { - final ts = _resolveTextStyle(isSentByMe, theme); - - return theme.timeStyle.copyWith(color: ts.color); - } + TextStyle _resolveTimeStyle(bool isSentByMe, ScaleChatTheme theme) => + theme.timeStyle; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -193,9 +150,7 @@ class VcTextMessageWidget extends StatelessWidget { 'receivedTextStyle', receivedTextStyle)) ..add(DiagnosticsProperty('timeStyle', timeStyle)) ..add(DiagnosticsProperty('showTime', showTime)) - ..add(DiagnosticsProperty('showStatus', showStatus)) - ..add(EnumProperty( - 'timeAndStatusPosition', timeAndStatusPosition)); + ..add(DiagnosticsProperty('showStatus', showStatus)); } } diff --git a/lib/chat/views/chat_component_widget.dart b/lib/chat/views/chat_component_widget.dart index 8cb4edc..aecf531 100644 --- a/lib/chat/views/chat_component_widget.dart +++ b/lib/chat/views/chat_component_widget.dart @@ -261,18 +261,36 @@ class _ChatComponentWidgetState extends State { chatAnimatedListBuilder: (context, itemBuilder) => ChatAnimatedListReversed( scrollController: _scrollController, + messageGroupingTimeoutInSeconds: 60, itemBuilder: itemBuilder), // Text message builder - textMessageBuilder: (context, message, index) => - VcTextMessageWidget( - message: message, - index: index, - padding: const EdgeInsets.symmetric( - vertical: 12, horizontal: 16) - .scaled(context) - // showTime: true, - // showStatus: true, - ), + textMessageBuilder: (context, message, index) { + var showTime = true; + if (_chatController.messages.length > 1 && + index < _chatController.messages.length - 1 && + message.time != null) { + final nextMessage = + _chatController.messages[index + 1]; + if (nextMessage.time != null) { + if (nextMessage.time! + .difference(message.time!) + .inSeconds < + 60 && + nextMessage.authorId == message.authorId) { + showTime = false; + } + } + } + return VcTextMessageWidget( + message: message, + index: index, + padding: const EdgeInsets.symmetric( + vertical: 12, horizontal: 16) + .scaled(context), + showTime: showTime, + showStatus: showTime, + ); + }, // Composer builder composerBuilder: (ctx) => VcComposerWidget( autofocus: true, diff --git a/lib/chat_list/views/chat_list_widget.dart b/lib/chat_list/views/chat_list_widget.dart index 04720e8..33ddaab 100644 --- a/lib/chat_list/views/chat_list_widget.dart +++ b/lib/chat_list/views/chat_list_widget.dart @@ -56,43 +56,38 @@ class ChatListWidget extends StatelessWidget { valueMapper: (c) => c.value); final chatListV = context.watch().state; - return chatListV - .builder((context, chatList) => SizedBox.expand( - child: styledTitleContainer( - context: context, - title: translate('chat_list.chats'), - child: (chatList.isEmpty) - ? const SizedBox.expand(child: EmptyChatListWidget()) - : TapRegion( - onTapOutside: (_) { - FocusScope.of(context).unfocus(); - }, - child: SearchableList( - initialList: chatList.map((x) => x.value).toList(), - itemBuilder: (c) { - switch (c.whichKind()) { - case proto.Chat_Kind.direct: - return _itemBuilderDirect( - c.direct, - contactMap, - ); - case proto.Chat_Kind.group: - return const Text( - 'group chats not yet supported!'); - case proto.Chat_Kind.notSet: - throw StateError('unknown chat kind'); - } - }, - filter: (value) => - _itemFilter(contactMap, chatList, value), - searchFieldPadding: - const EdgeInsets.fromLTRB(0, 0, 0, 4), - inputDecoration: InputDecoration( - labelText: translate('chat_list.search'), - ), - )).paddingAll(8), - ))) - .paddingLTRB(8, 0, 8, 8); + return chatListV.builder((context, chatList) => SizedBox.expand( + child: styledContainer( + context: context, + child: (chatList.isEmpty) + ? const SizedBox.expand(child: EmptyChatListWidget()) + : TapRegion( + onTapOutside: (_) { + FocusScope.of(context).unfocus(); + }, + child: SearchableList( + initialList: chatList.map((x) => x.value).toList(), + itemBuilder: (c) { + switch (c.whichKind()) { + case proto.Chat_Kind.direct: + return _itemBuilderDirect( + c.direct, + contactMap, + ); + case proto.Chat_Kind.group: + return const Text('group chats not yet supported!'); + case proto.Chat_Kind.notSet: + throw StateError('unknown chat kind'); + } + }, + filter: (value) => + _itemFilter(contactMap, chatList, value), + searchFieldPadding: const EdgeInsets.fromLTRB(0, 0, 0, 4), + inputDecoration: InputDecoration( + labelText: translate('chat_list.search'), + ), + )).paddingAll(8), + ))); }); } } diff --git a/lib/theme/models/scale_theme/scale_chat_theme.dart b/lib/theme/models/scale_theme/scale_chat_theme.dart index 5da0fe2..d1145cf 100644 --- a/lib/theme/models/scale_theme/scale_chat_theme.dart +++ b/lib/theme/models/scale_theme/scale_chat_theme.dart @@ -354,7 +354,10 @@ extension ScaleChatThemeExt on ScaleTheme { : scheme.primaryScale.calloutText, ), onlyEmojiFontSize: 64, - timeStyle: textTheme.bodySmall!.copyWith(fontSize: 9), + timeStyle: textTheme.bodySmall!.copyWith(fontSize: 9).copyWith( + color: config.preferBorders || config.useVisualIndicators + ? scheme.primaryScale.calloutBackground + : scheme.primaryScale.borderText), receivedMessageBodyTextStyle: textTheme.bodyLarge!.copyWith( color: config.preferBorders ? scheme.secondaryScale.calloutBackground diff --git a/lib/theme/views/widget_helpers.dart b/lib/theme/views/widget_helpers.dart index a02f207..2d8d626 100644 --- a/lib/theme/views/widget_helpers.dart +++ b/lib/theme/views/widget_helpers.dart @@ -464,6 +464,38 @@ Widget styledTitleContainer({ ])); } +Widget styledContainer({ + required BuildContext context, + required Widget child, + Color? borderColor, + Color? backgroundColor, +}) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + + return DecoratedBox( + decoration: ShapeDecoration( + color: borderColor ?? scale.primaryScale.border, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale), + )), + child: Column(children: [ + DecoratedBox( + decoration: ShapeDecoration( + color: + backgroundColor ?? scale.primaryScale.subtleBackground, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 8 * scaleConfig.borderRadiusScale), + )), + child: child) + .paddingAll(4) + .expanded() + ])); +} + Widget styledCard({ required BuildContext context, required Widget child, From 6421a775721715d6ec9926a308bd518b5d56db4e Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 28 May 2025 14:38:42 -0500 Subject: [PATCH 255/270] fix popup menu --- lib/contacts/views/contacts_browser.dart | 37 +++++++------------ lib/theme/models/scale_theme/scale_theme.dart | 3 +- pubspec.lock | 8 ---- pubspec.yaml | 1 - 4 files changed, 15 insertions(+), 34 deletions(-) diff --git a/lib/contacts/views/contacts_browser.dart b/lib/contacts/views/contacts_browser.dart index 10b207f..22c64c7 100644 --- a/lib/contacts/views/contacts_browser.dart +++ b/lib/contacts/views/contacts_browser.dart @@ -95,29 +95,18 @@ class _ContactsBrowserState extends State void Function()? onTap}) => PopupMenuItem( onTap: onTap, - child: DecoratedBox( - decoration: ShapeDecoration( - color: menuBackgroundColor, - shape: RoundedRectangleBorder( - side: scaleConfig.useVisualIndicators - ? BorderSide( - width: 2, - color: menuBorderColor, - strokeAlign: 0) - : BorderSide.none, - borderRadius: BorderRadius.circular( - 8 * scaleConfig.borderRadiusScale))), - child: Row(spacing: 4.scaled(context), children: [ - Icon(iconData, size: 32.scaled(context)), - Text( - text, - textScaler: MediaQuery.of(context).textScaler, - maxLines: 2, - textAlign: TextAlign.center, - ) - ]).paddingAll(4.scaled(context))) - .paddingLTRB(0, 2.scaled(context), 0, 2.scaled(context))); - + child: Row( + spacing: 8.scaled(context), + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Icon(iconData, size: 32.scaled(context)), + Text( + text, + textScaler: MediaQuery.of(context).textScaler, + maxLines: 2, + textAlign: TextAlign.center, + ) + ])); final inviteMenuItems = [ makeMenuButton( iconData: Icons.contact_page, @@ -141,7 +130,7 @@ class _ContactsBrowserState extends State return PopupMenuButton( itemBuilder: (_) => inviteMenuItems, - menuPadding: const EdgeInsets.symmetric(vertical: 4).scaled(context), + menuPadding: const EdgeInsets.symmetric(vertical: 8).scaled(context), tooltip: translate('add_contact_sheet.add_contact'), child: Icon( size: 32.scaled(context), Icons.person_add, color: menuIconColor)); diff --git a/lib/theme/models/scale_theme/scale_theme.dart b/lib/theme/models/scale_theme/scale_theme.dart index a428c2c..c1d41b2 100644 --- a/lib/theme/models/scale_theme/scale_theme.dart +++ b/lib/theme/models/scale_theme/scale_theme.dart @@ -192,9 +192,10 @@ class ScaleTheme extends ThemeExtension { ScaleInputDecoratorTheme(scheme, config, textTheme), sliderTheme: sliderTheme, popupMenuTheme: PopupMenuThemeData( - color: scheme.primaryScale.subtleBackground, + color: scheme.primaryScale.elementBackground, shadowColor: Colors.transparent, shape: RoundedRectangleBorder( + side: BorderSide(color: scheme.primaryScale.border, width: 2), borderRadius: BorderRadius.circular(8 * config.borderRadiusScale))), extensions: >[scheme, config, this]); diff --git a/pubspec.lock b/pubspec.lock index ad7857c..3b7f823 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1527,14 +1527,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.12.1" - star_menu: - dependency: "direct main" - description: - name: star_menu - sha256: f29c7d255677c49ec2412ec2d17220d967f54b72b9e6afc5688fe122ea4d1d78 - url: "https://pub.dev" - source: hosted - version: "4.0.1" stream_channel: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e933661..fccff99 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -92,7 +92,6 @@ dependencies: ref: main split_view: ^3.2.1 stack_trace: ^1.12.1 - star_menu: ^4.0.1 stream_transform: ^2.1.1 toastification: ^3.0.2 transitioned_indexed_stack: ^1.0.2 From b7752a7e95e62075f3eacc568144991dc687166a Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 30 May 2025 22:06:20 -0400 Subject: [PATCH 256/270] checkpoint on persistent queue update --- .../cubits/single_contact_messages_cubit.dart | 1 + .../example/integration_test/app_test.dart | 57 ++++++++++++------- packages/veilid_support/example/pubspec.lock | 2 +- packages/veilid_support/example/pubspec.yaml | 1 + .../lib/src/persistent_queue.dart | 55 +++++++++++++----- 5 files changed, 80 insertions(+), 36 deletions(-) diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index 878858b..a3e7656 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -107,6 +107,7 @@ class SingleContactMessagesCubit extends Cubit { table: 'SingleContactUnsentMessages', key: _remoteConversationRecordKey.toString(), fromBuffer: proto.Message.fromBuffer, + toBuffer: (x) => x.writeToBuffer(), closure: _processUnsentMessages, onError: (e, st) { log.error('Exception while processing unsent messages: $e\n$st\n'); diff --git a/packages/veilid_support/example/integration_test/app_test.dart b/packages/veilid_support/example/integration_test/app_test.dart index 5dc7acd..2d3d0e2 100644 --- a/packages/veilid_support/example/integration_test/app_test.dart +++ b/packages/veilid_support/example/integration_test/app_test.dart @@ -8,6 +8,7 @@ import 'fixtures/fixtures.dart'; import 'test_dht_log.dart'; import 'test_dht_record_pool.dart'; import 'test_dht_short_array.dart'; +import 'test_persistent_queue.dart'; import 'test_table_db_array.dart'; void main() { @@ -32,60 +33,70 @@ void main() { debugPrintSynchronously('Duration: ${endTime.difference(startTime)}'); }); - group('Attached Tests', () { + group('attached', () { setUpAll(veilidFixture.attach); tearDownAll(veilidFixture.detach); - group('DHT Support Tests', () { + group('persistent_queue', () { + test('persistent_queue:open_close', testPersistentQueueOpenClose); + test('persistent_queue:add', testPersistentQueueAdd); + test('persistent_queue:add_sync', testPersistentQueueAddSync); + test('persistent_queue:add_persist', testPersistentQueueAddPersist); + test('persistent_queue:add_sync_persist', + testPersistentQueueAddSyncPersist); + }); + + group('dht_support', () { setUpAll(updateProcessorFixture.setUp); setUpAll(tickerFixture.setUp); tearDownAll(tickerFixture.tearDown); tearDownAll(updateProcessorFixture.tearDown); - test('create pool', testDHTRecordPoolCreate); + test('create_pool', testDHTRecordPoolCreate); - group('DHTRecordPool Tests', () { + group('dht_record_pool', () { setUpAll(dhtRecordPoolFixture.setUp); tearDownAll(dhtRecordPoolFixture.tearDown); - test('create/delete record', testDHTRecordCreateDelete); - test('record scopes', testDHTRecordScopes); - test('create/delete deep record', testDHTRecordDeepCreateDelete); + test('dht_record_pool:create_delete', testDHTRecordCreateDelete); + test('dht_record_pool:scopes', testDHTRecordScopes); + test('dht_record_pool:deep_create_delete', + testDHTRecordDeepCreateDelete); }); - group('DHTShortArray Tests', () { + group('dht_short_array', () { setUpAll(dhtRecordPoolFixture.setUp); tearDownAll(dhtRecordPoolFixture.tearDown); for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { - test('create shortarray stride=$stride', + test('dht_short_array:create_stride_$stride', makeTestDHTShortArrayCreateDelete(stride: stride)); - test('add shortarray stride=$stride', + test('dht_short_array:add_stride_$stride', makeTestDHTShortArrayAdd(stride: stride)); } }); - group('DHTLog Tests', () { + group('dht_log', () { setUpAll(dhtRecordPoolFixture.setUp); tearDownAll(dhtRecordPoolFixture.tearDown); for (final stride in [256, 16 /*64, 32, 16, 8, 4, 2, 1 */]) { - test('create log stride=$stride', + test('dht_log:create_stride_$stride', makeTestDHTLogCreateDelete(stride: stride)); test( timeout: const Timeout(Duration(seconds: 480)), - 'add/truncate log stride=$stride', + 'dht_log:add_truncate_stride_$stride', makeTestDHTLogAddTruncate(stride: stride), ); } }); }); - group('TableDB Tests', () { - group('TableDBArray Tests', () { - // test('create/delete TableDBArray', testTableDBArrayCreateDelete); + group('table_db', () { + group('table_db_array', () { + test('table_db_array:create_delete', testTableDBArrayCreateDelete); - group('TableDBArray Add/Get Tests', () { + group('table_db_array:add_get', () { for (final params in [ // (99, 3, 15), @@ -110,7 +121,7 @@ void main() { test( timeout: const Timeout(Duration(seconds: 480)), - 'add/remove TableDBArray count = $count batchSize=$batchSize', + 'table_db_array:add_remove_count=${count}_batchSize=$batchSize', makeTestTableDBArrayAddGetClear( count: count, singles: singles, @@ -120,7 +131,7 @@ void main() { } }); - group('TableDBArray Insert Tests', () { + group('table_db_array:insert', () { for (final params in [ // (99, 3, 15), @@ -145,7 +156,8 @@ void main() { test( timeout: const Timeout(Duration(seconds: 480)), - 'insert TableDBArray count=$count singles=$singles batchSize=$batchSize', + 'table_db_array:insert_count=${count}_' + 'singles=${singles}_batchSize=$batchSize', makeTestTableDBArrayInsert( count: count, singles: singles, @@ -155,7 +167,7 @@ void main() { } }); - group('TableDBArray Remove Tests', () { + group('table_db_array:remove', () { for (final params in [ // (99, 3, 15), @@ -180,7 +192,8 @@ void main() { test( timeout: const Timeout(Duration(seconds: 480)), - 'remove TableDBArray count=$count singles=$singles batchSize=$batchSize', + 'table_db_array:remove_count=${count}_' + 'singles=${singles}_batchSize=$batchSize', makeTestTableDBArrayRemove( count: count, singles: singles, diff --git a/packages/veilid_support/example/pubspec.lock b/packages/veilid_support/example/pubspec.lock index 9af9773..bc4ab6f 100644 --- a/packages/veilid_support/example/pubspec.lock +++ b/packages/veilid_support/example/pubspec.lock @@ -106,7 +106,7 @@ packages: source: hosted version: "1.1.2" collection: - dependency: transitive + dependency: "direct main" description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" diff --git a/packages/veilid_support/example/pubspec.yaml b/packages/veilid_support/example/pubspec.yaml index b8333e6..42f9885 100644 --- a/packages/veilid_support/example/pubspec.yaml +++ b/packages/veilid_support/example/pubspec.yaml @@ -7,6 +7,7 @@ environment: sdk: '>=3.3.4 <4.0.0' dependencies: + collection: ^1.19.1 cupertino_icons: ^1.0.8 flutter: sdk: flutter diff --git a/packages/veilid_support/lib/src/persistent_queue.dart b/packages/veilid_support/lib/src/persistent_queue.dart index 939a5b3..4e5e541 100644 --- a/packages/veilid_support/lib/src/persistent_queue.dart +++ b/packages/veilid_support/lib/src/persistent_queue.dart @@ -9,19 +9,20 @@ import 'config.dart'; import 'table_db.dart'; import 'veilid_log.dart'; -class PersistentQueue - with TableDBBackedFromBuffer> { +class PersistentQueue with TableDBBackedFromBuffer> { // PersistentQueue( {required String table, required String key, required T Function(Uint8List) fromBuffer, + required Uint8List Function(T) toBuffer, required Future Function(IList) closure, bool deleteOnClose = true, void Function(Object, StackTrace)? onError}) : _table = table, _key = key, _fromBuffer = fromBuffer, + _toBuffer = toBuffer, _closure = closure, _deleteOnClose = deleteOnClose, _onError = onError { @@ -34,11 +35,13 @@ class PersistentQueue // Close the sync add stream await _syncAddController.close(); + await _syncAddTask; // Stop the processing trigger await _queueReady.close(); + await _processorTask; - // Wait for any setStates to finish + // No more queue actions await _queueMutex.acquire(); // Clean up table if desired @@ -47,27 +50,40 @@ class PersistentQueue } } + Future get wait async { + // Ensure the init finished + await _initWait(); + + if (_queue.isEmpty) { + return; + } + final completer = Completer(); + _queueDoneCompleter = completer; + await completer.future; + } + Future _init(Completer _) async { // Start the processor - unawaited(Future.delayed(Duration.zero, () async { + _processorTask = Future.delayed(Duration.zero, () async { await _initWait(); await for (final _ in _queueReady.stream) { await _process(); } - })); + }); // Start the sync add controller - unawaited(Future.delayed(Duration.zero, () async { + _syncAddTask = Future.delayed(Duration.zero, () async { await _initWait(); await for (final elem in _syncAddController.stream) { await addAll(elem); } - })); + }); // Load the queue if we have one try { await _queueMutex.protect(() async { _queue = await load() ?? await store(IList.empty()); + _sendUpdateEventsInner(); }); } on Exception catch (e, st) { if (_onError != null) { @@ -78,11 +94,20 @@ class PersistentQueue } } + void _sendUpdateEventsInner() { + assert(_queueMutex.isLocked, 'must be locked'); + if (_queue.isNotEmpty) { + if (!_queueReady.isClosed) { + _queueReady.sink.add(null); + } + } else { + _queueDoneCompleter?.complete(); + } + } + Future _updateQueueInner(IList newQueue) async { _queue = await store(newQueue); - if (_queue.isNotEmpty) { - _queueReady.sink.add(null); - } + _sendUpdateEventsInner(); } Future add(T item) async { @@ -213,7 +238,7 @@ class PersistentQueue Uint8List valueToBuffer(IList val) { final writer = CodedBufferWriter(); for (final elem in val) { - writer.writeRawBytes(elem.writeToBuffer()); + writer.writeRawBytes(_toBuffer(elem)); } return writer.toBuffer(); } @@ -221,12 +246,16 @@ class PersistentQueue final String _table; final String _key; final T Function(Uint8List) _fromBuffer; + final Uint8List Function(T) _toBuffer; final bool _deleteOnClose; final WaitSet _initWait = WaitSet(); - final Mutex _queueMutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); - IList _queue = IList.empty(); + final _queueMutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); + var _queue = IList.empty(); final StreamController> _syncAddController = StreamController(); final StreamController _queueReady = StreamController(); final Future Function(IList) _closure; final void Function(Object, StackTrace)? _onError; + late Future _processorTask; + late Future _syncAddTask; + Completer? _queueDoneCompleter; } From fa72782f3996e430313d7f5de735d8a5b0370a1e Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 1 Jun 2025 15:09:22 -0400 Subject: [PATCH 257/270] persistent queue fixes --- packages/veilid_support/example/pubspec.lock | 16 ++- packages/veilid_support/example/pubspec.yaml | 10 +- .../lib/src/persistent_queue.dart | 107 +++++++----------- .../lib/src/table_db_array.dart | 77 ++++++------- packages/veilid_support/pubspec.lock | 16 ++- packages/veilid_support/pubspec.yaml | 13 ++- pubspec.lock | 14 +-- pubspec.yaml | 12 +- 8 files changed, 131 insertions(+), 134 deletions(-) diff --git a/packages/veilid_support/example/pubspec.lock b/packages/veilid_support/example/pubspec.lock index bc4ab6f..5c4355b 100644 --- a/packages/veilid_support/example/pubspec.lock +++ b/packages/veilid_support/example/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: "direct dev" description: name: async_tools - sha256: afd5426e76631172f8ce6a6359b264b092fa9d2a52cd2528100115be9525e067 + sha256: "9611c1efeae7e6d342721d0c2caf2e4783d91fba6a9637d7badfa2dccf8de2a2" url: "https://pub.dev" source: hosted - version: "0.1.9" + version: "0.1.10" bloc: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: bloc_advanced_tools - sha256: dfb142569814952af8d93e7fe045972d847e29382471687db59913e253202f6e + sha256: "63e57000df7259e3007dbfbbfd7dae3e0eca60eb2ac93cbe0c5a3de0e77c9972" url: "https://pub.dev" source: hosted - version: "0.1.12" + version: "0.1.13" boolean_selector: dependency: transitive description: @@ -65,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + buffer: + dependency: transitive + description: + name: buffer + sha256: "389da2ec2c16283c8787e0adaede82b1842102f8c8aae2f49003a766c5c6b3d1" + url: "https://pub.dev" + source: hosted + version: "1.2.3" change_case: dependency: transitive description: diff --git a/packages/veilid_support/example/pubspec.yaml b/packages/veilid_support/example/pubspec.yaml index 42f9885..86c8e7e 100644 --- a/packages/veilid_support/example/pubspec.yaml +++ b/packages/veilid_support/example/pubspec.yaml @@ -1,10 +1,10 @@ name: example description: "Veilid Support Example" -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: '>=3.3.4 <4.0.0' + sdk: ">=3.3.4 <4.0.0" dependencies: collection: ^1.19.1 @@ -15,7 +15,7 @@ dependencies: path: ../ dev_dependencies: - async_tools: ^0.1.9 + async_tools: ^0.1.10 integration_test: sdk: flutter lint_hard: ^6.0.0 @@ -23,5 +23,9 @@ dev_dependencies: veilid_test: path: ../../../../veilid/veilid-flutter/packages/veilid_test +# dependency_overrides: +# async_tools: +# path: ../../../../dart_async_tools + flutter: uses-material-design: true diff --git a/packages/veilid_support/lib/src/persistent_queue.dart b/packages/veilid_support/lib/src/persistent_queue.dart index 4e5e541..efb4c86 100644 --- a/packages/veilid_support/lib/src/persistent_queue.dart +++ b/packages/veilid_support/lib/src/persistent_queue.dart @@ -2,13 +2,15 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:async_tools/async_tools.dart'; +import 'package:buffer/buffer.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:protobuf/protobuf.dart'; import 'config.dart'; import 'table_db.dart'; import 'veilid_log.dart'; +const _ksfSyncAdd = 'ksfSyncAdd'; + class PersistentQueue with TableDBBackedFromBuffer> { // PersistentQueue( @@ -17,7 +19,7 @@ class PersistentQueue with TableDBBackedFromBuffer> { required T Function(Uint8List) fromBuffer, required Uint8List Function(T) toBuffer, required Future Function(IList) closure, - bool deleteOnClose = true, + bool deleteOnClose = false, void Function(Object, StackTrace)? onError}) : _table = table, _key = key, @@ -33,13 +35,12 @@ class PersistentQueue with TableDBBackedFromBuffer> { // Ensure the init finished await _initWait(); - // Close the sync add stream - await _syncAddController.close(); - await _syncAddTask; + // Finish all sync adds + await serialFutureClose((this, _ksfSyncAdd)); // Stop the processing trigger + await _sspQueueReady.close(); await _queueReady.close(); - await _processorTask; // No more queue actions await _queueMutex.acquire(); @@ -50,7 +51,13 @@ class PersistentQueue with TableDBBackedFromBuffer> { } } - Future get wait async { + set deleteOnClose(bool d) { + _deleteOnClose = d; + } + + bool get deleteOnClose => _deleteOnClose; + + Future get waitEmpty async { // Ensure the init finished await _initWait(); @@ -64,21 +71,13 @@ class PersistentQueue with TableDBBackedFromBuffer> { Future _init(Completer _) async { // Start the processor - _processorTask = Future.delayed(Duration.zero, () async { + _sspQueueReady.follow(_queueReady.stream, true, (more) async { await _initWait(); - await for (final _ in _queueReady.stream) { + if (more) { await _process(); } }); - // Start the sync add controller - _syncAddTask = Future.delayed(Duration.zero, () async { - await _initWait(); - await for (final elem in _syncAddController.stream) { - await addAll(elem); - } - }); - // Load the queue if we have one try { await _queueMutex.protect(() async { @@ -98,7 +97,7 @@ class PersistentQueue with TableDBBackedFromBuffer> { assert(_queueMutex.isLocked, 'must be locked'); if (_queue.isNotEmpty) { if (!_queueReady.isClosed) { - _queueReady.sink.add(null); + _queueReady.sink.add(true); } } else { _queueDoneCompleter?.complete(); @@ -127,46 +126,24 @@ class PersistentQueue with TableDBBackedFromBuffer> { } void addSync(T item) { - _syncAddController.sink.add([item]); + serialFuture((this, _ksfSyncAdd), () async { + await add(item); + }); } void addAllSync(Iterable items) { - _syncAddController.sink.add(items); + serialFuture((this, _ksfSyncAdd), () async { + await addAll(items); + }); } - // Future get isEmpty async { - // await _initWait(); - // return state.asData!.value.isEmpty; - // } + Future pause() async { + await _sspQueueReady.pause(); + } - // Future get isNotEmpty async { - // await _initWait(); - // return state.asData!.value.isNotEmpty; - // } - - // Future get length async { - // await _initWait(); - // return state.asData!.value.length; - // } - - // Future pop() async { - // await _initWait(); - // return _processingMutex.protect(() async => _stateMutex.protect(() async { - // final removedItem = Output(); - // final queue = state.asData!.value.removeAt(0, removedItem); - // await _setStateInner(queue); - // return removedItem.value; - // })); - // } - - // Future> popAll() async { - // await _initWait(); - // return _processingMutex.protect(() async => _stateMutex.protect(() async { - // final queue = state.asData!.value; - // await _setStateInner(IList.empty); - // return queue; - // })); - // } + Future resume() async { + await _sspQueueReady.resume(); + } Future _process() async { try { @@ -210,9 +187,10 @@ class PersistentQueue with TableDBBackedFromBuffer> { IList valueFromBuffer(Uint8List bytes) { var out = IList(); try { - final reader = CodedBufferReader(bytes); - while (!reader.isAtEnd()) { - final bytes = reader.readBytesAsView(); + final reader = ByteDataReader()..add(bytes); + while (reader.remainingLength != 0) { + final count = reader.readUint32(); + final bytes = reader.read(count); try { final item = _fromBuffer(bytes); out = out.add(item); @@ -236,26 +214,29 @@ class PersistentQueue with TableDBBackedFromBuffer> { @override Uint8List valueToBuffer(IList val) { - final writer = CodedBufferWriter(); + final writer = ByteDataWriter(); for (final elem in val) { - writer.writeRawBytes(_toBuffer(elem)); + final bytes = _toBuffer(elem); + final count = bytes.lengthInBytes; + writer + ..writeUint32(count) + ..write(bytes); } - return writer.toBuffer(); + return writer.toBytes(); } final String _table; final String _key; final T Function(Uint8List) _fromBuffer; final Uint8List Function(T) _toBuffer; - final bool _deleteOnClose; + bool _deleteOnClose; final WaitSet _initWait = WaitSet(); final _queueMutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); var _queue = IList.empty(); - final StreamController> _syncAddController = StreamController(); - final StreamController _queueReady = StreamController(); final Future Function(IList) _closure; final void Function(Object, StackTrace)? _onError; - late Future _processorTask; - late Future _syncAddTask; Completer? _queueDoneCompleter; + + final StreamController _queueReady = StreamController(); + final _sspQueueReady = SingleStateProcessor(); } diff --git a/packages/veilid_support/lib/src/table_db_array.dart b/packages/veilid_support/lib/src/table_db_array.dart index 8b59336..53adeb0 100644 --- a/packages/veilid_support/lib/src/table_db_array.dart +++ b/packages/veilid_support/lib/src/table_db_array.dart @@ -46,7 +46,7 @@ class _TableDBArrayBase { await _initWait(); } - Future _init(_) async { + Future _init(Completer _) async { // Load the array details await _mutex.protect(() async { _tableDB = await Veilid.instance.openTableDB(_table, 1); @@ -102,27 +102,27 @@ class _TableDBArrayBase { Future _add(Uint8List value) async { await _initWait(); - return _writeTransaction((t) async => _addInner(t, value)); + return _writeTransaction((t) => _addInner(t, value)); } Future _addAll(List values) async { await _initWait(); - return _writeTransaction((t) async => _addAllInner(t, values)); + return _writeTransaction((t) => _addAllInner(t, values)); } Future _insert(int pos, Uint8List value) async { await _initWait(); - return _writeTransaction((t) async => _insertInner(t, pos, value)); + return _writeTransaction((t) => _insertInner(t, pos, value)); } Future _insertAll(int pos, List values) async { await _initWait(); - return _writeTransaction((t) async => _insertAllInner(t, pos, values)); + return _writeTransaction((t) => _insertAllInner(t, pos, values)); } Future _get(int pos) async { await _initWait(); - return _mutex.protect(() async { + return _mutex.protect(() { if (!_open) { throw StateError('not open'); } @@ -132,7 +132,7 @@ class _TableDBArrayBase { Future> _getRange(int start, [int? end]) async { await _initWait(); - return _mutex.protect(() async { + return _mutex.protect(() { if (!_open) { throw StateError('not open'); } @@ -142,14 +142,13 @@ class _TableDBArrayBase { Future _remove(int pos, {Output? out}) async { await _initWait(); - return _writeTransaction((t) async => _removeInner(t, pos, out: out)); + return _writeTransaction((t) => _removeInner(t, pos, out: out)); } Future _removeRange(int start, int end, {Output>? out}) async { await _initWait(); - return _writeTransaction( - (t) async => _removeRangeInner(t, start, end, out: out)); + return _writeTransaction((t) => _removeRangeInner(t, start, end, out: out)); } Future clear() async { @@ -331,24 +330,24 @@ class _TableDBArrayBase { //////////////////////////////////////////////////////////// // Private implementation - static final Uint8List _headKey = Uint8List.fromList([$_, $H, $E, $A, $D]); + static final _headKey = Uint8List.fromList([$_, $H, $E, $A, $D]); static Uint8List _entryKey(int k) => (ByteData(4)..setUint32(0, k)).buffer.asUint8List(); static Uint8List _chunkKey(int n) => (ByteData(2)..setUint16(0, n)).buffer.asUint8List(); Future _writeTransaction( - Future Function(VeilidTableDBTransaction) closure) async => + Future Function(VeilidTableDBTransaction) closure) => _mutex.protect(() async { if (!_open) { throw StateError('not open'); } - final _oldLength = _length; - final _oldNextFree = _nextFree; - final _oldMaxEntry = _maxEntry; - final _oldHeadDelta = _headDelta; - final _oldTailDelta = _tailDelta; + final oldLength = _length; + final oldNextFree = _nextFree; + final oldMaxEntry = _maxEntry; + final oldHeadDelta = _headDelta; + final oldTailDelta = _tailDelta; try { final out = await transactionScope(_tableDB, (t) async { final out = await closure(t); @@ -365,11 +364,11 @@ class _TableDBArrayBase { return out; } on Exception { // restore head - _length = _oldLength; - _nextFree = _oldNextFree; - _maxEntry = _oldMaxEntry; - _headDelta = _oldHeadDelta; - _tailDelta = _oldTailDelta; + _length = oldLength; + _nextFree = oldNextFree; + _maxEntry = oldMaxEntry; + _headDelta = oldHeadDelta; + _tailDelta = oldTailDelta; // invalidate caches because they could have been written to _chunkCache.clear(); _dirtyChunks.clear(); @@ -415,7 +414,7 @@ class _TableDBArrayBase { _dirtyChunks[chunkNumber] = chunk; } - Future _insertIndexEntry(int pos) async => _insertIndexEntries(pos, 1); + Future _insertIndexEntry(int pos) => _insertIndexEntries(pos, 1); Future _insertIndexEntries(int start, int length) async { if (length == 0) { @@ -474,7 +473,7 @@ class _TableDBArrayBase { _tailDelta += length; } - Future _removeIndexEntry(int pos) async => _removeIndexEntries(pos, 1); + Future _removeIndexEntry(int pos) => _removeIndexEntries(pos, 1); Future _removeIndexEntries(int start, int length) async { if (length == 0) { @@ -624,20 +623,20 @@ class _TableDBArrayBase { var _initDone = false; final VeilidCrypto _crypto; final WaitSet _initWait = WaitSet(); - final Mutex _mutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); + final _mutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); // Change tracking - int _headDelta = 0; - int _tailDelta = 0; + var _headDelta = 0; + var _tailDelta = 0; // Head state - int _length = 0; - int _nextFree = 0; - int _maxEntry = 0; - static const int _indexStride = 16384; + var _length = 0; + var _nextFree = 0; + var _maxEntry = 0; + static const _indexStride = 16384; final List<(int, Uint8List)> _chunkCache = []; final Map _dirtyChunks = {}; - static const int _chunkCacheLength = 3; + static const _chunkCacheLength = 3; final StreamController _changeStream = StreamController.broadcast(); @@ -711,13 +710,12 @@ class TableDBArrayJson extends _TableDBArrayBase { Future add(T value) => _add(jsonEncodeBytes(value)); - Future addAll(List values) async => + Future addAll(List values) => _addAll(values.map(jsonEncodeBytes).toList()); - Future insert(int pos, T value) async => - _insert(pos, jsonEncodeBytes(value)); + Future insert(int pos, T value) => _insert(pos, jsonEncodeBytes(value)); - Future insertAll(int pos, List values) async => + Future insertAll(int pos, List values) => _insertAll(pos, values.map(jsonEncodeBytes).toList()); Future get( @@ -774,13 +772,12 @@ class TableDBArrayProtobuf Future add(T value) => _add(value.writeToBuffer()); - Future addAll(List values) async => + Future addAll(List values) => _addAll(values.map((x) => x.writeToBuffer()).toList()); - Future insert(int pos, T value) async => - _insert(pos, value.writeToBuffer()); + Future insert(int pos, T value) => _insert(pos, value.writeToBuffer()); - Future insertAll(int pos, List values) async => + Future insertAll(int pos, List values) => _insertAll(pos, values.map((x) => x.writeToBuffer()).toList()); Future get( diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index b4c7eef..0d2320e 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: "direct main" description: name: async_tools - sha256: afd5426e76631172f8ce6a6359b264b092fa9d2a52cd2528100115be9525e067 + sha256: "9611c1efeae7e6d342721d0c2caf2e4783d91fba6a9637d7badfa2dccf8de2a2" url: "https://pub.dev" source: hosted - version: "0.1.9" + version: "0.1.10" bloc: dependency: "direct main" description: @@ -53,10 +53,10 @@ packages: dependency: "direct main" description: name: bloc_advanced_tools - sha256: dfb142569814952af8d93e7fe045972d847e29382471687db59913e253202f6e + sha256: "63e57000df7259e3007dbfbbfd7dae3e0eca60eb2ac93cbe0c5a3de0e77c9972" url: "https://pub.dev" source: hosted - version: "0.1.12" + version: "0.1.13" boolean_selector: dependency: transitive description: @@ -65,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + buffer: + dependency: "direct main" + description: + name: buffer + sha256: "389da2ec2c16283c8787e0adaede82b1842102f8c8aae2f49003a766c5c6b3d1" + url: "https://pub.dev" + source: hosted + version: "1.2.3" build: dependency: transitive description: diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index 8864fa6..5fdb74b 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -7,9 +7,10 @@ environment: sdk: ">=3.2.0 <4.0.0" dependencies: - async_tools: ^0.1.9 + async_tools: ^0.1.10 bloc: ^9.0.0 - bloc_advanced_tools: ^0.1.12 + bloc_advanced_tools: ^0.1.13 + buffer: ^1.2.3 charcode: ^1.4.0 collection: ^1.19.1 convert: ^3.1.2 @@ -29,10 +30,10 @@ dependencies: path: ../../../veilid/veilid-flutter # dependency_overrides: -# async_tools: -# path: ../../../dart_async_tools -# bloc_advanced_tools: -# path: ../../../bloc_advanced_tools +# async_tools: +# path: ../../../dart_async_tools +# bloc_advanced_tools: +# path: ../../../bloc_advanced_tools dev_dependencies: build_runner: ^2.4.15 diff --git a/pubspec.lock b/pubspec.lock index 3b7f823..ae51a3d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -92,10 +92,9 @@ packages: async_tools: dependency: "direct main" description: - name: async_tools - sha256: afd5426e76631172f8ce6a6359b264b092fa9d2a52cd2528100115be9525e067 - url: "https://pub.dev" - source: hosted + path: "../dart_async_tools" + relative: true + source: path version: "0.1.9" auto_size_text: dependency: "direct main" @@ -156,10 +155,9 @@ packages: bloc_advanced_tools: dependency: "direct main" description: - name: bloc_advanced_tools - sha256: dfb142569814952af8d93e7fe045972d847e29382471687db59913e253202f6e - url: "https://pub.dev" - source: hosted + path: "../bloc_advanced_tools" + relative: true + source: path version: "0.1.12" blurry_modal_progress_hud: dependency: "direct main" diff --git a/pubspec.yaml b/pubspec.yaml index fccff99..e605c0e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,13 +15,13 @@ dependencies: animated_theme_switcher: ^2.0.10 ansicolor: ^2.0.3 archive: ^4.0.4 - async_tools: ^0.1.9 + async_tools: ^0.1.10 auto_size_text: ^3.0.0 awesome_extensions: ^2.0.21 badges: ^3.1.2 basic_utils: ^5.8.2 bloc: ^9.0.0 - bloc_advanced_tools: ^0.1.12 + bloc_advanced_tools: ^0.1.13 blurry_modal_progress_hud: ^1.1.1 change_case: ^2.2.0 charcode: ^1.4.0 @@ -108,10 +108,10 @@ dependencies: dependency_overrides: intl: ^0.20.2 # Until flutter_translate updates intl -# async_tools: -# path: ../dart_async_tools -# bloc_advanced_tools: -# path: ../bloc_advanced_tools +# async_tools: +# path: ../dart_async_tools +# bloc_advanced_tools: +# path: ../bloc_advanced_tools # searchable_listview: # path: ../Searchable-Listview # flutter_chat_core: From 2caaf35d52b0dc66873abc0af85ea9026442373e Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 1 Jun 2025 18:26:12 -0400 Subject: [PATCH 258/270] update deps, fix context --- .../views/invitation_dialog.dart | 6 +- lib/layout/home/home_screen.dart | 90 ++++++++++--------- pubspec.lock | 26 ++++-- 3 files changed, 68 insertions(+), 54 deletions(-) diff --git a/lib/contact_invitation/views/invitation_dialog.dart b/lib/contact_invitation/views/invitation_dialog.dart index 3cd0bfb..a5a7fad 100644 --- a/lib/contact_invitation/views/invitation_dialog.dart +++ b/lib/contact_invitation/views/invitation_dialog.dart @@ -291,10 +291,12 @@ class InvitationDialogState extends State { const Icon(Icons.error).paddingAll(16) ]).toCenter(), if (_validInvitation != null && !_isValidating) - Column(children: [ + Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ProfileWidget( profile: _validInvitation!.remoteProfile, - byline: _validInvitation!.remoteProfile.pronouns, + byline: _validInvitation!.remoteProfile.pronouns.isEmpty + ? null + : _validInvitation!.remoteProfile.pronouns, ).paddingLTRB(0, 0, 0, 16), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index c9f1ecb..2aefcd3 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -67,53 +67,55 @@ class HomeScreenState extends State final scaleConfig = theme.extension()!; await showAlertWidgetModal( - context: context, - title: translate('splash.beta_title'), - child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.warning, size: 64.scaled(context)), - RichText( - textScaler: MediaQuery.of(context).textScaler, - textAlign: TextAlign.center, - text: TextSpan( - children: [ - TextSpan( - text: translate('splash.beta_text'), + context: context, + title: translate('splash.beta_title'), + child: Builder( + builder: (context) => + Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Icon(Icons.warning, size: 64.scaled(context)), + RichText( + textScaler: MediaQuery.of(context).textScaler, + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: translate('splash.beta_text'), + style: theme.textTheme.bodyMedium! + .copyWith(color: scale.primaryScale.appText), + ), + TextSpan( + text: 'https://veilid.com/chat/knownissues', + style: theme.textTheme.bodyMedium!.copyWith( + color: scaleConfig.useVisualIndicators + ? scale.secondaryScale.primaryText + : scale.secondaryScale.primary, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => launchUrlString( + 'https://veilid.com/chat/knownissues'), + ), + ], + ), + ), + Row(mainAxisSize: MainAxisSize.min, children: [ + StatefulBuilder( + builder: (context, setState) => Checkbox( + value: displayBetaWarning, + onChanged: (value) { + setState(() { + displayBetaWarning = value ?? true; + }); + }, + )), + Text( + translate('settings_page.display_beta_warning'), style: theme.textTheme.bodyMedium! .copyWith(color: scale.primaryScale.appText), ), - TextSpan( - text: 'https://veilid.com/chat/knownissues', - style: theme.textTheme.bodyMedium!.copyWith( - color: scaleConfig.useVisualIndicators - ? scale.secondaryScale.primaryText - : scale.secondaryScale.primary, - decoration: TextDecoration.underline, - ), - recognizer: TapGestureRecognizer() - ..onTap = () => - launchUrlString('https://veilid.com/chat/knownissues'), - ), - ], - ), - ), - Row(mainAxisSize: MainAxisSize.min, children: [ - StatefulBuilder( - builder: (context, setState) => Checkbox( - value: displayBetaWarning, - onChanged: (value) { - setState(() { - displayBetaWarning = value ?? true; - }); - }, - )), - Text( - translate('settings_page.display_beta_warning'), - style: theme.textTheme.bodyMedium! - .copyWith(color: scale.primaryScale.appText), - ), - ]), - ]), - ); + ]), + ]), + )); final preferencesInstance = PreferencesRepository.instance; await preferencesInstance.set(preferencesInstance.value.copyWith( diff --git a/pubspec.lock b/pubspec.lock index ae51a3d..779806e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -92,10 +92,11 @@ packages: async_tools: dependency: "direct main" description: - path: "../dart_async_tools" - relative: true - source: path - version: "0.1.9" + name: async_tools + sha256: "9611c1efeae7e6d342721d0c2caf2e4783d91fba6a9637d7badfa2dccf8de2a2" + url: "https://pub.dev" + source: hosted + version: "0.1.10" auto_size_text: dependency: "direct main" description: @@ -155,10 +156,11 @@ packages: bloc_advanced_tools: dependency: "direct main" description: - path: "../bloc_advanced_tools" - relative: true - source: path - version: "0.1.12" + name: bloc_advanced_tools + sha256: "63e57000df7259e3007dbfbbfd7dae3e0eca60eb2ac93cbe0c5a3de0e77c9972" + url: "https://pub.dev" + source: hosted + version: "0.1.13" blurry_modal_progress_hud: dependency: "direct main" description: @@ -175,6 +177,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + buffer: + dependency: transitive + description: + name: buffer + sha256: "389da2ec2c16283c8787e0adaede82b1842102f8c8aae2f49003a766c5c6b3d1" + url: "https://pub.dev" + source: hosted + version: "1.2.3" build: dependency: transitive description: From 8fe09555d13933bcf9cdfbf72a29246194c13dbd Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 1 Jun 2025 20:57:02 -0400 Subject: [PATCH 259/270] ensure reconciliation happens when remote conversation is added --- lib/chat/cubits/single_contact_messages_cubit.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index a3e7656..0ec1037 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -198,6 +198,9 @@ class SingleContactMessagesCubit extends Cubit { // Init the new DHTLog if we should _remoteMessagesRecordKey = remoteMessagesRecordKey; await _initRcvdMessagesDHTLog(); + + // Run reconciliation once for all input queues + _reconciliation.reconcileMessages(null); }); } From 3c276d42d9654118ecce6a8e58a6c052a3f5d934 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 1 Jun 2025 21:04:18 -0400 Subject: [PATCH 260/270] changelog --- CHANGELOG.md | 1 + .../test_persistent_queue.dart | 196 ++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 packages/veilid_support/example/integration_test/test_persistent_queue.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f7eefa..a5dc471 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Fixed issue with Android 'back' button exiting the app (#331) - Deprecated accounts no longer crash application at startup - Simplify SingleContactMessagesCubit and MessageReconciliation +- Ensure first messages get received when opening a new chat - Update flutter_chat_ui to 2.0.0 - Accessibility improvements - Text scaling diff --git a/packages/veilid_support/example/integration_test/test_persistent_queue.dart b/packages/veilid_support/example/integration_test/test_persistent_queue.dart new file mode 100644 index 0000000..51f8004 --- /dev/null +++ b/packages/veilid_support/example/integration_test/test_persistent_queue.dart @@ -0,0 +1,196 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:async_tools/async_tools.dart'; +import 'package:collection/collection.dart'; +import 'package:test/test.dart'; +import 'package:veilid_support/veilid_support.dart'; + +Future testPersistentQueueOpenClose() async { + final pq = PersistentQueue( + table: 'persistent_queue_integration_test', + key: 'open_close', + fromBuffer: (buf) => utf8.decode(buf), + toBuffer: (s) => utf8.encode(s), + closure: (elems) async { + // + }); + + await pq.close(); +} + +Future testPersistentQueueAdd() async { + final added = {}; + for (var n = 0; n < 100; n++) { + final elem = 'FOOBAR #$n'; + added.add(elem); + } + + final done = {}; + final pq = PersistentQueue( + table: 'persistent_queue_integration_test', + key: 'add', + fromBuffer: (buf) => utf8.decode(buf), + toBuffer: (s) => utf8.encode(s), + closure: (elems) async { + done.addAll(elems); + }); + + var oddeven = false; + for (final chunk in added.slices(10)) { + if (!oddeven) { + await chunk.map(pq.add).wait; + } else { + await pq.addAll(chunk); + } + oddeven = !oddeven; + } + + await pq.close(); + + expect(done, equals(added)); +} + +Future testPersistentQueueAddSync() async { + final added = {}; + for (var n = 0; n < 100; n++) { + final elem = 'FOOBAR #$n'; + added.add(elem); + } + + final done = {}; + final pq = PersistentQueue( + table: 'persistent_queue_integration_test', + key: 'add_sync', + fromBuffer: (buf) => utf8.decode(buf), + toBuffer: (s) => utf8.encode(s), + closure: (elems) async { + done.addAll(elems); + }); + + var oddeven = false; + for (final chunk in added.slices(10)) { + if (!oddeven) { + await chunk.map((x) async { + await asyncSleep(Duration.zero); + pq.addSync(x); + }).wait; + } else { + pq.addAllSync(chunk); + } + oddeven = !oddeven; + } + + await pq.close(); + + expect(done, equals(added)); +} + +Future testPersistentQueueAddPersist() async { + final added = {}; + for (var n = 0; n < 100; n++) { + final elem = 'FOOBAR #$n'; + added.add(elem); + } + + final done = {}; + + late final PersistentQueue pq; + + pq = PersistentQueue( + table: 'persistent_queue_integration_test', + key: 'add_persist', + fromBuffer: (buf) => utf8.decode(buf), + toBuffer: (s) => utf8.encode(s), + closure: (elems) async { + done.addAll(elems); + }); + + // Start it paused + await pq.pause(); + + // Add all elements + var oddeven = false; + + for (final chunk in added.slices(10)) { + if (!oddeven) { + await chunk.map(pq.add).wait; + } else { + await pq.addAll(chunk); + } + oddeven = !oddeven; + } + + // Close the persistent queue + await pq.close(); + + // Create a new persistent queue that processes the items + final pq2 = PersistentQueue( + table: 'persistent_queue_integration_test', + key: 'add_persist', + fromBuffer: (buf) => utf8.decode(buf), + toBuffer: (s) => utf8.encode(s), + closure: (elems) async { + done.addAll(elems); + }); + await pq2.waitEmpty; + await pq2.close(); + + expect(done, equals(added)); +} + +Future testPersistentQueueAddSyncPersist() async { + final added = {}; + for (var n = 0; n < 100; n++) { + final elem = 'FOOBAR #$n'; + added.add(elem); + } + + final done = {}; + + late final PersistentQueue pq; + + pq = PersistentQueue( + table: 'persistent_queue_integration_test', + key: 'add_persist', + fromBuffer: (buf) => utf8.decode(buf), + toBuffer: (s) => utf8.encode(s), + closure: (elems) async { + done.addAll(elems); + }); + + // Start it paused + await pq.pause(); + + // Add all elements + var oddeven = false; + + for (final chunk in added.slices(10)) { + if (!oddeven) { + await chunk.map((x) async { + await asyncSleep(Duration.zero); + pq.addSync(x); + }).wait; + } else { + pq.addAllSync(chunk); + } + oddeven = !oddeven; + } + + // Close the persistent queue + await pq.close(); + + // Create a new persistent queue that processes the items + final pq2 = PersistentQueue( + table: 'persistent_queue_integration_test', + key: 'add_persist', + fromBuffer: (buf) => utf8.decode(buf), + toBuffer: (s) => utf8.encode(s), + closure: (elems) async { + done.addAll(elems); + }); + await pq2.waitEmpty; + await pq2.close(); + + expect(done, equals(added)); +} From 04092fed74e89657d18a1f0e4a7159972d56f016 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 1 Jun 2025 21:05:09 -0400 Subject: [PATCH 261/270] oops --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5dc471..2e11c9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ - Accessibility improvements - Text scaling - Keyboard shortcuts Ctrl + / Ctrl - to change font size + - Keyboard shortcut Ctrl+Alt+C - to change color scheme + - Keyboard shortcut Ctrl+Alt+B - to change theme brightness ## v0.4.7 ## - *Community Contributions* From ee37d0dbba1b486ab5974f4d333ad6348795894c Mon Sep 17 00:00:00 2001 From: iKranium-Labs Date: Tue, 3 Jun 2025 17:47:09 +0000 Subject: [PATCH 262/270] refactor README.md for clarity and structure; enhance setup instructions and... --- README.md | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 160 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index dc02697..5e303cd 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,175 @@ # VeilidChat -VeilidChat is a chat application written for the Veilid (https://www.veilid.com) distributed application platform. It has a familiar and simple interface and is designed for private, and secure person-to-person communications. +## Overview -For more information about VeilidChat: https://veilid.com/chat/ +VeilidChat is a decentralized, secure, and private chat application built upon the [Veilid](https://www.veilid.com) distributed application platform. It offers a familiar messaging interface while leveraging Veilid's underlying end-to-end encrypted and peer-to-peer communication capabilities to ensure privacy and security for person-to-person communications without relying on centralized servers. -For more information about the Veilid network protocol and app development platform: https://veilid.com +For more information about VeilidChat: + +For more information about the Veilid network protocol and app development platform: ## Setup -While this is still in development, you must have a clone of the Veilid source checked out at `../veilid` relative to the working directory of this repository. +### Prerequisites + +VeilidChat is a Flutter application that interacts with the core Veilid library via Foreign Function Interface (FFI). While this is still in development, you **must** have a clone of the Veilid source checked out at `../veilid` relative to the working directory of this repository for the setup scripts to work. This is because the veilid-core source and build setup (including FFI components), which Veilidchat relies on, reside there. -### For Linux Systems: ``` -./dev-setup/setup_linux.sh +your_workspace/ +├── veilid/ <-- Veilid core repository +└── veilidchat/ <-- This repository ``` -### For Mac Systems: -``` -./dev-setup/setup_macos.sh +Refer to the main [Veilid repository](https://gitlab.com/veilid/veilid) for instructions. + +
+ +### Flutter Installation + +VeilidChat requires the Flutter SDK to build and run. Ensure Flutter is installed and available in your system's PATH. Choose one of the methods below: + + +**Option 1: Standard Installation (Recommended)** + +Follow the official Flutter documentation for your operating system to install the SDK directly. + +* **Official Flutter Install Guides:** + * [Windows](https://docs.flutter.dev/get-started/install/windows) + * [macOS](https://docs.flutter.dev/get-started/install/macos) + * [Linux](https://docs.flutter.dev/get-started/install/linux) + * [ChromeOS](https://docs.flutter.dev/get-started/install/chromeos) + + +**Option 2: Installation via IDE Extension (Beginner-friendly for VS Code)** + +Varioius IDEs may offer Flutter extensions that can assist with setup. For VS Code, the official Flutter extension can assist you through the SDK installation process. + +1. Open VS Code. +2. Go to the Extensions view (`Ctrl+Shift+X` or `Cmd+Shift+X`). +3. Search for "Flutter" and install the official extension published by Dart Code. +4. Follow the prompts from the extension to install the Flutter SDK. + +
+ +**Running Veilid Core Setup Scripts:** + +In order to run the VeilidChat application, you will need the [Veilid repository](https://gitlab.com/veilid/veilid) repository set up correctly as mentioned in [Prerequisites](#prerequisites) above. The veilidchat setup scripts in [`./dev-setup`](./dev-setup) handle building the necessary Veilid components and checking for or installing other required tools like `protoc`. + +**Note:** These scripts require Flutter to be **already installed and accessible in your system's PATH** before they are run. + +To run these setup scripts (from the [`veilidchat`](veilidchat) directory): + +* For Linux Systems: Run [./dev-setup/setup_linux.sh](./dev-setup/setup_linux.sh) (Installs protoc and protoc-gen-dart) +* For Mac Systems: Run [./dev-setup/setup_macos.sh](./dev-setup/setup_macos.sh) (Installs protoc and protoc-gen-dart) +* For Windows Systems: Run [./dev-setup/setup_windows.bat](./dev-setup/setup_windows.bat) (**Requires manual installation of protoc beforehand**) + (check [`./dev-setup`](./dev-setup) for other platforms) + +These scripts will check for required dependencies and set up the environment. For Windows users, please ensure you have manually downloaded and installed the protoc compiler (version 25.3 or higher is recommended) and added its directory to your system's PATH *before* running the setup script. The Windows script will verify its presence. + + +**Note on Python Environments:** The dev-setup scripts in the main Veilid repository may utilize Python virtual environments (`venv`) for managing dependencies needed for build processes (like FFI generation scripts). If your system uses a system-managed Python environment or you encounter permission issues, ensure you follow any instructions provided by the setup scripts regarding environment activation or configuration. + + +### Verifying Installation + +After installing Flutter and running the ./dev-setup scripts, verify that everything is set up correctly. Open a terminal and run the following command from anywhere accessible by your PATH: + +`$ flutter doctor` + +This command checks your environment and displays a report of the status of your Flutter installation and connected devices. It will list any missing dependencies or configuration issues for common development platforms (Android, iOS, Web, Desktop). + + +**Example Output (Partial):** ``` -## Updating Code +$ flutter doctor +Doctor summary (to see all details, run flutter doctor -v): +[√] Flutter (Channel stable, 3.x.x, on macOS 13.x.x 22Gxxx darwin-x64, locale en-US) +[√] Android toolchain - develop for Android devices (Android SDK version 3x.x.x) +[!] Xcode - develop for iOS and macOS + ✗ Xcode installation is incomplete. + Install Xcode from the App Store. +[√] Chrome - develop for the web +[√] Linux toolchain - develop for Linux desktop +[√] VS Code (version 1.xx.x) +[√] Connected device (1 available) -### To update the WASM binary from `veilid-wasm`: -* Debug WASM: run `./dev-setup/wasm_update.sh` -* Release WASM: run `./dev-setup/wasm_update.sh release` +! Doctor found issues in 1 category. +Address any issues reported by `flutter doctor` before proceeding. +``` +
+ +## Building and Launching + +VeilidChat is a Flutter application and can be built and launched on various platforms supported by Flutter, provided you have the necessary SDKs and devices/emulators configured as verified by `flutter doctor`. + +1. **Ensure Flutter dependencies are installed**: + From the `veilidchat` directory, run: + ```bash + flutter pub get + ``` + +2. **List available devices**: + To see which devices (simulators, emulators, connected physical devices, desktop targets, web browsers) are available to run the application on, use the command: + ```bash + flutter devices + ``` + +3. **Run on a specific device**: + Use the `flutter run` command followed by the `-d` flag and the device ID from the `flutter devices` list. + + * **Example (Android emulator/device):** + Assuming an Android device ID like `emulator-5554`: + ```bash + flutter run -d emulator-5554 + ``` + If only one device is connected, you can often omit the `-d` flag. + + * **Example (iOS simulator/device):** + Assuming an iOS simulator ID like `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`: + ```bash + flutter run -d xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` (replace with actual ID) + ``` + Or, to target the default iOS simulator: + ```bash + flutter run -d simulator + ``` + + * **Example (Linux Desktop):** + ```bash + flutter run -d linux + ``` + + * **Example (macOS Desktop):** + ```bash + flutter run -d macos + ``` + + * **Example (Windows Desktop):** + ```bash + flutter run -d windows + ``` + + * **Example (Web):** + ```bash + flutter run -d web # Or a specific browser like 'chrome' or 'firefox' if listed by `flutter devices` + ``` + This will typically launch the application in your selected web browser. + +
+ +## Updating the WASM Binary + +### To update the WASM binary +[from the `veilid-wasm` package located in [`../veilid/veilid-wasm`](../veilid/veilid-wasm)] + + +From the VeilidChat repository working directory ([`./veilidchat`](./veilidchat)), run the appropriate script: + +* Debug WASM: [`./dev-setup/wasm_update.sh`](./dev-setup/wasm_update.sh) +* Release WASM: [`./dev-setup/wasm_update.sh release`](./dev-setup/wasm_update.sh) + +
+ +Refer to the official [Flutter documentation](https://docs.flutter.dev/) for more detailed information on building and deployment with Flutter. \ No newline at end of file From 0ad7a1c0c1323187cd238886a3b7684e98e3ee0a Mon Sep 17 00:00:00 2001 From: TC Johnson Date: Tue, 3 Jun 2025 13:00:38 -0500 Subject: [PATCH 263/270] Updated CHANGELOG for v0.4.8 release Also corrected some typos and linting in the new README.md --- CHANGELOG.md | 9 ++++-- README.md | 83 ++++++++++++++++++++++++++-------------------------- 2 files changed, 47 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e11c9c..21576d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## UNRELEASED ## +## v0.4.8 ## - Fix reconciliation `advance()` - Add `pool stats` command @@ -13,8 +13,11 @@ - Keyboard shortcut Ctrl+Alt+C - to change color scheme - Keyboard shortcut Ctrl+Alt+B - to change theme brightness +- _Community Contributions_ + - Refactor README.md for clarity and structure; enhance setup instructions and Flutter installation guidance @iKranium-Labs // @iKranium + ## v0.4.7 ## -- *Community Contributions* +- _Community Contributions_ - Fix getting stuck on splash screen when veilid is already started @bmv437 / @bgrift - Fix routing to home after initial account creation @bmv437 / @bgrift - edit_account_form visual improvements @bmv437 / @bgrift @@ -79,7 +82,7 @@ - Support away/busy/free state - Support away/busy/free messages - Start of UI for auto-away feature (incomplete) -- *Community Contributions Shoutouts* +- _Community Contributions Shoutouts_ - @ethnh - @sajattack - @jasikpark diff --git a/README.md b/README.md index 5e303cd..c20c08e 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ For more information about the Veilid network protocol and app development platf VeilidChat is a Flutter application that interacts with the core Veilid library via Foreign Function Interface (FFI). While this is still in development, you **must** have a clone of the Veilid source checked out at `../veilid` relative to the working directory of this repository for the setup scripts to work. This is because the veilid-core source and build setup (including FFI components), which Veilidchat relies on, reside there. -``` +```shell your_workspace/ ├── veilid/ <-- Veilid core repository └── veilidchat/ <-- This repository @@ -22,34 +22,28 @@ your_workspace/ Refer to the main [Veilid repository](https://gitlab.com/veilid/veilid) for instructions. -
- ### Flutter Installation VeilidChat requires the Flutter SDK to build and run. Ensure Flutter is installed and available in your system's PATH. Choose one of the methods below: - **Option 1: Standard Installation (Recommended)** Follow the official Flutter documentation for your operating system to install the SDK directly. -* **Official Flutter Install Guides:** - * [Windows](https://docs.flutter.dev/get-started/install/windows) - * [macOS](https://docs.flutter.dev/get-started/install/macos) - * [Linux](https://docs.flutter.dev/get-started/install/linux) - * [ChromeOS](https://docs.flutter.dev/get-started/install/chromeos) - +* **Official Flutter Install Guides:** + * [Windows](https://docs.flutter.dev/get-started/install/windows) + * [macOS](https://docs.flutter.dev/get-started/install/macos) + * [Linux](https://docs.flutter.dev/get-started/install/linux) + * [ChromeOS](https://docs.flutter.dev/get-started/install/chromeos) **Option 2: Installation via IDE Extension (Beginner-friendly for VS Code)** -Varioius IDEs may offer Flutter extensions that can assist with setup. For VS Code, the official Flutter extension can assist you through the SDK installation process. +Various IDEs may offer Flutter extensions that can assist with setup. For VS Code, the official Flutter extension can assist you through the SDK installation process. -1. Open VS Code. -2. Go to the Extensions view (`Ctrl+Shift+X` or `Cmd+Shift+X`). -3. Search for "Flutter" and install the official extension published by Dart Code. -4. Follow the prompts from the extension to install the Flutter SDK. - -
+1. Open VS Code. +2. Go to the Extensions view (`Ctrl+Shift+X` or `Cmd+Shift+X`). +3. Search for "Flutter" and install the official extension published by Dart Code. +4. Follow the prompts from the extension to install the Flutter SDK. **Running Veilid Core Setup Scripts:** @@ -59,18 +53,16 @@ In order to run the VeilidChat application, you will need the [Veilid repository To run these setup scripts (from the [`veilidchat`](veilidchat) directory): -* For Linux Systems: Run [./dev-setup/setup_linux.sh](./dev-setup/setup_linux.sh) (Installs protoc and protoc-gen-dart) -* For Mac Systems: Run [./dev-setup/setup_macos.sh](./dev-setup/setup_macos.sh) (Installs protoc and protoc-gen-dart) -* For Windows Systems: Run [./dev-setup/setup_windows.bat](./dev-setup/setup_windows.bat) (**Requires manual installation of protoc beforehand**) +* For Linux Systems: Run [./dev-setup/setup_linux.sh](./dev-setup/setup_linux.sh) (Installs protoc and protoc-gen-dart) +* For Mac Systems: Run [./dev-setup/setup_macos.sh](./dev-setup/setup_macos.sh) (Installs protoc and protoc-gen-dart) +* For Windows Systems: Run [./dev-setup/setup_windows.bat](./dev-setup/setup_windows.bat) (**Requires manual installation of protoc beforehand**) (check [`./dev-setup`](./dev-setup) for other platforms) These scripts will check for required dependencies and set up the environment. For Windows users, please ensure you have manually downloaded and installed the protoc compiler (version 25.3 or higher is recommended) and added its directory to your system's PATH *before* running the setup script. The Windows script will verify its presence. - **Note on Python Environments:** The dev-setup scripts in the main Veilid repository may utilize Python virtual environments (`venv`) for managing dependencies needed for build processes (like FFI generation scripts). If your system uses a system-managed Python environment or you encounter permission issues, ensure you follow any instructions provided by the setup scripts regarding environment activation or configuration. - -### Verifying Installation +## Verifying Installation After installing Flutter and running the ./dev-setup scripts, verify that everything is set up correctly. Open a terminal and run the following command from anywhere accessible by your PATH: @@ -78,9 +70,9 @@ After installing Flutter and running the ./dev-setup scripts, verify that everyt This command checks your environment and displays a report of the status of your Flutter installation and connected devices. It will list any missing dependencies or configuration issues for common development platforms (Android, iOS, Web, Desktop). - **Example Output (Partial):** -``` + +```shell $ flutter doctor Doctor summary (to see all details, run flutter doctor -v): @@ -98,78 +90,85 @@ Doctor summary (to see all details, run flutter doctor -v): Address any issues reported by `flutter doctor` before proceeding. ``` -
## Building and Launching VeilidChat is a Flutter application and can be built and launched on various platforms supported by Flutter, provided you have the necessary SDKs and devices/emulators configured as verified by `flutter doctor`. -1. **Ensure Flutter dependencies are installed**: +1. **Ensure Flutter dependencies are installed**: From the `veilidchat` directory, run: + ```bash flutter pub get ``` -2. **List available devices**: +2. **List available devices**: To see which devices (simulators, emulators, connected physical devices, desktop targets, web browsers) are available to run the application on, use the command: + ```bash flutter devices ``` -3. **Run on a specific device**: +3. **Run on a specific device**: Use the `flutter run` command followed by the `-d` flag and the device ID from the `flutter devices` list. - * **Example (Android emulator/device):** + * **Example (Android emulator/device):** Assuming an Android device ID like `emulator-5554`: + ```bash flutter run -d emulator-5554 ``` + If only one device is connected, you can often omit the `-d` flag. - * **Example (iOS simulator/device):** + * **Example (iOS simulator/device):** Assuming an iOS simulator ID like `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`: + ```bash flutter run -d xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` (replace with actual ID) ``` + Or, to target the default iOS simulator: + ```bash flutter run -d simulator ``` - * **Example (Linux Desktop):** + * **Example (Linux Desktop):** + ```bash flutter run -d linux ``` - * **Example (macOS Desktop):** + * **Example (macOS Desktop):** + ```bash flutter run -d macos ``` - * **Example (Windows Desktop):** + * **Example (Windows Desktop):** + ```bash flutter run -d windows ``` - * **Example (Web):** + * **Example (Web):** + ```bash flutter run -d web # Or a specific browser like 'chrome' or 'firefox' if listed by `flutter devices` ``` - This will typically launch the application in your selected web browser. -
+ This will typically launch the application in your selected web browser. ## Updating the WASM Binary ### To update the WASM binary -[from the `veilid-wasm` package located in [`../veilid/veilid-wasm`](../veilid/veilid-wasm)] +[from the `veilid-wasm` package located in [`../veilid/veilid-wasm`](../veilid/veilid-wasm)] From the VeilidChat repository working directory ([`./veilidchat`](./veilidchat)), run the appropriate script: -* Debug WASM: [`./dev-setup/wasm_update.sh`](./dev-setup/wasm_update.sh) -* Release WASM: [`./dev-setup/wasm_update.sh release`](./dev-setup/wasm_update.sh) - -
+* Debug WASM: [`./dev-setup/wasm_update.sh`](./dev-setup/wasm_update.sh) +* Release WASM: [`./dev-setup/wasm_update.sh release`](./dev-setup/wasm_update.sh) Refer to the official [Flutter documentation](https://docs.flutter.dev/) for more detailed information on building and deployment with Flutter. \ No newline at end of file From 2f597ef1a20f2b6437763972a68554bda18f9916 Mon Sep 17 00:00:00 2001 From: TC Johnson Date: Tue, 3 Jun 2025 13:02:10 -0500 Subject: [PATCH 264/270] =?UTF-8?q?Version=20update:=20v0.4.7=20=E2=86=92?= =?UTF-8?q?=20v0.4.8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index a5b4502..84b9f17 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.4.7+0 +current_version = 0.4.8+0 commit = False tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)\+(?P\d+) diff --git a/pubspec.yaml b/pubspec.yaml index e605c0e..cee2ec7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: veilidchat description: VeilidChat publish_to: "none" -version: 0.4.7+20 +version: 0.4.8+21 environment: sdk: ">=3.2.0 <4.0.0" From aeaf34e55ddea9d1af63369c422cced394996374 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 2 Jun 2025 15:47:37 -0400 Subject: [PATCH 265/270] fix lints --- lib/contacts/views/contacts_browser.dart | 8 +++--- .../lib/dht_support/src/dht_log/dht_log.dart | 14 +++++----- .../src/dht_log/dht_log_cubit.dart | 8 +++--- .../src/dht_log/dht_log_spine.dart | 27 ++++++++++--------- .../src/dht_log/dht_log_write.dart | 6 ++--- .../src/dht_record/dht_record.dart | 14 +++++----- .../src/dht_record/dht_record_pool.dart | 26 +++++++++--------- .../dht_record/dht_record_pool_private.dart | 4 ++- .../lib/dht_support/src/dht_record/stats.dart | 5 ++++ .../src/dht_short_array/dht_short_array.dart | 16 +++++------ .../src/interfaces/dht_closeable.dart | 2 +- .../src/interfaces/refreshable_cubit.dart | 2 +- 12 files changed, 70 insertions(+), 62 deletions(-) diff --git a/lib/contacts/views/contacts_browser.dart b/lib/contacts/views/contacts_browser.dart index 22c64c7..c04a4e4 100644 --- a/lib/contacts/views/contacts_browser.dart +++ b/lib/contacts/views/contacts_browser.dart @@ -83,11 +83,11 @@ class _ContactsBrowserState extends State final menuIconColor = scaleConfig.preferBorders ? scaleScheme.primaryScale.hoverBorder : scaleScheme.primaryScale.hoverBorder; - final menuBackgroundColor = scaleConfig.preferBorders - ? scaleScheme.primaryScale.activeElementBackground - : scaleScheme.primaryScale.activeElementBackground; + // final menuBackgroundColor = scaleConfig.preferBorders + // ? scaleScheme.primaryScale.activeElementBackground + // : scaleScheme.primaryScale.activeElementBackground; - final menuBorderColor = scaleScheme.primaryScale.hoverBorder; + // final menuBorderColor = scaleScheme.primaryScale.hoverBorder; PopupMenuEntry makeMenuButton( {required IconData iconData, diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart index 2687bdc..20471da 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart @@ -211,12 +211,12 @@ class DHTLog implements DHTDeleteable { OwnedDHTRecordPointer get recordPointer => _spine.recordPointer; /// Runs a closure allowing read-only access to the log - Future operate(Future Function(DHTLogReadOperations) closure) async { + Future operate(Future Function(DHTLogReadOperations) closure) { if (!isOpen) { throw StateError('log is not open'); } - return _spine.operate((spine) async { + return _spine.operate((spine) { final reader = _DHTLogRead._(spine); return closure(reader); }); @@ -228,12 +228,12 @@ class DHTLog implements DHTDeleteable { /// Throws DHTOperateException if the write could not be performed /// at this time Future operateAppend( - Future Function(DHTLogWriteOperations) closure) async { + Future Function(DHTLogWriteOperations) closure) { if (!isOpen) { throw StateError('log is not open'); } - return _spine.operateAppend((spine) async { + return _spine.operateAppend((spine) { final writer = _DHTLogWrite._(spine); return closure(writer); }); @@ -247,12 +247,12 @@ class DHTLog implements DHTDeleteable { /// eventual consistency pass. Future operateAppendEventual( Future Function(DHTLogWriteOperations) closure, - {Duration? timeout}) async { + {Duration? timeout}) { if (!isOpen) { throw StateError('log is not open'); } - return _spine.operateAppendEventual((spine) async { + return _spine.operateAppendEventual((spine) { final writer = _DHTLogWrite._(spine); return closure(writer); }, timeout: timeout); @@ -308,7 +308,7 @@ class DHTLog implements DHTDeleteable { int _openCount; // Watch mutex to ensure we keep the representation valid - final Mutex _listenMutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); + final _listenMutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); // Stream of external changes StreamController? _watchController; } diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart index a7884f9..3a618dc 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_cubit.dart @@ -106,13 +106,13 @@ class DHTLogCubit extends Cubit> await _refreshNoWait(forceRefresh: forceRefresh); } - Future _refreshNoWait({bool forceRefresh = false}) async => - busy((emit) async => _refreshInner(emit, forceRefresh: forceRefresh)); + Future _refreshNoWait({bool forceRefresh = false}) => + busy((emit) => _refreshInner(emit, forceRefresh: forceRefresh)); Future _refreshInner(void Function(AsyncValue>) emit, {bool forceRefresh = false}) async { late final int length; - final windowElements = await _log.operate((reader) async { + final windowElements = await _log.operate((reader) { length = reader.length; return _loadElementsFromReader(reader, _windowTail, _windowSize); }); @@ -237,7 +237,7 @@ class DHTLogCubit extends Cubit> late final DHTLog _log; final T Function(List data) _decodeElement; StreamSubscription? _subscription; - bool _wantsCloseRecord = false; + var _wantsCloseRecord = false; final _sspUpdate = SingleStatelessProcessor(); // Accumulated deltas since last update diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index 6323692..0aebd6c 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -44,6 +44,8 @@ class _SubkeyData { _SubkeyData({required this.subkey, required this.data}); int subkey; Uint8List data; + // lint conflict + // ignore: omit_obvious_property_types bool changed = false; } @@ -123,12 +125,12 @@ class _DHTLogSpine { } // Will deep delete all segment records as they are children - Future delete() async => _spineMutex.protect(_spineRecord.delete); + Future delete() => _spineMutex.protect(_spineRecord.delete); - Future operate(Future Function(_DHTLogSpine) closure) async => - _spineMutex.protect(() async => closure(this)); + Future operate(Future Function(_DHTLogSpine) closure) => + _spineMutex.protect(() => closure(this)); - Future operateAppend(Future Function(_DHTLogSpine) closure) async => + Future operateAppend(Future Function(_DHTLogSpine) closure) => _spineMutex.protect(() async { final oldHead = _head; final oldTail = _tail; @@ -150,7 +152,7 @@ class _DHTLogSpine { }); Future operateAppendEventual(Future Function(_DHTLogSpine) closure, - {Duration? timeout}) async { + {Duration? timeout}) { final timeoutTs = timeout == null ? null : Veilid.instance.now().offset(TimestampDuration.fromDuration(timeout)); @@ -264,7 +266,7 @@ class _DHTLogSpine { ///////////////////////////////////////////////////////////////////////////// // Spine element management - static final Uint8List _emptySegmentKey = + static final _emptySegmentKey = Uint8List.fromList(List.filled(TypedKey.decodedLength(), 0)); static Uint8List _makeEmptySubkey() => Uint8List.fromList(List.filled( DHTLog.segmentsPerSubkey * TypedKey.decodedLength(), 0)); @@ -420,7 +422,7 @@ class _DHTLogSpine { Future<_DHTLogPosition?> lookupPositionBySegmentNumber( int segmentNumber, int segmentPos, - {bool onlyOpened = false}) async => + {bool onlyOpened = false}) => _spineCacheMutex.protect(() async { // See if we have this segment opened already final openedSegment = _openedSegments[segmentNumber]; @@ -468,7 +470,7 @@ class _DHTLogSpine { segmentNumber: segmentNumber); }); - Future<_DHTLogPosition?> lookupPosition(int pos) async { + Future<_DHTLogPosition?> lookupPosition(int pos) { assert(_spineMutex.isLocked, 'should be locked'); // Check if our position is in bounds @@ -487,7 +489,7 @@ class _DHTLogSpine { return lookupPositionBySegmentNumber(segmentNumber, segmentPos); } - Future _segmentClosed(int segmentNumber) async { + Future _segmentClosed(int segmentNumber) { assert(_spineMutex.isLocked, 'should be locked'); return _spineCacheMutex.protect(() async { final sa = _openedSegments[segmentNumber]!; @@ -709,7 +711,7 @@ class _DHTLogSpine { DHTShortArray.maxElements; // Spine head mutex to ensure we keep the representation valid - final Mutex _spineMutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); + final _spineMutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); // Subscription to head record internal changes StreamSubscription? _subscription; // Notify closure for external spine head changes @@ -729,9 +731,8 @@ class _DHTLogSpine { // LRU cache of DHT spine elements accessed recently // Pair of position and associated shortarray segment - final Mutex _spineCacheMutex = - Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); + final _spineCacheMutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); final List _openCache; final Map _openedSegments; - static const int _openCacheSize = 3; + static const _openCacheSize = 3; } diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart index 397a1dc..00eaa94 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart @@ -60,7 +60,7 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { if (aLookup.shortArray == bLookup.shortArray) { await bLookup.close(); return aLookup.scope((sa) => sa.operateWriteEventual( - (aWrite) async => aWrite.swap(aLookup.pos, bLookup.pos))); + (aWrite) => aWrite.swap(aLookup.pos, bLookup.pos))); } else { final bItem = Output(); return aLookup.scope( @@ -101,7 +101,7 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { } // Write item to the segment - return lookup.scope((sa) async => sa.operateWrite((write) async { + return lookup.scope((sa) => sa.operateWrite((write) async { // If this a new segment, then clear it in case we have wrapped around if (lookup.pos == 0) { await write.clear(); @@ -140,7 +140,7 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { dws.add((_) async { try { - await lookup.scope((sa) async => sa.operateWrite((write) async { + await lookup.scope((sa) => sa.operateWrite((write) async { // If this a new segment, then clear it in // case we have wrapped around if (lookup.pos == 0) { 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 b9218e4..893cb80 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 @@ -92,7 +92,7 @@ class DHTRecord implements DHTDeleteable { /// Returns true if the deletion was processed immediately /// Returns false if the deletion was marked for later @override - Future delete() async => DHTRecordPool.instance.deleteRecord(key); + Future delete() => DHTRecordPool.instance.deleteRecord(key); //////////////////////////////////////////////////////////////////////////// // Public API @@ -122,7 +122,7 @@ class DHTRecord implements DHTDeleteable { {int subkey = -1, VeilidCrypto? crypto, DHTRecordRefreshMode refreshMode = DHTRecordRefreshMode.cached, - Output? outSeqNum}) async => + Output? outSeqNum}) => _wrapStats('get', () async { subkey = subkeyOrDefault(subkey); @@ -227,7 +227,7 @@ class DHTRecord implements DHTDeleteable { {int subkey = -1, VeilidCrypto? crypto, KeyPair? writer, - Output? outSeqNum}) async => + Output? outSeqNum}) => _wrapStats('tryWriteBytes', () async { subkey = subkeyOrDefault(subkey); final lastSeq = await _localSubkeySeq(subkey); @@ -238,8 +238,8 @@ class DHTRecord implements DHTDeleteable { key, subkey, encryptedNewValue, writer: 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 + // 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(key, subkey); if (newValueData == null) { assert(newValueData != null, "can't get value that was just set"); @@ -280,7 +280,7 @@ class DHTRecord implements DHTDeleteable { {int subkey = -1, VeilidCrypto? crypto, KeyPair? writer, - Output? outSeqNum}) async => + Output? outSeqNum}) => _wrapStats('eventualWriteBytes', () async { subkey = subkeyOrDefault(subkey); final lastSeq = await _localSubkeySeq(subkey); @@ -331,7 +331,7 @@ class DHTRecord implements DHTDeleteable { {int subkey = -1, VeilidCrypto? crypto, KeyPair? writer, - Output? outSeqNum}) async => + Output? outSeqNum}) => _wrapStats('eventualUpdateBytes', () async { subkey = subkeyOrDefault(subkey); 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 6dc0634..cfa123d 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 @@ -20,13 +20,13 @@ part 'dht_record.dart'; part 'dht_record_pool_private.dart'; /// Maximum number of concurrent DHT operations to perform on the network -const int kMaxDHTConcurrency = 8; +const kMaxDHTConcurrency = 8; /// Total number of times to try in a 'VeilidAPIExceptionKeyNotFound' loop -const int kDHTKeyNotFoundTries = 3; +const kDHTKeyNotFoundTries = 3; /// Total number of times to try in a 'VeilidAPIExceptionTryAgain' loop -const int kDHTTryAgainTries = 3; +const kDHTTryAgainTries = 3; typedef DHTRecordPoolLogger = void Function(String message); @@ -105,7 +105,7 @@ class DHTRecordPool with TableDBBackedJson { int defaultSubkey = 0, VeilidCrypto? crypto, KeyPair? writer, - }) async => + }) => _mutex.protect(() async { final dhtctx = routingContext ?? _routingContext; @@ -139,7 +139,7 @@ class DHTRecordPool with TableDBBackedJson { VeilidRoutingContext? routingContext, TypedKey? parent, int defaultSubkey = 0, - VeilidCrypto? crypto}) async => + VeilidCrypto? crypto}) => _recordTagLock.protect(recordKey, closure: () async { final dhtctx = routingContext ?? _routingContext; @@ -164,7 +164,7 @@ class DHTRecordPool with TableDBBackedJson { TypedKey? parent, int defaultSubkey = 0, VeilidCrypto? crypto, - }) async => + }) => _recordTagLock.protect(recordKey, closure: () async { final dhtctx = routingContext ?? _routingContext; @@ -223,8 +223,8 @@ class DHTRecordPool with TableDBBackedJson { /// otherwise mark that record for deletion eventually /// Returns true if the deletion was processed immediately /// Returns false if the deletion was marked for later - Future deleteRecord(TypedKey recordKey) async => - _mutex.protect(() async => _deleteRecordInner(recordKey)); + Future deleteRecord(TypedKey recordKey) => + _mutex.protect(() => _deleteRecordInner(recordKey)); // If everything underneath is closed including itself, return the // list of children (and itself) to finally actually delete @@ -314,7 +314,7 @@ class DHTRecordPool with TableDBBackedJson { /// Generate default VeilidCrypto for a writer static Future privateCryptoFromTypedSecret( - TypedKey typedSecret) async => + TypedKey typedSecret) => VeilidCryptoPrivate.fromTypedKey(typedSecret, _cryptoDomainDHT); //////////////////////////////////////////////////////////////////////////// @@ -362,7 +362,7 @@ class DHTRecordPool with TableDBBackedJson { required VeilidCrypto crypto, required KeyPair? writer, required TypedKey? parent, - required int defaultSubkey}) async => + required int defaultSubkey}) => _stats.measure(recordKey, debugName, '_recordOpenCommon', () async { log('openDHTRecord: debugName=$debugName key=$recordKey'); @@ -428,8 +428,8 @@ class DHTRecordPool with TableDBBackedJson { // Already opened - // See if we need to reopen the record with a default writer and possibly - // a different routing context + // 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) { await dhtctx.openDHTRecord(recordKey, writer: writer); // New writer if we didn't specify one before @@ -889,7 +889,7 @@ class DHTRecordPool with TableDBBackedJson { } /// Ticker to check watch state change requests - Future tick() async => _mutex.protect(() async { + Future tick() => _mutex.protect(() async { // See if any opened records need watch state changes for (final kv in _opened.entries) { final openedRecordKey = kv.key; diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool_private.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool_private.dart index 05b93b0..2474c87 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool_private.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool_private.dart @@ -1,7 +1,7 @@ part of 'dht_record_pool.dart'; // DHT crypto domain -const String _cryptoDomainDHT = 'dht'; +const _cryptoDomainDHT = 'dht'; // Singlefuture keys const _sfPollWatch = '_pollWatch'; @@ -32,6 +32,8 @@ class _SharedDHTRecordData { DHTRecordDescriptor recordDescriptor; KeyPair? defaultWriter; VeilidRoutingContext defaultRoutingContext; + // lint conflict + // ignore: omit_obvious_property_types bool needsWatchStateUpdate = false; _WatchState? unionWatchState; } diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/stats.dart b/packages/veilid_support/lib/dht_support/src/dht_record/stats.dart index 6388f5c..22b517b 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/stats.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/stats.dart @@ -80,7 +80,12 @@ class DHTCallStats { ' all latency: ${latency?.debugString()}\n'; ///////////////////////////// + + // lint conflict + // ignore: omit_obvious_property_types int calls = 0; + // lint conflict + // ignore: omit_obvious_property_types int timeouts = 0; LatencyStats? latency; LatencyStats? successLatency; 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 ccf7d18..4a03cd9 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 @@ -174,7 +174,7 @@ class DHTShortArray implements DHTDeleteable { /// Returns true if the deletion was processed immediately /// Returns false if the deletion was marked for later @override - Future delete() async => _head.delete(); + Future delete() => _head.delete(); //////////////////////////////////////////////////////////////////////////// // Public API @@ -201,12 +201,12 @@ class DHTShortArray implements DHTDeleteable { /// Runs a closure allowing read-only access to the shortarray Future operate( - Future Function(DHTShortArrayReadOperations) closure) async { + Future Function(DHTShortArrayReadOperations) closure) { if (!isOpen) { throw StateError('short array is not open"'); } - return _head.operate((head) async { + return _head.operate((head) { final reader = _DHTShortArrayRead._(head); return closure(reader); }); @@ -218,12 +218,12 @@ class DHTShortArray implements DHTDeleteable { /// Throws DHTOperateException if the write could not be performed /// at this time Future operateWrite( - Future Function(DHTShortArrayWriteOperations) closure) async { + Future Function(DHTShortArrayWriteOperations) closure) { if (!isOpen) { throw StateError('short array is not open"'); } - return _head.operateWrite((head) async { + return _head.operateWrite((head) { final writer = _DHTShortArrayWrite._(head); return closure(writer); }); @@ -237,12 +237,12 @@ class DHTShortArray implements DHTDeleteable { /// eventual consistency pass. Future operateWriteEventual( Future Function(DHTShortArrayWriteOperations) closure, - {Duration? timeout}) async { + {Duration? timeout}) { if (!isOpen) { throw StateError('short array is not open"'); } - return _head.operateWriteEventual((head) async { + return _head.operateWriteEventual((head) { final writer = _DHTShortArrayWrite._(head); return closure(writer); }, timeout: timeout); @@ -291,7 +291,7 @@ class DHTShortArray implements DHTDeleteable { int _openCount; // Watch mutex to ensure we keep the representation valid - final Mutex _listenMutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); + final _listenMutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); // Stream of external changes StreamController? _watchController; } diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_closeable.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_closeable.dart index 0fb10ab..bda4afb 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_closeable.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_closeable.dart @@ -56,7 +56,7 @@ extension DHTDeletableExt on DHTDeleteable { /// Scopes a closure that conditionally deletes the DHTCloseable on exit Future maybeDeleteScope( - bool delete, Future Function(D) scopeFunction) async { + bool delete, Future Function(D) scopeFunction) { if (delete) { return deleteScope(scopeFunction); } diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/refreshable_cubit.dart b/packages/veilid_support/lib/dht_support/src/interfaces/refreshable_cubit.dart index a04e1bb..d987b38 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/refreshable_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/refreshable_cubit.dart @@ -12,5 +12,5 @@ abstract mixin class RefreshableCubit { bool get wantsRefresh => _wantsRefresh; //////////////////////////////////////////////////////////////////////////// - bool _wantsRefresh = false; + var _wantsRefresh = false; } From d1559c949d9849191569ce8a3abb68bdf2013348 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 2 Jun 2025 15:56:00 -0400 Subject: [PATCH 266/270] fix lints --- lib/chat/cubits/chat_component_cubit.dart | 2 +- lib/chat/cubits/reconciliation/author_input_queue.dart | 2 +- lib/chat/cubits/reconciliation/author_input_source.dart | 4 ++-- lib/chat/cubits/reconciliation/message_integrity.dart | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/chat/cubits/chat_component_cubit.dart b/lib/chat/cubits/chat_component_cubit.dart index 737b1a4..c0473be 100644 --- a/lib/chat/cubits/chat_component_cubit.dart +++ b/lib/chat/cubits/chat_component_cubit.dart @@ -85,7 +85,7 @@ class ChatComponentCubit extends Cubit { _onChangedContacts(_contactListCubit.state); } - Future _initAsync(Completer _cancel) async { + Future _initAsync(Completer cancel) async { // Subscribe to remote user info await _updateConversationSubscriptions(); } diff --git a/lib/chat/cubits/reconciliation/author_input_queue.dart b/lib/chat/cubits/reconciliation/author_input_queue.dart index 9a65e82..f750e77 100644 --- a/lib/chat/cubits/reconciliation/author_input_queue.dart +++ b/lib/chat/cubits/reconciliation/author_input_queue.dart @@ -294,5 +294,5 @@ class AuthorInputQueue { InputWindow? _currentWindow; /// Desired maximum window length - static const int _maxWindowLength = 256; + static const _maxWindowLength = 256; } diff --git a/lib/chat/cubits/reconciliation/author_input_source.dart b/lib/chat/cubits/reconciliation/author_input_source.dart index f974dae..e7ba765 100644 --- a/lib/chat/cubits/reconciliation/author_input_source.dart +++ b/lib/chat/cubits/reconciliation/author_input_source.dart @@ -26,11 +26,11 @@ class AuthorInputSource { //////////////////////////////////////////////////////////////////////////// - Future getTailPosition() async => + Future getTailPosition() => _dhtLog.operate((reader) async => reader.length); Future> getWindow( - int startPosition, int windowLength) async => + int startPosition, int windowLength) => _dhtLog.operate((reader) async { // Don't allow negative length if (windowLength <= 0) { diff --git a/lib/chat/cubits/reconciliation/message_integrity.dart b/lib/chat/cubits/reconciliation/message_integrity.dart index 2fd1956..2c4bb85 100644 --- a/lib/chat/cubits/reconciliation/message_integrity.dart +++ b/lib/chat/cubits/reconciliation/message_integrity.dart @@ -19,7 +19,7 @@ class MessageIntegrity { //////////////////////////////////////////////////////////////////////////// // Public interface - Future generateMessageId(proto.Message? previous) async { + Future generateMessageId(proto.Message? previous) { if (previous == null) { // If there's no last sent message, // we start at a hash of the identity public key @@ -47,7 +47,7 @@ class MessageIntegrity { message.signature = signature.toProto(); } - Future verifyMessage(proto.Message message) async { + Future verifyMessage(proto.Message message) { // Ensure the message is signed assert(message.hasSignature(), 'should not verify unsigned message'); final signature = message.signature.toVeilid(); From b192c44d5c6f83a1970ab7d79d59d1a5d5459d5f Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 2 Jun 2025 16:10:19 -0400 Subject: [PATCH 267/270] remove swap from dhtlog --- .../reconciliation/message_integrity.dart | 5 ++ .../src/dht_log/dht_log_write.dart | 54 ------------------- .../dht_short_array_write.dart | 1 + .../src/interfaces/dht_random_write.dart | 5 -- .../src/interfaces/interfaces.dart | 1 + 5 files changed, 7 insertions(+), 59 deletions(-) diff --git a/lib/chat/cubits/reconciliation/message_integrity.dart b/lib/chat/cubits/reconciliation/message_integrity.dart index 2c4bb85..40b3b18 100644 --- a/lib/chat/cubits/reconciliation/message_integrity.dart +++ b/lib/chat/cubits/reconciliation/message_integrity.dart @@ -47,6 +47,11 @@ class MessageIntegrity { message.signature = signature.toProto(); } + // XXX: change bool to an enum to allow for reconciling deleted + // XXX: messages. if a message is deleted it will not verify, but its + // XXX: signature will still be in place for the message chain. + // XXX: it should be added to a list to check for a ControlDelete that + // XXX: appears later in the message log. Future verifyMessage(proto.Message message) { // Ensure the message is signed assert(message.hasSignature(), 'should not verify unsigned message'); diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart index 00eaa94..e3697c8 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart @@ -36,60 +36,6 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { return true; } - @override - Future swap(int aPos, int bPos) async { - if (aPos < 0 || aPos >= _spine.length) { - throw IndexError.withLength(aPos, _spine.length); - } - if (bPos < 0 || bPos >= _spine.length) { - throw IndexError.withLength(bPos, _spine.length); - } - final aLookup = await _spine.lookupPosition(aPos); - if (aLookup == null) { - throw DHTExceptionInvalidData('_DHTLogWrite::swap aPos=$aPos bPos=$bPos ' - '_spine.length=${_spine.length}'); - } - final bLookup = await _spine.lookupPosition(bPos); - if (bLookup == null) { - await aLookup.close(); - throw DHTExceptionInvalidData('_DHTLogWrite::swap aPos=$aPos bPos=$bPos ' - '_spine.length=${_spine.length}'); - } - - // Swap items in the segments - if (aLookup.shortArray == bLookup.shortArray) { - await bLookup.close(); - return aLookup.scope((sa) => sa.operateWriteEventual( - (aWrite) => aWrite.swap(aLookup.pos, bLookup.pos))); - } else { - final bItem = Output(); - return aLookup.scope( - (sa) => bLookup.scope((sb) => sa.operateWriteEventual((aWrite) async { - if (bItem.value == null) { - final aItem = await aWrite.get(aLookup.pos); - if (aItem == null) { - throw DHTExceptionInvalidData( - '_DHTLogWrite::swap aPos=$aPos bPos=$bPos ' - 'aLookup.pos=${aLookup.pos} bLookup.pos=${bLookup.pos} ' - '_spine.length=${_spine.length}'); - } - await sb.operateWriteEventual((bWrite) async { - final success = await bWrite - .tryWriteItem(bLookup.pos, aItem, output: bItem); - if (!success) { - throw const DHTExceptionOutdated(); - } - }); - } - final success = - await aWrite.tryWriteItem(aLookup.pos, bItem.value!); - if (!success) { - throw const DHTExceptionOutdated(); - } - }))); - } - } - @override Future add(Uint8List value) async { // Allocate empty index at the end of the list 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 index fa3b1c6..bd3431d 100644 --- 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 @@ -6,6 +6,7 @@ part of 'dht_short_array.dart'; abstract class DHTShortArrayWriteOperations implements DHTRandomRead, + DHTRandomSwap, DHTRandomWrite, DHTInsertRemove, DHTAdd, diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart index 5b3f032..0d8f3ac 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_write.dart @@ -23,11 +23,6 @@ abstract class DHTRandomWrite { /// of the container. Future tryWriteItem(int pos, Uint8List newValue, {Output? output}); - - /// Swap items at position 'aPos' and 'bPos' in the DHTArray. - /// Throws an IndexError if either of the positions swapped exceeds the length - /// of the container - Future swap(int aPos, int bPos); } extension DHTRandomWriteExt on DHTRandomWrite { diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart b/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart index a162dc8..8a019dd 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/interfaces.dart @@ -3,6 +3,7 @@ export 'dht_clear.dart'; export 'dht_closeable.dart'; export 'dht_insert_remove.dart'; export 'dht_random_read.dart'; +export 'dht_random_swap.dart'; export 'dht_random_write.dart'; export 'dht_truncate.dart'; export 'exceptions.dart'; From 28580bad887be76487b5dc9fd8c481c7bd485769 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 5 Jun 2025 23:43:13 +0200 Subject: [PATCH 268/270] new qr code scanner --- assets/i18n/en.json | 7 +- ios/Podfile.lock | 7 - .../cubits/contact_invitation_list_cubit.dart | 10 + .../views/camera_qr_scanner.dart | 473 ++++++++++++++++++ .../views/scan_invitation_dialog.dart | 351 ++++--------- lib/contact_invitation/views/views.dart | 1 + lib/theme/views/scanner_error_widget.dart | 54 -- lib/theme/views/views.dart | 1 - macos/Flutter/GeneratedPluginRegistrant.swift | 2 - macos/Podfile.lock | 7 - .../lib/dht_support/src/dht_log/dht_log.dart | 2 +- .../src/dht_log/dht_log_spine.dart | 13 +- .../src/dht_log/dht_log_write.dart | 12 +- .../dht_short_array/dht_short_array_head.dart | 2 +- .../dht_support/src/interfaces/dht_add.dart | 22 +- .../src/interfaces/dht_random_swap.dart | 9 + .../src/interfaces/exceptions.dart | 19 +- pubspec.lock | 14 +- pubspec.yaml | 3 +- 19 files changed, 636 insertions(+), 373 deletions(-) create mode 100644 lib/contact_invitation/views/camera_qr_scanner.dart delete mode 100644 lib/theme/views/scanner_error_widget.dart create mode 100644 packages/veilid_support/lib/dht_support/src/interfaces/dht_random_swap.dart diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 50b0904..206c0b0 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -225,15 +225,16 @@ "scan_invitation_dialog": { "title": "Scan Contact Invite", "instructions": "Position the contact invite QR code in the frame", - "scan_qr_here": "Click here to scan a contact invite QR code:", - "paste_qr_here": "Camera scanning is only available on mobile devices. You can copy a QR code image and paste it here:", + "scan_qr_here": "Click here to scan a contact invite QR code with your device's camera:", + "paste_qr_here": "You can copy a QR code image and paste it by clicking here:", "scan": "Scan", "paste": "Paste", "not_an_image": "Pasted data is not an image", "could_not_decode_image": "Could not decode pasted image", "not_a_valid_qr_code": "Not a valid QR code", "error": "Failed to capture QR code", - "permission_error": "Capturing QR codes requires camera permisions. Allow camera permissions for VeilidChat in your settings." + "permission_error": "Capturing QR codes requires camera permisions. Allow camera permissions for VeilidChat in your settings.", + "camera_error": "Camera error" }, "enter_pin_dialog": { "enter_pin": "Enter PIN", diff --git a/ios/Podfile.lock b/ios/Podfile.lock index add7488..0f5fb0f 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -6,9 +6,6 @@ PODS: - Flutter (1.0.0) - flutter_native_splash (2.4.3): - Flutter - - mobile_scanner (7.0.0): - - Flutter - - FlutterMacOS - package_info_plus (0.4.5): - Flutter - pasteboard (0.0.1): @@ -38,7 +35,6 @@ DEPENDENCIES: - file_saver (from `.symlinks/plugins/file_saver/ios`) - Flutter (from `Flutter`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - - mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - pasteboard (from `.symlinks/plugins/pasteboard/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) @@ -59,8 +55,6 @@ EXTERNAL SOURCES: :path: Flutter flutter_native_splash: :path: ".symlinks/plugins/flutter_native_splash/ios" - mobile_scanner: - :path: ".symlinks/plugins/mobile_scanner/darwin" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" pasteboard: @@ -87,7 +81,6 @@ SPEC CHECKSUMS: file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf - mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index 332341d..b31c453 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:async_tools/async_tools.dart'; import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; +import 'package:convert/convert.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; @@ -165,6 +166,11 @@ class ContactInvitationListCubit }); }); + log.debug('createInvitation:\n' + 'contactRequestInboxKey=$contactRequestInboxKey\n' + 'bytes=${signedContactInvitationBytes.lengthInBytes}\n' + '${hex.encode(signedContactInvitationBytes)}'); + return (signedContactInvitationBytes, contactRequestInboxKey); } @@ -222,6 +228,10 @@ class ContactInvitationListCubit required GetEncryptionKeyCallback getEncryptionKeyCallback, required CancelRequest cancelRequest, }) async { + log.debug('validateInvitation:\n' + 'bytes=${inviteData.lengthInBytes}\n' + '${hex.encode(inviteData)}'); + final pool = DHTRecordPool.instance; // Get contact request inbox from invitation diff --git a/lib/contact_invitation/views/camera_qr_scanner.dart b/lib/contact_invitation/views/camera_qr_scanner.dart new file mode 100644 index 0000000..11d8f77 --- /dev/null +++ b/lib/contact_invitation/views/camera_qr_scanner.dart @@ -0,0 +1,473 @@ +import 'dart:async'; + +import 'package:async_tools/async_tools.dart'; +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:camera/camera.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:zxing2/qrcode.dart'; + +import '../../theme/theme.dart'; + +enum _FrameState { + notFound, + formatError, + checksumError, +} + +class _ScannerOverlay extends CustomPainter { + _ScannerOverlay(this.scanWindow, this.frameColor); + + final Rect scanWindow; + final Color? frameColor; + + @override + void paint(Canvas canvas, Size size) { + final backgroundPath = Path()..addRect(Rect.largest); + final cutoutPath = Path()..addRect(scanWindow); + + final backgroundPaint = Paint() + ..color = (frameColor ?? Colors.black).withAlpha(127) + ..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; +} + +/// Camera QR scanner +class CameraQRScanner extends StatefulWidget { + const CameraQRScanner( + {required Widget Function(BuildContext) loadingBuilder, + required Widget Function( + BuildContext, Object error, StackTrace? stackTrace) + errorBuilder, + required Widget Function(BuildContext) bottomRowBuilder, + required void Function(String) showNotification, + required void Function(String, Object? error, StackTrace? stackTrace) + logError, + required void Function(T) onDone, + T? Function(Result)? onDetect, + T? Function(CameraImage)? onImageAvailable, + Size? scanSize, + Color? formatErrorFrameColor, + Color? checksumErrorFrameColor, + String? cameraErrorMessage, + String? deniedErrorMessage, + String? deniedWithoutPromptErrorMessage, + String? restrictedErrorMessage, + super.key}) + : _loadingBuilder = loadingBuilder, + _errorBuilder = errorBuilder, + _bottomRowBuilder = bottomRowBuilder, + _showNotification = showNotification, + _logError = logError, + _scanSize = scanSize, + _onDetect = onDetect, + _onDone = onDone, + _onImageAvailable = onImageAvailable, + _formatErrorFrameColor = formatErrorFrameColor, + _checksumErrorFrameColor = checksumErrorFrameColor, + _cameraErrorMessage = cameraErrorMessage, + _deniedErrorMessage = deniedErrorMessage, + _deniedWithoutPromptErrorMessage = deniedWithoutPromptErrorMessage, + _restrictedErrorMessage = restrictedErrorMessage; + @override + State> createState() => _CameraQRScannerState(); + + //////////////////////////////////////////////////////////////////////////// + + final Widget Function(BuildContext) _loadingBuilder; + final Widget Function(BuildContext, Object error, StackTrace? stackTrace) + _errorBuilder; + final Widget Function(BuildContext) _bottomRowBuilder; + final void Function(String) _showNotification; + final void Function(String, Object? error, StackTrace? stackTrace) _logError; + final T? Function(Result)? _onDetect; + final void Function(T) _onDone; + final T? Function(CameraImage)? _onImageAvailable; + + final Size? _scanSize; + final Color? _formatErrorFrameColor; + final Color? _checksumErrorFrameColor; + final String? _cameraErrorMessage; + final String? _deniedErrorMessage; + final String? _deniedWithoutPromptErrorMessage; + final String? _restrictedErrorMessage; +} + +class _CameraQRScannerState extends State> + with WidgetsBindingObserver, TickerProviderStateMixin { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + + // Async Init + _initWait.add(_init); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + unawaited(_controller?.dispose()); + super.dispose(); + } + + // #docregion AppLifecycle + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + final cameraController = _controller; + + // App state changed before we got the chance to initialize. + if (cameraController == null || !cameraController.value.isInitialized) { + return; + } + + if (state == AppLifecycleState.inactive) { + unawaited(cameraController.dispose()); + } else if (state == AppLifecycleState.resumed) { + unawaited(_initializeCameraController(cameraController.description)); + } + } + // #enddocregion AppLifecycle + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final activeColor = theme.colorScheme.primary; + final inactiveColor = theme.colorScheme.onPrimary; + + final scanSize = widget._scanSize; + final scanWindow = scanSize == null + ? null + : Rect.fromCenter( + center: Offset.zero, + width: scanSize.width, + height: scanSize.height, + ); + + return Scaffold( + body: FutureBuilder( + future: _initWait(), + builder: (context, av) => av.when( + error: (e, st) => widget._errorBuilder(context, e, st), + loading: () => widget._loadingBuilder(context), + data: (data, isComplete) => Column( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(1), + child: Center( + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + _cameraPreviewWidget(context), + if (scanWindow != null) + IgnorePointer( + child: CustomPaint( + foregroundPainter: _ScannerOverlay( + scanWindow, + switch (_frameState) { + _FrameState.notFound => null, + _FrameState.formatError => + widget._formatErrorFrameColor, + _FrameState.checksumError => + widget._checksumErrorFrameColor + }), + )), + ]), + ), + ), + ), + widget._bottomRowBuilder(context), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _cameraToggleWidget(), + _torchToggleWidget(activeColor, inactiveColor) + ], + ), + ], + ), + ))); + } + + /// Display the preview from the camera + /// (or a message if the preview is not available). + Widget _cameraPreviewWidget(BuildContext context) { + final cameraController = _controller; + + if (cameraController == null || !cameraController.value.isInitialized) { + return widget._loadingBuilder(context); + } else { + return Listener( + onPointerDown: (_) => _pointers++, + onPointerUp: (_) => _pointers--, + child: CameraPreview( + cameraController, + child: LayoutBuilder( + builder: (context, constraints) => GestureDetector( + behavior: HitTestBehavior.opaque, + onScaleStart: _handleScaleStart, + onScaleUpdate: _handleScaleUpdate, + onTapDown: (details) => + _onViewFinderTap(details, constraints), + )), + ), + ); + } + } + + void _handleScaleStart(ScaleStartDetails details) { + _baseScale = _currentScale; + } + + Future _handleScaleUpdate(ScaleUpdateDetails details) async { + // When there are not exactly two fingers on screen don't scale + if (_controller == null || _pointers != 2) { + return; + } + + _currentScale = (_baseScale * details.scale) + .clamp(_minAvailableZoom, _maxAvailableZoom); + + await _controller!.setZoomLevel(_currentScale); + } + + Widget _torchToggleWidget(Color activeColor, Color inactiveColor) => + IconButton( + icon: const Icon(Icons.highlight), + color: _controller?.value.flashMode == FlashMode.torch + ? activeColor + : inactiveColor, + onPressed: _controller != null + ? () => _onSetFlashModeButtonPressed( + _controller?.value.flashMode == FlashMode.torch + ? FlashMode.off + : FlashMode.torch) + : null, + ); + + Widget _cameraToggleWidget() { + final currentCameraDescription = _controller?.description; + return IconButton( + icon: + Icon(isAndroid ? Icons.flip_camera_android : Icons.flip_camera_ios), + onPressed: (currentCameraDescription == null || _cameras.isEmpty) + ? null + : () { + final nextCameraIndex = + (_cameras.indexOf(currentCameraDescription) + 1) % + _cameras.length; + unawaited(_onNewCameraSelected(_cameras[nextCameraIndex])); + }); + } + + void _onViewFinderTap(TapDownDetails details, BoxConstraints constraints) { + final cameraController = _controller; + if (cameraController == null) { + return; + } + + final offset = Offset( + details.localPosition.dx / constraints.maxWidth, + details.localPosition.dy / constraints.maxHeight, + ); + unawaited(cameraController.setExposurePoint(offset)); + unawaited(cameraController.setFocusPoint(offset)); + } + + Future _onNewCameraSelected(CameraDescription cameraDescription) { + if (_controller != null) { + return _controller!.setDescription(cameraDescription); + } else { + return _initializeCameraController(cameraDescription); + } + } + + Future _initializeCameraController( + CameraDescription cameraDescription) async { + final cameraController = CameraController( + cameraDescription, + kIsWeb ? ResolutionPreset.max : ResolutionPreset.medium, + enableAudio: false, + imageFormatGroup: ImageFormatGroup.jpeg, + ); + + _controller = cameraController; + + // If the controller is updated then update the UI. + cameraController.addListener(() { + if (mounted) { + setState(() {}); + } + if (cameraController.value.hasError && + (cameraController.value.errorDescription?.isNotEmpty ?? false)) { + widget._showNotification( + '${widget._cameraErrorMessage ?? 'Camera error'}: ' + '${cameraController.value.errorDescription!}'); + } + }); + + try { + await cameraController.initialize(); + + try { + _maxAvailableZoom = await cameraController.getMaxZoomLevel(); + _minAvailableZoom = await cameraController.getMinZoomLevel(); + } on PlatformException { + _maxAvailableZoom = 1; + _minAvailableZoom = 1; + } + + await cameraController.startImageStream((cameraImage) { + final out = + (widget._onImageAvailable ?? _onImageAvailable)(cameraImage); + if (out != null) { + _controller = null; + unawaited(cameraController.dispose()); + widget._onDone(out); + } + }); + } on CameraException catch (e, st) { + switch (e.code) { + case 'CameraAccessDenied': + widget._showNotification( + widget._deniedErrorMessage ?? 'You have denied camera access.'); + case 'CameraAccessDeniedWithoutPrompt': + // iOS only + widget._showNotification(widget._deniedWithoutPromptErrorMessage ?? + 'Please go to Settings app to enable camera access.'); + case 'CameraAccessRestricted': + // iOS only + widget._showNotification( + widget._restrictedErrorMessage ?? 'Camera access is restricted.'); + default: + _showCameraException(e, st); + } + } + + if (mounted) { + setState(() {}); + } + } + + T? _onImageAvailable(CameraImage cameraImage) { + try { + final plane = cameraImage.planes.firstOrNull; + if (plane == null) { + return null; + } + + // final image = JpegDecoder().decode(plane.bytes); + // if (image == null) { + // return; + // } + + // final abgrImage = image + // .convert(numChannels: 4) + // .getBytes(order: ChannelOrder.abgr) + // .buffer + // .asInt32List(); + + final abgrImage = plane.bytes.buffer.asInt32List(); + + final source = + RGBLuminanceSource(cameraImage.width, cameraImage.height, abgrImage); + + final bitmap = BinaryBitmap(HybridBinarizer(source)); + + final reader = QRCodeReader(); + try { + final result = reader.decode(bitmap); + return widget._onDetect?.call(result); + } on NotFoundException { + _setFrameState(_FrameState.notFound); + } on FormatReaderException { + _setFrameState(_FrameState.formatError); + } on ChecksumException { + _setFrameState(_FrameState.checksumError); + } + + // Should also catch errors from QRCodeReader + // ignore: avoid_catches_without_on_clauses + } catch (e, st) { + widget._logError('Unexpected error: $e\n$st', e, st); + } + return null; + } + + void _setFrameState(_FrameState frameState) { + if (mounted) { + if (_frameState != frameState) { + setState(() { + _frameState = frameState; + }); + } + } + } + + void _onSetFlashModeButtonPressed(FlashMode mode) { + unawaited(_setFlashMode(mode).then((_) { + if (mounted) { + setState(() {}); + } + })); + } + + Future _setFlashMode(FlashMode mode) async { + if (_controller == null) { + return; + } + + try { + await _controller!.setFlashMode(mode); + } on CameraException catch (e, st) { + _showCameraException(e, st); + rethrow; + } + } + + void _showCameraException(CameraException e, StackTrace st) { + _logCameraException(e, st); + widget._showNotification('Error: ${e.code}\n${e.description}'); + } + + void _logCameraException(CameraException e, StackTrace st) { + final code = e.code; + final message = e.description; + widget._logError( + 'CameraException: $code${message == null ? '' : '\nMessage: $message'}', + e, + st); + } + + Future _init(Completer cancel) async { + _cameras = await availableCameras(); + if (_cameras.isNotEmpty) { + await _onNewCameraSelected(_cameras.first); + } + } + + //////////////////////////////////////////////////////////////////////////// + + CameraController? _controller; + final _initWait = WaitSet(); + late final List _cameras; + var _minAvailableZoom = 1.0; + var _maxAvailableZoom = 1.0; + var _currentScale = 1.0; + var _baseScale = 1.0; + var _pointers = 0; + _FrameState _frameState = _FrameState.notFound; +} diff --git a/lib/contact_invitation/views/scan_invitation_dialog.dart b/lib/contact_invitation/views/scan_invitation_dialog.dart index 8fbdf5c..058383d 100644 --- a/lib/contact_invitation/views/scan_invitation_dialog.dart +++ b/lib/contact_invitation/views/scan_invitation_dialog.dart @@ -2,106 +2,19 @@ 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/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:provider/provider.dart'; import 'package:zxing2/qrcode.dart'; import '../../notifications/notifications.dart'; import '../../theme/theme.dart'; +import '../../tools/tools.dart'; +import 'camera_qr_scanner.dart'; import 'invitation_dialog.dart'; -// class BarcodeOverlay extends CustomPainter { -// BarcodeOverlay({ -// required this.barcode, -// required this.boxFit, -// required this.capture, -// required this.size, -// }); - -// final BarcodeCapture capture; -// final Barcode barcode; -// final BoxFit boxFit; -// final Size size; - -// @override -// void paint(Canvas canvas, Size size) { -// final adjustedSize = applyBoxFit(boxFit, 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.size.width : size.width) / -// adjustedSize.destination.width; -// final ratioHeight = (Platform.isIOS ? capture.size.height : 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.withAlpha(127) - ..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 ScanInvitationDialog extends StatefulWidget { const ScanInvitationDialog({required Locator locator, super.key}) : _locator = locator; @@ -122,7 +35,7 @@ class ScanInvitationDialog extends StatefulWidget { } class ScanInvitationDialogState extends State { - bool scanned = false; + var _scanned = false; @override void initState() { @@ -131,14 +44,14 @@ class ScanInvitationDialogState extends State { void onValidationCancelled() { setState(() { - scanned = false; + _scanned = false; }); } void onValidationSuccess() {} void onValidationFailed() { setState(() { - scanned = false; + _scanned = false; }); } @@ -146,142 +59,62 @@ class ScanInvitationDialogState extends State { 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) => - 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.withAlpha(127), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - IconButton( - color: Colors.white, - icon: ValueListenableBuilder( - valueListenable: cameraController, - builder: (context, state, child) { - switch (state.torchState) { - case TorchState.off: - return Icon(Icons.flash_off, - color: - scale.grayScale.subtleBackground); - case TorchState.on: - return Icon(Icons.flash_on, - color: scale.primaryScale.primary); - case TorchState.auto: - return Icon(Icons.flash_auto, - color: scale.primaryScale.primary); - case TorchState.unavailable: - return Icon(Icons.no_flash, - color: scale.primaryScale.primary); - } - }, - ), - iconSize: 32, - onPressed: cameraController.toggleTorch, - ), - SizedBox( - width: windowSize.width - 120, - height: 50, - child: FittedBox( - child: Text( + CameraQRScanner( + scanSize: const Size(200, 200), + loadingBuilder: (context) => waitingPage(), + errorBuilder: (coRntext, e, st) => errorPage(e, st), + bottomRowBuilder: (context) => FittedBox( + fit: BoxFit.scaleDown, + child: Text( translate( 'scan_invitation_dialog.instructions'), - overflow: TextOverflow.fade, - style: Theme.of(context) - .textTheme - .labelLarge! - .copyWith(color: Colors.white), - ), - ), + overflow: TextOverflow.ellipsis, + style: theme.textTheme.labelLarge), ), - IconButton( - color: Colors.white, - icon: ValueListenableBuilder( - valueListenable: cameraController, - builder: (context, state, child) { - switch (state.cameraDirection) { - case CameraFacing.front: - return const Icon(Icons.camera_front); - case CameraFacing.back: - return const Icon(Icons.camera_rear); - case CameraFacing.external: - return const Icon(Icons.camera_alt); - case CameraFacing.unknown: - return const Icon(Icons.question_mark); - } - }, - ), - iconSize: 32, - onPressed: cameraController.switchCamera, - ), - ], - ), - ), - ), + showNotification: (s) {}, + logError: log.error, + cameraErrorMessage: + translate('scan_invitation_dialog.camera_error'), + deniedErrorMessage: + translate('scan_invitation_dialog.permission_error'), + deniedWithoutPromptErrorMessage: + translate('scan_invitation_dialog.permission_error'), + restrictedErrorMessage: + translate('scan_invitation_dialog.permission_error'), + onDetect: (result) { + final byteSegments = result + .resultMetadata[ResultMetadataType.byteSegments]; + if (byteSegments != null) { + final segs = byteSegments as List; + + final byteData = Uint8List.fromList(segs[0].toList()); + return byteData; + } + return null; + }, + onDone: (result) { + Navigator.of(context).pop(result); + }), Align( alignment: Alignment.topRight, child: IconButton( color: Colors.white, - icon: - Icon(Icons.close, color: scale.grayScale.primary), - iconSize: 32, - onPressed: () => { - SchedulerBinding.instance - .addPostFrameCallback((_) { - cameraController.dispose(); - Navigator.pop(context); - }) - })), + icon: Icon(Icons.close, + color: scale.primaryScale.appText), + iconSize: 32.scaled(context), + onPressed: () { + Navigator.of(context).pop(); + })), ], )); - } on MobileScannerException catch (e) { - if (e.errorCode == MobileScannerErrorCode.permissionDenied) { - context - .read() - .error(text: translate('scan_invitation_dialog.permission_error')); - } else { - context - .read() - .error(text: translate('scan_invitation_dialog.error')); - } } on Exception catch (_) { context .read() @@ -342,76 +175,66 @@ class ScanInvitationDialogState extends State { InvitationDialogState 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_invitation_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_invitation_dialog.scan'))), - ).paddingLTRB(0, 0, 0, 8) - ]); + if (_scanned) { + return const SizedBox.shrink(); } - return Column(mainAxisSize: MainAxisSize.min, children: [ - if (!scanned) + + final children = []; + if (isiOS || isAndroid) { + children.addAll([ Text( - translate('scan_invitation_dialog.paste_qr_here'), + translate('scan_invitation_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 pasteQRImage(context); + final inviteData = await scanQRImage(context); if (inviteData != null) { - await validateInviteData(inviteData: inviteData); setState(() { - scanned = true; + _scanned = true; }); + await validateInviteData(inviteData: inviteData); } }, - child: Text(translate('scan_invitation_dialog.paste'))), + child: Text(translate('scan_invitation_dialog.scan'))), ).paddingLTRB(0, 0, 0, 8) + ]); + } + + children.addAll([ + Text( + translate('scan_invitation_dialog.paste_qr_here'), + ).paddingLTRB(0, 0, 0, 8), + 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_invitation_dialog.paste'))), + ).paddingLTRB(0, 0, 0, 8) ]); + + return Column(mainAxisSize: MainAxisSize.min, children: children); } @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return InvitationDialog( - locator: widget._locator, - onValidationCancelled: onValidationCancelled, - onValidationSuccess: onValidationSuccess, - onValidationFailed: onValidationFailed, - inviteControlIsValid: inviteControlIsValid, - buildInviteControl: buildInviteControl); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('scanned', scanned)); - } + Widget build(BuildContext context) => InvitationDialog( + locator: widget._locator, + onValidationCancelled: onValidationCancelled, + onValidationSuccess: onValidationSuccess, + onValidationFailed: onValidationFailed, + inviteControlIsValid: inviteControlIsValid, + buildInviteControl: buildInviteControl); } diff --git a/lib/contact_invitation/views/views.dart b/lib/contact_invitation/views/views.dart index 241513d..319296b 100644 --- a/lib/contact_invitation/views/views.dart +++ b/lib/contact_invitation/views/views.dart @@ -1,3 +1,4 @@ +export 'camera_qr_scanner.dart'; export 'contact_invitation_display.dart'; export 'contact_invitation_item_widget.dart'; export 'contact_invitation_list_widget.dart'; diff --git a/lib/theme/views/scanner_error_widget.dart b/lib/theme/views/scanner_error_widget.dart deleted file mode 100644 index d5463f4..0000000 --- a/lib/theme/views/scanner_error_widget.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; - -class ScannerErrorWidget extends StatelessWidget { - const ScannerErrorWidget({required this.error, super.key}); - - final MobileScannerException error; - - @override - Widget build(BuildContext context) { - String errorMessage; - - switch (error.errorCode) { - case MobileScannerErrorCode.controllerUninitialized: - errorMessage = 'Controller not ready.'; - case MobileScannerErrorCode.permissionDenied: - errorMessage = 'Permission denied'; - case MobileScannerErrorCode.unsupported: - errorMessage = 'Scanning is unsupported on this device'; - default: - errorMessage = 'Generic Error'; - } - - return ColoredBox( - color: Colors.black, - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding( - padding: EdgeInsets.only(bottom: 16), - child: Icon(Icons.error, color: Colors.white), - ), - Text( - errorMessage, - style: const TextStyle(color: Colors.white), - ), - Text( - error.errorDetails?.message ?? '', - style: const TextStyle(color: Colors.white), - ), - ], - ), - ), - ); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('error', error)); - } -} diff --git a/lib/theme/views/views.dart b/lib/theme/views/views.dart index 1144440..b62c4bc 100644 --- a/lib/theme/views/views.dart +++ b/lib/theme/views/views.dart @@ -4,7 +4,6 @@ export 'pop_control.dart'; export 'preferences/preferences.dart'; export 'recovery_key_widget.dart'; export 'responsive.dart'; -export 'scanner_error_widget.dart'; export 'styled_widgets/styled_button_box.dart'; export 'styled_widgets/styled_widgets.dart'; export 'widget_helpers.dart'; diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index a599497..7e4a42f 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,7 +6,6 @@ import FlutterMacOS import Foundation import file_saver -import mobile_scanner import package_info_plus import pasteboard import path_provider_foundation @@ -21,7 +20,6 @@ import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin")) - MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index beb7d0e..bd9fccf 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -2,9 +2,6 @@ PODS: - file_saver (0.0.1): - FlutterMacOS - FlutterMacOS (1.0.0) - - mobile_scanner (7.0.0): - - Flutter - - FlutterMacOS - package_info_plus (0.0.1): - FlutterMacOS - pasteboard (0.0.1): @@ -34,7 +31,6 @@ PODS: DEPENDENCIES: - file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) @@ -52,8 +48,6 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos FlutterMacOS: :path: Flutter/ephemeral - mobile_scanner: - :path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin package_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos pasteboard: @@ -80,7 +74,6 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: file_saver: e35bd97de451dde55ff8c38862ed7ad0f3418d0f FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 package_info_plus: f0052d280d17aa382b932f399edf32507174e870 pasteboard: 278d8100149f940fb795d6b3a74f0720c890ecb7 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart index 20471da..da74df1 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log.dart @@ -243,7 +243,7 @@ class DHTLog implements DHTDeleteable { /// 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 a value if its changes also - /// succeeded, and throw DHTExceptionTryAgain to trigger another + /// succeeded, and throw DHTExceptionOutdated to trigger another /// eventual consistency pass. Future operateAppendEventual( Future Function(DHTLogWriteOperations) closure, diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart index 0aebd6c..ce231e2 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_spine.dart @@ -250,12 +250,13 @@ class _DHTLogSpine { final headDelta = _ringDistance(newHead, oldHead); final tailDelta = _ringDistance(newTail, oldTail); if (headDelta > _positionLimit ~/ 2 || tailDelta > _positionLimit ~/ 2) { - throw DHTExceptionInvalidData('_DHTLogSpine::_updateHead ' - '_head=$_head _tail=$_tail ' - 'oldHead=$oldHead oldTail=$oldTail ' - 'newHead=$newHead newTail=$newTail ' - 'headDelta=$headDelta tailDelta=$tailDelta ' - '_positionLimit=$_positionLimit'); + throw DHTExceptionInvalidData( + cause: '_DHTLogSpine::_updateHead ' + '_head=$_head _tail=$_tail ' + 'oldHead=$oldHead oldTail=$oldTail ' + 'newHead=$newHead newTail=$newTail ' + 'headDelta=$headDelta tailDelta=$tailDelta ' + '_positionLimit=$_positionLimit'); } } diff --git a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart index e3697c8..590fbb2 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_log/dht_log_write.dart @@ -18,7 +18,8 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { final lookup = await _spine.lookupPosition(pos); if (lookup == null) { throw DHTExceptionInvalidData( - '_DHTLogRead::tryWriteItem pos=$pos _spine.length=${_spine.length}'); + cause: '_DHTLogRead::tryWriteItem pos=$pos ' + '_spine.length=${_spine.length}'); } // Write item to the segment @@ -75,10 +76,11 @@ class _DHTLogWrite extends _DHTLogRead implements DHTLogWriteOperations { final lookup = await _spine.lookupPosition(insertPos + valueIdx); if (lookup == null) { - throw DHTExceptionInvalidData('_DHTLogWrite::addAll ' - '_spine.length=${_spine.length}' - 'insertPos=$insertPos valueIdx=$valueIdx ' - 'values.length=${values.length} '); + throw DHTExceptionInvalidData( + cause: '_DHTLogWrite::addAll ' + '_spine.length=${_spine.length}' + 'insertPos=$insertPos valueIdx=$valueIdx ' + 'values.length=${values.length} '); } final sacount = min(remaining, DHTShortArray.maxElements - lookup.pos); 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 1785b28..5b224cb 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 @@ -531,7 +531,7 @@ class _DHTShortArrayHead { //////////////////////////////////////////////////////////////////////////// // Head/element mutex to ensure we keep the representation valid - final Mutex _headMutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); + final _headMutex = Mutex(debugLockTimeout: kIsDebugMode ? 60 : null); // Subscription to head record internal changes StreamSubscription? _subscription; // Notify closure for external head changes diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_add.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_add.dart index dc79350..28d2fbb 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/dht_add.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_add.dart @@ -15,27 +15,37 @@ abstract class DHTAdd { Future add(Uint8List value); /// Try to add a list of items to the DHT container. - /// Return if the elements were successfully added. - /// Throws DHTExceptionTryAgain if the state changed before the elements could + /// Return the number of elements successfully added. + /// Throws DHTExceptionTryAgain if the state changed before any elements could /// be added or a newer value was found on the network. + /// Throws DHTConcurrencyLimit if the number values in the list was too large + /// at this time /// Throws a StateError if the container exceeds its maximum size. Future addAll(List values); } extension DHTAddExt on DHTAdd { /// Convenience function: - /// Like tryAddItem but also encodes the input value as JSON and parses the - /// returned element as JSON + /// Like add but also encodes the input value as JSON Future addJson( T newValue, ) => add(jsonEncodeBytes(newValue)); /// Convenience function: - /// Like tryAddItem but also encodes the input value as a protobuf object - /// and parses the returned element as a protobuf object + /// Like add but also encodes the input value as a protobuf object Future addProtobuf( T newValue, ) => add(newValue.writeToBuffer()); + + /// Convenience function: + /// Like addAll but also encodes the input values as JSON + Future addAllJson(List values) => + addAll(values.map(jsonEncodeBytes).toList()); + + /// Convenience function: + /// Like addAll but also encodes the input values as protobuf objects + Future addAllProtobuf(List values) => + addAll(values.map((x) => x.writeToBuffer()).toList()); } diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_swap.dart b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_swap.dart new file mode 100644 index 0000000..8aa4dc1 --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/interfaces/dht_random_swap.dart @@ -0,0 +1,9 @@ +//////////////////////////////////////////////////////////////////////////// +// Writer interface +// ignore: one_member_abstracts +abstract class DHTRandomSwap { + /// Swap items at position 'aPos' and 'bPos' in the DHTArray. + /// Throws an IndexError if either of the positions swapped exceeds the length + /// of the container + Future swap(int aPos, int bPos); +} diff --git a/packages/veilid_support/lib/dht_support/src/interfaces/exceptions.dart b/packages/veilid_support/lib/dht_support/src/interfaces/exceptions.dart index 134f5fa..01354f0 100644 --- a/packages/veilid_support/lib/dht_support/src/interfaces/exceptions.dart +++ b/packages/veilid_support/lib/dht_support/src/interfaces/exceptions.dart @@ -1,14 +1,25 @@ class DHTExceptionOutdated implements Exception { const DHTExceptionOutdated( - [this.cause = 'operation failed due to newer dht value']); + {this.cause = 'operation failed due to newer dht value'}); final String cause; @override String toString() => 'DHTExceptionOutdated: $cause'; } +class DHTConcurrencyLimit implements Exception { + const DHTConcurrencyLimit( + {required this.limit, + this.cause = 'failed due to maximum parallel operation limit'}); + final String cause; + final int limit; + + @override + String toString() => 'DHTConcurrencyLimit: $cause (limit=$limit)'; +} + class DHTExceptionInvalidData implements Exception { - const DHTExceptionInvalidData(this.cause); + const DHTExceptionInvalidData({this.cause = 'data was invalid'}); final String cause; @override @@ -16,7 +27,7 @@ class DHTExceptionInvalidData implements Exception { } class DHTExceptionCancelled implements Exception { - const DHTExceptionCancelled([this.cause = 'operation was cancelled']); + const DHTExceptionCancelled({this.cause = 'operation was cancelled'}); final String cause; @override @@ -25,7 +36,7 @@ class DHTExceptionCancelled implements Exception { class DHTExceptionNotAvailable implements Exception { const DHTExceptionNotAvailable( - [this.cause = 'request could not be completed at this time']); + {this.cause = 'request could not be completed at this time'}); final String cause; @override diff --git a/pubspec.lock b/pubspec.lock index 779806e..95c1262 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -274,7 +274,7 @@ packages: source: hosted version: "1.3.1" camera: - dependency: transitive + dependency: "direct main" description: name: camera sha256: "413d2b34fe28496c35c69ede5b232fb9dd5ca2c3a4cb606b14efc1c7546cc8cb" @@ -394,7 +394,7 @@ packages: source: hosted version: "1.19.1" convert: - dependency: transitive + dependency: "direct main" description: name: convert sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 @@ -912,14 +912,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - mobile_scanner: - dependency: "direct main" - description: - name: mobile_scanner - sha256: "72f06a071aa8b14acea3ab43ea7949eefe4a2469731ae210e006ba330a033a8c" - url: "https://pub.dev" - source: hosted - version: "7.0.0" nested: dependency: transitive description: @@ -1765,7 +1757,7 @@ packages: path: "../veilid/veilid-flutter" relative: true source: path - version: "0.4.6" + version: "0.4.7" veilid_support: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index cee2ec7..5cdbcb0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,10 +23,12 @@ dependencies: bloc: ^9.0.0 bloc_advanced_tools: ^0.1.13 blurry_modal_progress_hud: ^1.1.1 + camera: ^0.11.1 change_case: ^2.2.0 charcode: ^1.4.0 circular_profile_avatar: ^2.0.5 circular_reveal_animation: ^2.0.1 + convert: ^3.1.2 cupertino_icons: ^1.0.8 equatable: ^2.0.7 expansion_tile_group: ^2.2.0 @@ -58,7 +60,6 @@ dependencies: json_annotation: ^4.9.0 loggy: ^2.0.3 meta: ^1.16.0 - mobile_scanner: ^7.0.0 package_info_plus: ^8.3.0 pasteboard: ^0.4.0 path: ^1.9.1 From a1a6872e3ff534221c32dad97fddb7e9af9807d2 Mon Sep 17 00:00:00 2001 From: "Ethan Hindmarsh #YOLO #Ally.lol" Date: Fri, 6 Jun 2025 21:58:13 -0500 Subject: [PATCH 269/270] update version bump logic --- version_bump.sh | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/version_bump.sh b/version_bump.sh index cd17b37..9733313 100755 --- a/version_bump.sh +++ b/version_bump.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Fail out if any step has an error set -e @@ -12,8 +12,11 @@ elif [ "$1" == "minor" ]; then elif [ "$1" == "major" ]; then echo Bumping major version PART=major +elif [ "$1" == "build" ]; then + echo Bumping build code + PART=build else - echo Unsupported part! Specify 'patch', 'minor', or 'major' + echo Unsupported part! Specify 'build', 'patch', 'minor', or 'major' exit 1 fi @@ -25,7 +28,6 @@ increment_buildcode() { local new_buildcode=$((buildcode + 1)) echo "${major_minor_patch}+${new_buildcode}" } - # Function to get the current version from pubspec.yaml get_current_version() { awk '/^version: / { print $2 }' pubspec.yaml @@ -43,25 +45,23 @@ update_version() { eval "$SED_CMD 's/version: .*/version: ${new_version}/' pubspec.yaml" } -# I pray none of this errors! - I think it should popup an error should that happen.. - current_version=$(get_current_version) echo "Current Version: $current_version" -# Bump the major, minor, or patch version using bump2version -bump2version --current-version $current_version $PART +if [ "$PART" == "build" ]; then + final_version=$(increment_buildcode $current_version) +else + # Bump the major, minor, or patch version using bump2version + bump2version --current-version $current_version $PART -# Get the new version after bump2version -new_version=$(get_current_version) + new_version_base=$(get_current_version) -# Preserve the current build code -buildcode=${current_version#*+} -new_version="${new_version%+*}+${buildcode}" - -# Increment the build code -final_version=$(increment_buildcode $new_version) + buildcode=${current_version#*+} + intermediate_version="${new_version_base%+*}+${buildcode}" + final_version=$(increment_buildcode $intermediate_version) +fi # Update pubspec.yaml with the final version update_version $final_version @@ -71,4 +71,3 @@ echo "New Version: $final_version" #git add pubspec.yaml #git commit -m "Bump version to $final_version" #git tag "v$final_version" - From 7a1fa6dfb8c865acfe3ed68a45ad8a0c58abdcc9 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 14 Jun 2025 15:31:15 -0400 Subject: [PATCH 270/270] update for api fixes --- assets/i18n/en.json | 3 ++- .../cubits/contact_invitation_list_cubit.dart | 11 ++++++++++ .../views/invitation_dialog.dart | 11 ++++++++++ .../src/dht_record/dht_record.dart | 20 +++++++++++-------- .../lib/identity_support/super_identity.dart | 10 ++++++---- 5 files changed, 42 insertions(+), 13 deletions(-) diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 206c0b0..f3348b5 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -209,7 +209,8 @@ "protected_with_pin": "Contact invitation is protected with a PIN", "protected_with_password": "Contact invitation is protected with a password", "invalid_pin": "Invalid PIN", - "invalid_password": "Invalid password" + "invalid_password": "Invalid password", + "invalid_identity": "Contact invitation is for an missing or unavailable identity" }, "waiting_invitation": { "accepted": "Contact invitation accepted from {name}", diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index b31c453..4875263 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -20,6 +20,13 @@ class ContactInviteInvalidKeyException implements Exception { final EncryptionKeyType type; } +class ContactInviteInvalidIdentityException implements Exception { + const ContactInviteInvalidIdentityException( + this.contactSuperIdentityRecordKey) + : super(); + final TypedKey contactSuperIdentityRecordKey; +} + typedef GetEncryptionKeyCallback = Future Function( VeilidCryptoSystem cs, EncryptionKeyType encryptionKeyType, @@ -301,6 +308,10 @@ class ContactInvitationListCubit final contactSuperIdentity = await SuperIdentity.open( superRecordKey: contactSuperIdentityRecordKey) .withCancel(cancelRequest); + if (contactSuperIdentity == null) { + throw ContactInviteInvalidIdentityException( + contactSuperIdentityRecordKey); + } // Verify final idcs = await contactSuperIdentity.currentInstance.cryptoSystem; diff --git a/lib/contact_invitation/views/invitation_dialog.dart b/lib/contact_invitation/views/invitation_dialog.dart index a5a7fad..f4c7fcc 100644 --- a/lib/contact_invitation/views/invitation_dialog.dart +++ b/lib/contact_invitation/views/invitation_dialog.dart @@ -216,6 +216,17 @@ class InvitationDialogState extends State { _isValidating = false; _validInvitation = validatedContactInvitation; }); + } on ContactInviteInvalidIdentityException catch (_) { + if (mounted) { + context + .read() + .error(text: translate('invitation_dialog.invalid_identity')); + } + setState(() { + _isValidating = false; + _validInvitation = null; + widget.onValidationFailed(); + }); } on ContactInviteInvalidKeyException catch (e) { String errorText; switch (e.type) { 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 893cb80..6fb9d9e 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 @@ -226,7 +226,7 @@ class DHTRecord implements DHTDeleteable { Future tryWriteBytes(Uint8List newValue, {int subkey = -1, VeilidCrypto? crypto, - KeyPair? writer, + SetDHTValueOptions? options, Output? outSeqNum}) => _wrapStats('tryWriteBytes', () async { subkey = subkeyOrDefault(subkey); @@ -236,7 +236,9 @@ class DHTRecord implements DHTDeleteable { // Set the new data if possible var newValueData = await _routingContext.setDHTValue( key, subkey, encryptedNewValue, - writer: writer ?? _writer); + options: SetDHTValueOptions( + writer: options?.writer ?? _writer, + allowOffline: options?.allowOffline)); 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 @@ -292,7 +294,8 @@ class DHTRecord implements DHTDeleteable { // Set the new data newValueData = await _routingContext.setDHTValue( key, subkey, encryptedNewValue, - writer: writer ?? _writer); + options: SetDHTValueOptions( + writer: writer ?? _writer, allowOffline: false)); // Repeat if newer data on the network was found } while (newValueData != null); @@ -351,7 +354,8 @@ class DHTRecord implements DHTDeleteable { oldValue = await tryWriteBytes(updatedValue, subkey: subkey, crypto: crypto, - writer: writer, + options: SetDHTValueOptions( + writer: writer ?? _writer, allowOffline: false), outSeqNum: outSeqNum); // Repeat update if newer data on the network was found @@ -362,12 +366,12 @@ class DHTRecord implements DHTDeleteable { Future tryWriteJson(T Function(dynamic) fromJson, T newValue, {int subkey = -1, VeilidCrypto? crypto, - KeyPair? writer, + SetDHTValueOptions? options, Output? outSeqNum}) => tryWriteBytes(jsonEncodeBytes(newValue), subkey: subkey, crypto: crypto, - writer: writer, + options: options, outSeqNum: outSeqNum) .then((out) { if (out == null) { @@ -381,12 +385,12 @@ class DHTRecord implements DHTDeleteable { T Function(List) fromBuffer, T newValue, {int subkey = -1, VeilidCrypto? crypto, - KeyPair? writer, + SetDHTValueOptions? options, Output? outSeqNum}) => tryWriteBytes(newValue.writeToBuffer(), subkey: subkey, crypto: crypto, - writer: writer, + options: options, outSeqNum: outSeqNum) .then((out) { if (out == null) { diff --git a/packages/veilid_support/lib/identity_support/super_identity.dart b/packages/veilid_support/lib/identity_support/super_identity.dart index c8fd59d..008967d 100644 --- a/packages/veilid_support/lib/identity_support/super_identity.dart +++ b/packages/veilid_support/lib/identity_support/super_identity.dart @@ -98,18 +98,20 @@ sealed class SuperIdentity with _$SuperIdentity { } /// Opens an existing super identity, validates it, and returns it - static Future open({required TypedKey superRecordKey}) async { + static Future open({required TypedKey superRecordKey}) async { final pool = DHTRecordPool.instance; // SuperIdentity DHT record is public/unencrypted return (await pool.openRecordRead(superRecordKey, debugName: 'SuperIdentity::openSuperIdentity::SuperIdentityRecord')) .deleteScope((superRec) async { - final superIdentity = (await superRec.getJson(SuperIdentity.fromJson, - refreshMode: DHTRecordRefreshMode.network))!; + final superIdentity = await superRec.getJson(SuperIdentity.fromJson, + refreshMode: DHTRecordRefreshMode.network); + if (superIdentity == null) { + return null; + } await superIdentity.validate(superRecordKey: superRecordKey); - return superIdentity; }); }