Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3240382498 | |||
| 8c6b70304d | |||
| fc0d6b6057 | |||
| 6295378b78 |
@@ -1,4 +1,3 @@
|
|||||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
|
||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
@@ -6,6 +5,7 @@ plugins {
|
|||||||
alias(libs.plugins.androidApplication)
|
alias(libs.plugins.androidApplication)
|
||||||
alias(libs.plugins.composeMultiplatform)
|
alias(libs.plugins.composeMultiplatform)
|
||||||
alias(libs.plugins.composeCompiler)
|
alias(libs.plugins.composeCompiler)
|
||||||
|
kotlin("plugin.serialization") version libs.versions.kotlin.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
@@ -14,11 +14,16 @@ kotlin {
|
|||||||
jvmTarget.set(JvmTarget.JVM_11)
|
jvmTarget.set(JvmTarget.JVM_11)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
androidMain.dependencies {
|
androidMain.dependencies {
|
||||||
implementation(libs.compose.uiToolingPreview)
|
implementation(libs.compose.uiToolingPreview)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
|
implementation("androidx.navigation:navigation-compose:2.8.8")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
|
||||||
|
implementation("androidx.datastore:datastore-preferences:1.2.1")
|
||||||
|
implementation("androidx.datastore:datastore-preferences-core:1.2.1")
|
||||||
|
implementation("org.jetbrains.compose.material3:material3*:1.10.0-alpha05")
|
||||||
}
|
}
|
||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
implementation(libs.compose.runtime)
|
implementation(libs.compose.runtime)
|
||||||
|
|||||||
@@ -1,48 +1,73 @@
|
|||||||
package su.reya.coop
|
package su.reya.coop
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.safeContentPadding
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.runtime.remember
|
||||||
import org.jetbrains.compose.resources.painterResource
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import coop.composeapp.generated.resources.Res
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import coop.composeapp.generated.resources.compose_multiplatform
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import androidx.navigation.toRoute
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import su.reya.coop.coop.storage.SecretStore
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Preview
|
fun App(dbPath: String) {
|
||||||
fun App() {
|
val context = LocalContext.current
|
||||||
|
val nostr = remember { Nostr() }
|
||||||
|
val secretStore = remember { SecretStore(context) }
|
||||||
|
val viewModel: NostrViewModel = viewModel { NostrViewModel(nostr, secretStore) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.initAndConnect(dbPath)
|
||||||
|
}
|
||||||
|
|
||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
var showContent by remember { mutableStateOf(false) }
|
rememberCoroutineScope()
|
||||||
Column(
|
val navController = rememberNavController()
|
||||||
modifier = Modifier
|
|
||||||
.background(MaterialTheme.colorScheme.primaryContainer)
|
// Get user's signer status
|
||||||
.safeContentPadding()
|
val hasSecretFlow = remember {
|
||||||
.fillMaxSize(),
|
flow {
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
emit(secretStore.has("user_signer"))
|
||||||
) {
|
|
||||||
Button(onClick = { showContent = !showContent }) {
|
|
||||||
Text("Click me!")
|
|
||||||
}
|
}
|
||||||
AnimatedVisibility(showContent) {
|
}
|
||||||
val greeting = remember { Greeting().greet() }
|
val hasSecret by hasSecretFlow.collectAsState(initial = null)
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
if (hasSecret == null) {
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
// Loading state
|
||||||
) {
|
return@MaterialTheme
|
||||||
Image(painterResource(Res.drawable.compose_multiplatform), null)
|
}
|
||||||
Text("Compose: $greeting")
|
|
||||||
}
|
NavHost(
|
||||||
|
navController = navController,
|
||||||
|
startDestination = if (hasSecret == true) Screen.Onboarding else Screen.Home
|
||||||
|
) {
|
||||||
|
composable<Screen.Onboarding> { backStackEntry ->
|
||||||
|
OnboardingScreen(
|
||||||
|
onOpenImport = { navController.navigate(Screen.Import) },
|
||||||
|
onOpenNew = { navController.navigate(Screen.New) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable<Screen.Import> { backStackEntry ->
|
||||||
|
ImportScreen()
|
||||||
|
}
|
||||||
|
composable<Screen.New> { backStackEntry ->
|
||||||
|
NewScreen()
|
||||||
|
}
|
||||||
|
composable<Screen.Home> { backStackEntry ->
|
||||||
|
HomeScreen(
|
||||||
|
onOpenChat = { id -> navController.navigate(Screen.Chat(id)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable<Screen.Chat> { backStackEntry ->
|
||||||
|
val chat: Screen.Chat = backStackEntry.toRoute()
|
||||||
|
ChatScreen(id = chat.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,19 @@ import androidx.activity.compose.setContent
|
|||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Get database directory
|
||||||
|
val dbDir = File(filesDir, "nostr")
|
||||||
|
dbDir.mkdirs()
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
App()
|
App(dbDir.absolutePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
82
composeApp/src/androidMain/kotlin/su/reya/coop/Screens.kt
Normal file
82
composeApp/src/androidMain/kotlin/su/reya/coop/Screens.kt
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package su.reya.coop
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
sealed interface Screen {
|
||||||
|
@Serializable
|
||||||
|
data object Home : Screen
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Chat(val id: String) : Screen
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Onboarding : Screen
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Import : Screen
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object New : Screen
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomeScreen(onOpenChat: (String) -> Unit) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Text("Home Screen")
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Button(onClick = { onOpenChat("123") }) {
|
||||||
|
Text("Open Chat 123")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatScreen(id: String) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Text("Chat Screen (ID: $id)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OnboardingScreen(onOpenImport: () -> Unit, onOpenNew: () -> Unit) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Text("Onboarding Screen")
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Button(onClick = onOpenImport) {
|
||||||
|
Text("Import")
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Button(onClick = onOpenNew) {
|
||||||
|
Text("New")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ImportScreen() {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Text("Import Screen")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NewScreen() {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Text("New Screen")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package su.reya.coop.coop.storage
|
||||||
|
|
||||||
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
|
import android.security.keystore.KeyProperties
|
||||||
|
import android.util.Base64
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.security.KeyStore
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.KeyGenerator
|
||||||
|
import javax.crypto.SecretKey
|
||||||
|
import javax.crypto.spec.GCMParameterSpec
|
||||||
|
|
||||||
|
data class SecretEntry(
|
||||||
|
val encrypted: String,
|
||||||
|
val iv: String
|
||||||
|
)
|
||||||
|
|
||||||
|
class SecretCrypto {
|
||||||
|
private val keyAlias = "coop"
|
||||||
|
private val keyStoreType = "AndroidKeyStore"
|
||||||
|
private val transformation = "AES/GCM/NoPadding"
|
||||||
|
|
||||||
|
fun encrypt(content: String): SecretEntry {
|
||||||
|
// Initialize cipher
|
||||||
|
val cipher = Cipher.getInstance(transformation)
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey())
|
||||||
|
|
||||||
|
// Encrypt content
|
||||||
|
val encrypted = cipher.doFinal(content.toByteArray())
|
||||||
|
val iv = cipher.iv
|
||||||
|
|
||||||
|
return SecretEntry(
|
||||||
|
encrypted = Base64.encodeToString(encrypted, Base64.NO_WRAP),
|
||||||
|
iv = Base64.encodeToString(iv, Base64.NO_WRAP)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decrypt(entry: SecretEntry): String {
|
||||||
|
val encrypted = Base64.decode(entry.encrypted, Base64.NO_WRAP)
|
||||||
|
val iv = Base64.decode(entry.iv, Base64.NO_WRAP)
|
||||||
|
|
||||||
|
// Initialize cipher
|
||||||
|
val cipher = Cipher.getInstance(transformation)
|
||||||
|
val spec = GCMParameterSpec(128, iv)
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, getOrCreateKey(), spec)
|
||||||
|
|
||||||
|
// Decrypt content
|
||||||
|
val plaintext = cipher.doFinal(encrypted)
|
||||||
|
|
||||||
|
return String(plaintext, StandardCharsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getOrCreateKey(): SecretKey {
|
||||||
|
val keyStore = KeyStore.getInstance(keyStoreType).apply { load(null) }
|
||||||
|
val existingKey = keyStore.getKey(keyAlias, null)
|
||||||
|
|
||||||
|
// Return existing key if available
|
||||||
|
if (existingKey is SecretKey) return existingKey
|
||||||
|
|
||||||
|
// Construct a new key generator
|
||||||
|
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, keyStoreType)
|
||||||
|
|
||||||
|
// Initialize key generation parameters
|
||||||
|
val spec = KeyGenParameterSpec.Builder(
|
||||||
|
keyAlias,
|
||||||
|
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
|
||||||
|
)
|
||||||
|
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||||
|
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||||
|
.setKeySize(256)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
// Generate a new key
|
||||||
|
keyGenerator.init(spec)
|
||||||
|
|
||||||
|
return keyGenerator.generateKey()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package su.reya.coop.coop.storage
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import su.reya.coop.storage.SecretStorage
|
||||||
|
|
||||||
|
private val Context.dataStore by preferencesDataStore("secret_store")
|
||||||
|
|
||||||
|
class SecretStore(private val context: Context) : SecretStorage {
|
||||||
|
private val crypto = SecretCrypto()
|
||||||
|
|
||||||
|
override suspend fun set(key: String, value: String) {
|
||||||
|
val entry = crypto.encrypt(value)
|
||||||
|
|
||||||
|
context.dataStore.edit { prefs ->
|
||||||
|
prefs[stringPreferencesKey("${key}_encrypted")] = entry.encrypted
|
||||||
|
prefs[stringPreferencesKey("${key}_iv")] = entry.iv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun get(key: String): String? {
|
||||||
|
val prefs = context.dataStore.data.first()
|
||||||
|
val encrypted = prefs[stringPreferencesKey("${key}_encrypted")] ?: return null
|
||||||
|
val iv = prefs[stringPreferencesKey("${key}_iv")] ?: return null
|
||||||
|
|
||||||
|
return crypto.decrypt(SecretEntry(encrypted, iv))
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun clear(key: String) {
|
||||||
|
context.dataStore.edit { prefs ->
|
||||||
|
prefs.remove(stringPreferencesKey("${key}_encrypted"))
|
||||||
|
prefs.remove(stringPreferencesKey("${key}_iv"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun has(key: String): Boolean {
|
||||||
|
val prefs = context.dataStore.data.first()
|
||||||
|
return prefs[stringPreferencesKey("${key}_encrypted")] != null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,4 +9,14 @@ org.gradle.caching=true
|
|||||||
|
|
||||||
#Android
|
#Android
|
||||||
android.nonTransitiveRClass=true
|
android.nonTransitiveRClass=true
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
|
android.defaults.buildfeatures.resvalues=true
|
||||||
|
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
|
||||||
|
android.enableAppCompileTimeRClass=false
|
||||||
|
android.usesSdkInManifest.disallowed=false
|
||||||
|
android.uniquePackageNames=false
|
||||||
|
android.dependency.useConstraints=true
|
||||||
|
android.r8.strictFullModeForKeepRules=false
|
||||||
|
android.r8.optimizedResourceShrinking=false
|
||||||
|
android.builtInKotlin=false
|
||||||
|
android.newDsl=false
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.13.2"
|
agp = "9.2.0"
|
||||||
android-compileSdk = "36"
|
android-compileSdk = "36"
|
||||||
android-minSdk = "24"
|
android-minSdk = "24"
|
||||||
android-targetSdk = "36"
|
android-targetSdk = "36"
|
||||||
@@ -8,10 +8,12 @@ androidx-appcompat = "1.7.1"
|
|||||||
androidx-core = "1.18.0"
|
androidx-core = "1.18.0"
|
||||||
androidx-espresso = "3.7.0"
|
androidx-espresso = "3.7.0"
|
||||||
androidx-lifecycle = "2.10.0"
|
androidx-lifecycle = "2.10.0"
|
||||||
|
androidx-navigation = "2.8.8"
|
||||||
androidx-testExt = "1.3.0"
|
androidx-testExt = "1.3.0"
|
||||||
composeMultiplatform = "1.10.3"
|
composeMultiplatform = "1.10.3"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
kotlin = "2.3.20"
|
kotlin = "2.3.20"
|
||||||
|
kotlinx-serialization = "1.8.0"
|
||||||
material3 = "1.10.0-alpha05"
|
material3 = "1.10.0-alpha05"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
@@ -23,6 +25,8 @@ androidx-testExt-junit = { module = "androidx.test.ext:junit", version.ref = "an
|
|||||||
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-espresso" }
|
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-espresso" }
|
||||||
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
|
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
|
||||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
|
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
|
||||||
|
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
|
||||||
|
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
|
||||||
compose-uiTooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "composeMultiplatform" }
|
compose-uiTooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "composeMultiplatform" }
|
||||||
androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
|
androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
|
||||||
androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
||||||
|
|||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ kotlin {
|
|||||||
jvmTarget.set(JvmTarget.JVM_11)
|
jvmTarget.set(JvmTarget.JVM_11)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
listOf(
|
listOf(
|
||||||
iosArm64(),
|
iosArm64(),
|
||||||
iosSimulatorArm64()
|
iosSimulatorArm64()
|
||||||
@@ -21,10 +21,12 @@ kotlin {
|
|||||||
isStatic = true
|
isStatic = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
// put your Multiplatform dependencies here
|
implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
|
||||||
|
implementation("org.rust-nostr:nostr-sdk-kmp:0.44.3")
|
||||||
}
|
}
|
||||||
commonTest.dependencies {
|
commonTest.dependencies {
|
||||||
implementation(libs.kotlin.test)
|
implementation(libs.kotlin.test)
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package su.reya.coop
|
|
||||||
|
|
||||||
class Greeting {
|
|
||||||
private val platform = getPlatform()
|
|
||||||
|
|
||||||
fun greet(): String {
|
|
||||||
return "Hello, ${platform.name}!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
32
shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt
Normal file
32
shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package su.reya.coop
|
||||||
|
|
||||||
|
import rust.nostr.sdk.Client
|
||||||
|
import rust.nostr.sdk.ClientBuilder
|
||||||
|
import rust.nostr.sdk.ClientOptions
|
||||||
|
import rust.nostr.sdk.NostrDatabase
|
||||||
|
import rust.nostr.sdk.NostrGossip
|
||||||
|
import rust.nostr.sdk.RelayUrl
|
||||||
|
|
||||||
|
class Nostr {
|
||||||
|
var client: Client? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun init(dbPath: String) {
|
||||||
|
val lmdb = NostrDatabase.lmdb(dbPath)
|
||||||
|
val gossip = NostrGossip.inMemory()
|
||||||
|
val opts = ClientOptions().automaticAuthentication(false)
|
||||||
|
|
||||||
|
client = ClientBuilder().database(lmdb).gossip(gossip).opts(opts).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun connect() {
|
||||||
|
this.client?.addRelay(RelayUrl.parse("wss://relay.damus.io"))
|
||||||
|
this.client?.addRelay(RelayUrl.parse("wss://relay.primal.net"))
|
||||||
|
this.client?.addRelay(RelayUrl.parse("wss://user.kindpag.es"))
|
||||||
|
this.client?.connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun disconnect() {
|
||||||
|
this.client?.shutdown()
|
||||||
|
}
|
||||||
|
}
|
||||||
44
shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt
Normal file
44
shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package su.reya.coop
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.NonCancellable
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import su.reya.coop.storage.SecretStorage
|
||||||
|
|
||||||
|
class NostrViewModel(
|
||||||
|
private val nostr: Nostr,
|
||||||
|
private val secretStore: SecretStorage
|
||||||
|
) : ViewModel() {
|
||||||
|
private val _isConnected = MutableStateFlow(false)
|
||||||
|
val isConnected = _isConnected.asStateFlow()
|
||||||
|
|
||||||
|
fun initAndConnect(dbPath: String) {
|
||||||
|
// Initialize nostr client
|
||||||
|
nostr.init(dbPath)
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
// Connect to bootstrap relays
|
||||||
|
nostr.connect()
|
||||||
|
_isConnected.value = true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_isConnected.value = false
|
||||||
|
println(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
// Ensure all relays are disconnect
|
||||||
|
viewModelScope.launch {
|
||||||
|
withContext(NonCancellable) {
|
||||||
|
nostr.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package su.reya.coop
|
|
||||||
|
|
||||||
interface Platform {
|
|
||||||
val name: String
|
|
||||||
}
|
|
||||||
|
|
||||||
expect fun getPlatform(): Platform
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package su.reya.coop.storage
|
||||||
|
|
||||||
|
interface SecretStorage {
|
||||||
|
suspend fun get(key: String): String?
|
||||||
|
suspend fun set(key: String, value: String)
|
||||||
|
suspend fun clear(key: String)
|
||||||
|
suspend fun has(key: String): Boolean
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user