From 0abc6317735b60af7aa02cec637b66e5c66dfd2b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 4 Dec 2025 14:41:33 -0500 Subject: [PATCH] - Removes compose bom from Quartz to avoid unnecessary dependencies. - Removes old datetime dependencies from Quartz - Moves compose.runtime dependencies to compose.runtime.annotation - Adds dependency on coroutines directly (instead of through compose runtime) - Removes old secp256 target dependencies - Adds Default scope for NostrClient and Relay Authenticator - Updates readme --- README.md | 25 +++++++++++++------ ammolite/build.gradle | 3 --- .../ammolite/relays/BundledUpdate.kt | 5 ---- gradle/libs.versions.toml | 12 ++++----- quartz/build.gradle.kts | 18 +++++-------- .../nip01Core/relay/client/NostrClient.kt | 5 +++- .../relay/client/auth/RelayAuthenticator.kt | 5 +++- .../NostrClientSubscriptionAsFlowTest.kt | 4 +++ 8 files changed, 41 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 700f2cefe..46f7773a0 100644 --- a/README.md +++ b/README.md @@ -239,6 +239,15 @@ openssl base64 < | tr -d '\n' | tee some_signing_key.j ### Installing +Add Maven Central and Google Maven to your repositories: + +```gradle +repositories { + mavenCentral() + google() +} +``` + Add the following line to your `commonMain` dependencies: ```gradle @@ -283,8 +292,8 @@ val amberSigner = NostrSignerExternal( ) ``` -Create a single NostrClient for the entire application and control which relays it will access by -registering subscriptions and sending events. The pool will automatically changed based on filters + +Create a single `NostrClient` for the entire application and control which relays it will access by +registering subscriptions and sending events. The pool will automatically change based on filters + outbox events. You will need a coroutine scope to process events and if you are using OKHttp, we offer a basic @@ -301,9 +310,11 @@ val client = NostrClient(socketBuilder, appScope) If you want to auth, given a logged-in `signer`: ```kt -val authCoordinator = RelayAuthenticator(client, appScope) { challenge, relay -> - val authedEvent = RelayAuthEvent.create(relay.url, challenge, signer) - client.sendIfExists(authedEvent, relay.url) +val authCoordinator = RelayAuthenticator(client, appScope) { authTemplate -> + listOf( + // for each signed-in user, return an event + signer.sign(authTemplate) + ) } ``` @@ -311,8 +322,8 @@ To make a request subscription simply do: ```kt val metadataSub = client.req( - relays = listOf("wss://nos.lol", "wss://nostr.mom"), - filters = Filter( + relay = "wss://nos.lol", + filter = Filter( kinds = listOf(MetadataEvent.KIND), authors = listOf(signer.pubkey) ) diff --git a/ammolite/build.gradle b/ammolite/build.gradle index 3578fb626..45f99f5b8 100644 --- a/ammolite/build.gradle +++ b/ammolite/build.gradle @@ -53,11 +53,8 @@ dependencies { // Lru Cache used in Relay stats implementation libs.androidx.core.ktx - // Compose's @Immutable and @Stable classes - implementation libs.androidx.compose.runtime implementation project(path: ':quartz') - testImplementation libs.junit androidTestImplementation libs.androidx.junit diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/BundledUpdate.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/BundledUpdate.kt index e94e62a61..037a603db 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/BundledUpdate.kt +++ b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/BundledUpdate.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.ammolite.relays -import androidx.compose.runtime.Stable import com.vitorpamplona.ammolite.service.checkNotInMainThread import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineDispatcher @@ -37,7 +36,6 @@ import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.atomic.AtomicBoolean /** This class is designed to have a waiting time between two calls of invalidate */ -@Stable class BundledUpdate( val delay: Long, val dispatcher: CoroutineDispatcher = Dispatchers.IO, @@ -63,7 +61,6 @@ class BundledUpdate( } /** This class is designed to have a waiting time between two calls of invalidate */ -@Stable class BasicBundledUpdate( val delay: Long, val dispatcher: CoroutineDispatcher = Dispatchers.IO, @@ -101,7 +98,6 @@ class BasicBundledUpdate( } /** This class is designed to have a waiting time between two calls of invalidate */ -@Stable class BundledInsert( val delay: Long, val dispatcher: CoroutineDispatcher = Dispatchers.IO, @@ -127,7 +123,6 @@ class BundledInsert( } /** This class is designed to have a waiting time between two calls of invalidate */ -@Stable class BasicBundledInsert( val delay: Long, val dispatcher: CoroutineDispatcher = Dispatchers.IO, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6ecf4ca97..aa01546a5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,6 +14,7 @@ benchmarkJunit4 = "1.4.1" biometricKtx = "1.2.0-alpha05" coil = "3.3.0" composeBom = "2025.12.00" +composeRuntimeAnnotation = "1.10.0" coreKtx = "1.17.0" datastore = "1.2.0" espressoCore = "3.7.0" @@ -26,7 +27,7 @@ jtorctl = "0.4.5.7" junit = "4.13.2" kotlin = "2.2.21" kotlinxCollectionsImmutable = "0.4.0" -kotlinxDatetime = "0.7.1" +kotlinxCoroutinesCore = "1.10.2" kotlinxSerialization = "1.9.0" kotlinxSerializationPlugin = "2.2.21" languageId = "17.0.6" @@ -77,8 +78,8 @@ androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = " androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidxCamera" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } -androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime" } -androidx-collection-jvm = { group = "androidx.collection", name = "collection-jvm", version.ref = "androidxCollection" } +androidx-compose-runtime-annotation = { group = "androidx.compose.runtime", name = "runtime-annotation", version.ref = "composeRuntimeAnnotation" } +androidx-collection = { group = "androidx.collection", name = "collection", version.ref = "androidxCollection" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } @@ -120,7 +121,7 @@ jna = { group = "net.java.dev.jna", name = "jna", version.ref = "jna" } jtorctl = { module = "info.guardianproject:jtorctl", version.ref = "jtorctl" } junit = { group = "junit", name = "junit", version.ref = "junit" } kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" } -kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" } kotlinx-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kotlinxSerialization" } lazysodium-java = { group = "com.goterl", name = "lazysodium-java", version.ref = "lazysodiumJava" } @@ -137,9 +138,6 @@ rfc3986-normalizer = { group = "org.czeal", name = "rfc3986", version.ref = "rfc secp256k1-kmp-common = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp", version.ref = "secp256k1KmpJniAndroid" } secp256k1-kmp-jni-android = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-android", version.ref = "secp256k1KmpJniAndroid" } secp256k1-kmp-jni-jvm = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-jvm", version.ref = "secp256k1KmpJniAndroid" } -secp256k1-kmp-jni-jvm-linux = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-jvm-linux", version.ref = "secp256k1KmpJniAndroid" } -secp256k1-kmp-jni-jvm-darwin = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-jvm-darwin", version.ref = "secp256k1KmpJniAndroid" } -secp256k1-kmp-jni-jvm-mingw = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-jvm-mingw", version.ref = "secp256k1KmpJniAndroid" } tor-android = { module = "info.guardianproject:tor-android", version.ref = "torAndroid" } unifiedpush = { group = "com.github.UnifiedPush", name = "android-connector", version.ref = "unifiedpush" } url-detector = { group = "io.github.url-detector", name = "url-detector", version.ref = "urlDetector" } diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index 8fd17de15..c5b947c87 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -95,10 +95,13 @@ kotlin { commonMain { dependencies { implementation(libs.kotlin.stdlib) - implementation(project.dependencies.platform(libs.androidx.compose.bom)) + implementation(libs.kotlinx.coroutines.core) + + // For LruCache + implementation(libs.androidx.collection) // @Immutable and @Stable - implementation(libs.androidx.compose.runtime) + implementation(libs.androidx.compose.runtime.annotation) // Bitcoin secp256k1 bindings api(libs.secp256k1.kmp.common) @@ -106,9 +109,6 @@ kotlin { // Kotlin serialization for the times where we need the Json tree and performance is not that important. implementation(libs.kotlinx.serialization.json) - // in your shared module's dependencies block - // implementation( libs.kotlinx.datetime) - // immutable collections to avoid recomposition implementation(libs.kotlinx.collections.immutable) } @@ -126,9 +126,6 @@ kotlin { dependsOn(commonMain.get()) dependencies { - // For LruCache - implementation(libs.androidx.collection.jvm) - // Normalizes URLs api(libs.rfc3986.normalizer) @@ -157,7 +154,7 @@ kotlin { dependsOn(jvmAndroid) dependencies { // Bitcoin secp256k1 bindings - api(libs.secp256k1.kmp.jni.jvm) + implementation (libs.secp256k1.kmp.jni.jvm) // LibSodium for ChaCha encryption (NIP-44) implementation (libs.lazysodium.java) @@ -178,9 +175,6 @@ kotlin { dependencies { implementation(libs.androidx.core.ktx) - // @Immutable and @Stable - implementation(libs.androidx.compose.runtime) - // Bitcoin secp256k1 bindings to Android api(libs.secp256k1.kmp.jni.android) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt index cc3ef2db3..d5e764b80 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt @@ -37,7 +37,10 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.IO +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine @@ -74,7 +77,7 @@ import kotlinx.coroutines.launch */ class NostrClient( private val websocketBuilder: WebsocketBuilder, - private val scope: CoroutineScope, + private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()), ) : INostrClient, IRelayClientListener { private val relayPool: RelayPool = RelayPool(websocketBuilder, this) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/auth/RelayAuthenticator.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/auth/RelayAuthenticator.kt index 0574ef147..7b468f9e2 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/auth/RelayAuthenticator.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/auth/RelayAuthenticator.kt @@ -32,6 +32,9 @@ import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch interface IAuthStatus { @@ -44,7 +47,7 @@ object EmptyIAuthStatus : IAuthStatus { class RelayAuthenticator( val client: INostrClient, - val scope: CoroutineScope, + val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()), val signWithAllLoggedInUsers: suspend (EventTemplate) -> List, ) : IAuthStatus { private val authStatus = mutableMapOf() diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionAsFlowTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionAsFlowTest.kt index 0910410d6..8e90f70fc 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionAsFlowTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionAsFlowTest.kt @@ -28,6 +28,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.debounce @@ -43,6 +45,7 @@ class NostrClientSubscriptionAsFlowTest : BaseNostrClientTest() { return joinToString { (it.createdAt - starting).toString() } } + @OptIn(ExperimentalCoroutinesApi::class) @Test fun testNostrClientSubscriptionAsFlow() = runTest { @@ -81,6 +84,7 @@ class NostrClientSubscriptionAsFlowTest : BaseNostrClientTest() { assertEquals(10, feedStates.size) } + @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) @Test fun testNostrClientSubscriptionAsFlowDebouncing() = runTest {