Merge branch 'mantra' into claude/serene-dubinsky-41325e

The ios build line, ten commits, against one nip30303 tag fix. Not a single
file is touched by both sides, so nothing had to be reconciled: this branch
changes TranslationArtifactVersionIdTag and adds TagRoundTripTest, and mantra
arrives with the Darwin ktor engine, the Room constructor, the gradle 9 stub
task, the pin bump and the comma sweep.

19543df2 wrote down the thing to check here: Phase 3 took the commas out of 84
commonTest names to get linkDebugTestIosArm64 past Kotlin/Native's "Name
contains illegal characters: ','", and nothing enforces it, so every merge
arriving with new commonTest files can quietly undo it. This one brings
TagRoundTripTest, whose six names carry no comma. Checked rather than assumed.
Apostrophes are fine -- the sweep kept "the artifact's kind" -- so "no tag
parses another tag's array" stands.

The pin bump is a chain of gitlinks and nothing else: e0d2b823 moves
lightning-kmp to e5b22f5, which moves bitcoin-kmp to 9e3c4f8, which moves
secp256k1-kmp to 8adf6911, onto the merged threshold tips. native/secp256k1 is
unchanged. All four were already in this worktree's object store, so the chain
followed offline.

testDebugUnitTest 605 and jvmTest 1227, both zero failures -- 599 and 1221
before this branch, plus the six new ones in each. The m3 audit is green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kgothatso NgakoandClaude Opus 5 committed 2026-10-03 01:24:22 +02:00
commit 7b504ae5fb
52 files changed
+718 -93

No files matched your search

