diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 2c6a928e..9f70db05 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -153,6 +153,12 @@ kotlin { if (org.gradle.internal.os.OperatingSystem.current().isMacOsX) { iosMain.dependencies { implementation(libs.sqldelight.native.driver) + + // CIO has no TLS implementation on native ("TLS sessions are not + // supported on Native platform"), so every wss/https call it makes on + // a device fails at the handshake. Darwin (NSURLSession) is the engine + // apple targets must use. + implementation(libs.ktor.client.darwin) } } } diff --git a/composeApp/src/androidMain/kotlin/press/mantra/compose/network/PlatformHttpClientEngine.android.kt b/composeApp/src/androidMain/kotlin/press/mantra/compose/network/PlatformHttpClientEngine.android.kt new file mode 100644 index 00000000..a1cfbe1b --- /dev/null +++ b/composeApp/src/androidMain/kotlin/press/mantra/compose/network/PlatformHttpClientEngine.android.kt @@ -0,0 +1,6 @@ +package press.mantra.compose.network + +import io.ktor.client.engine.HttpClientEngineFactory +import io.ktor.client.engine.cio.CIO + +internal actual fun platformHttpClientEngineFactory(): HttpClientEngineFactory<*> = CIO diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/MantraApp.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/MantraApp.kt index 321545fa..324ef5da 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/MantraApp.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/MantraApp.kt @@ -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() diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/network/PlatformHttpClientEngine.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/network/PlatformHttpClientEngine.kt new file mode 100644 index 00000000..11d09c53 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/network/PlatformHttpClientEngine.kt @@ -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<*> diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/network/sockets/NostrSocketClientFactory.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/network/sockets/NostrSocketClientFactory.kt index 99262b0d..860702c6 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/network/sockets/NostrSocketClientFactory.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/network/sockets/NostrSocketClientFactory.kt @@ -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 diff --git a/composeApp/src/iosMain/kotlin/press/mantra/compose/network/PlatformHttpClientEngine.ios.kt b/composeApp/src/iosMain/kotlin/press/mantra/compose/network/PlatformHttpClientEngine.ios.kt new file mode 100644 index 00000000..6bbe3a40 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/press/mantra/compose/network/PlatformHttpClientEngine.ios.kt @@ -0,0 +1,6 @@ +package press.mantra.compose.network + +import io.ktor.client.engine.HttpClientEngineFactory +import io.ktor.client.engine.darwin.Darwin + +internal actual fun platformHttpClientEngineFactory(): HttpClientEngineFactory<*> = Darwin diff --git a/composeApp/src/jvmMain/kotlin/press/mantra/compose/network/PlatformHttpClientEngine.jvm.kt b/composeApp/src/jvmMain/kotlin/press/mantra/compose/network/PlatformHttpClientEngine.jvm.kt new file mode 100644 index 00000000..a1cfbe1b --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/press/mantra/compose/network/PlatformHttpClientEngine.jvm.kt @@ -0,0 +1,6 @@ +package press.mantra.compose.network + +import io.ktor.client.engine.HttpClientEngineFactory +import io.ktor.client.engine.cio.CIO + +internal actual fun platformHttpClientEngineFactory(): HttpClientEngineFactory<*> = CIO diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 34c59f78..5e6f2132 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -80,6 +80,7 @@ kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serializa kotlinx-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kotlinxSerialization" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } +ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } lightning-kmp-core = { module = "fr.acinq.lightning:lightning-kmp-core", version.ref = "lightningKmpCore" }