@@ -5,17 +5,38 @@ import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import coil3.ImageLoader
import coil3.compose.setSingletonImageLoaderFactory
import coil3.network.ktor3.KtorNetworkFetcherFactory
import io.ktor.client.HttpClient
import press.mantra.compose.network.platformHttpClientEngineFactory
import press.mantra.compose.ui.composable.navigation.MantraNavHost
import press.mantra.compose.ui.composable.widgets.ProvideSnackbarHost
import press.mantra.compose.ui.theme.MantraTheme
import fr.acinq.phoenix.PhoenixGlobal
/**
* Coil's default ktor fetcher builds a bare `HttpClient()`, whose engine is whichever the
* classpath happens to serve first. With CIO that means no TLS on ios, so images over
* https fail exactly like the relay sockets did. Pin the singleton loader to the same
* platform engine the sockets use.
*/
private val imageHttpClient by lazy { HttpClient(platformHttpClientEngineFactory()) }
@Composable
fun MantraApp(
navController: NavHostController,
mantraGlobal: MantraGlobal,
phoenixGlobal: PhoenixGlobal
) {
setSingletonImageLoaderFactory { context ->
ImageLoader.Builder(context)
.components {
add(KtorNetworkFetcherFactory(httpClient = { imageHttpClient }))
}
.build()
}
MantraTheme {
Surface(
modifier = Modifier.fillMaxSize()
@@ -2,6 +2,7 @@ package press.mantra.compose.database
import androidx.room3.ColumnTypeConverters
import androidx.room3.AutoMigration
import androidx.room3.ConstructedBy
import androidx.room3.Database
import androidx.room3.RoomDatabase
import androidx.room3.immediateTransaction
@@ -366,6 +367,7 @@ val GENESIS_AT = Instant.fromEpochMilliseconds(1231006505000L)
AutoMigration(from = 21, to = 22),
]
)
@ConstructedBy(MantraDatabaseConstructor::class)
@ColumnTypeConverters(MantraConverters::class)
abstract class MantraDatabase: RoomDatabase() {
abstract fun broadcastNostrEventReceiptDao(): BroadcastNostrEventReceiptDao
@@ -0,0 +1,11 @@
package press.mantra.compose.network
import io.ktor.client.engine.HttpClientEngineFactory
/**
* The engine every Ktor client in the app is built on. CIO serves android and the desktop
* jvm; on apple targets its TLS layer does not exist (`TLS sessions are not supported on
* Native platform`), so every wss/https call fails at the handshake and ios uses Darwin
* (NSURLSession) instead.
*/
internal expect fun platformHttpClientEngineFactory(): HttpClientEngineFactory<*>
@@ -28,5 +28,6 @@ interface EventPublishTransport {
*
* @see RelayPool.publishEvent
*/
@Throws(press.mantra.compose.exceptions.NostrPublishException::class, kotlin.coroutines.cancellation.CancellationException::class)
suspend fun publishEvent(nostrEvent: NostrEvent, relays: Set<RelayDTO>): Flow<NostrPublishResult>
}
@@ -286,7 +286,7 @@ class RelayPool(
)
}
@Throws(press.mantra.compose.exceptions.NostrPublishException::class)
@Throws(press.mantra.compose.exceptions.NostrPublishException::class, kotlin.coroutines.cancellation.CancellationException::class)
suspend fun publishEvent(nostrEvent: press.mantra.compose.database.model.NostrEvent, relays: Set<press.mantra.compose.network.dto.RelayDTO> = emptySet()): Flow<NostrPublishResult> {
val relayUrls = relays.map { NormalizedRelayUrl(it.url).displayUrl() }
@@ -100,7 +100,7 @@ class RelaysSocketManager(
relayPool.closePool()
}
@Throws(press.mantra.compose.exceptions.NostrPublishException::class)
@Throws(press.mantra.compose.exceptions.NostrPublishException::class, kotlin.coroutines.cancellation.CancellationException::class)
suspend fun publishEvent(nostrEvent: press.mantra.compose.database.model.NostrEvent): Flow<NostrPublishResult> {
return if (userRelays.isNotEmpty()) {
relayPool.publishEvent(
@@ -115,7 +115,7 @@ class RelaysSocketManager(
}
}
@Throws(press.mantra.compose.exceptions.NostrPublishException::class)
@Throws(press.mantra.compose.exceptions.NostrPublishException::class, kotlin.coroutines.cancellation.CancellationException::class)
override suspend fun publishEvent(nostrEvent: press.mantra.compose.database.model.NostrEvent, relays: Set<press.mantra.compose.network.dto.RelayDTO>): Flow<NostrPublishResult> {
logger.d("publishEvent: $nostrEvent")
return relayPool.publishEvent(
@@ -4,10 +4,11 @@ import io.ktor.client.HttpClient
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.serialization.kotlinx.KotlinxWebsocketSerializationConverter
import kotlinx.serialization.json.Json
import press.mantra.compose.network.platformHttpClientEngineFactory
internal val defaultSocketsHttpClient by lazy {
HttpClient {
HttpClient(platformHttpClientEngineFactory()) {
install(WebSockets) {
contentConverter = KotlinxWebsocketSerializationConverter(Json {
isLenient = true
@@ -30,7 +30,7 @@ class NostrPublisherRepository(
}
}
@Throws(press.mantra.compose.exceptions.NostrPublishException::class)
@Throws(press.mantra.compose.exceptions.NostrPublishException::class, kotlin.coroutines.cancellation.CancellationException::class)
private suspend fun publishAndImportEvent(signedNostrEvent: press.mantra.compose.database.model.NostrEvent, outboxRelays: List<String> = emptyList()) {
relaysSocketManager.publishEvent(signedNostrEvent)
importEvent(signedNostrEvent)
@@ -52,7 +52,7 @@ class NostrPublisherRepository(
}
}
@Throws(press.mantra.compose.exceptions.NostrPublishException::class, press.mantra.compose.exceptions.SignatureException::class)
@Throws(press.mantra.compose.exceptions.NostrPublishException::class, press.mantra.compose.exceptions.SignatureException::class, kotlin.coroutines.cancellation.CancellationException::class)
suspend fun signPublishImportNostrEvent(
unsignedNostrEvent: press.mantra.compose.database.model.UnsignedNostrEvent,
outboxRelays: List<String>,
@@ -63,7 +63,7 @@ class NostrPublisherRepository(
}
@Throws(press.mantra.compose.exceptions.NostrPublishException::class, press.mantra.compose.exceptions.SignatureException::class)
@Throws(press.mantra.compose.exceptions.NostrPublishException::class, press.mantra.compose.exceptions.SignatureException::class, kotlin.coroutines.cancellation.CancellationException::class)
suspend fun publishRelayList(userId: String, relays: List<press.mantra.compose.network.dto.RelayDTO>): press.mantra.compose.database.model.NostrEvent {
val signedNostrEvent = nostrNotary.signRelayListMetadata(userId = userId, relays = relays).unwrapOrThrow()
relaysSocketManager.publishEvent(nostrEvent = signedNostrEvent, relays = relays.toSet())