add nostr view model

This commit is contained in:
2026-04-25 08:52:42 +07:00
parent 8c6b70304d
commit 3240382498
11 changed files with 100 additions and 94 deletions

View File

@@ -23,6 +23,7 @@ kotlin {
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 {
implementation(libs.compose.runtime)

View File

@@ -1,72 +1,53 @@
package su.reya.coop
import android.content.Context
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.preferencesDataStore
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
private val FIRST_TIME_KEY = booleanPreferencesKey("first_time")
import kotlinx.coroutines.flow.flow
import su.reya.coop.coop.storage.SecretStore
@Composable
fun App() {
fun App(dbPath: String) {
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 {
val context = LocalContext.current
val scope = rememberCoroutineScope()
rememberCoroutineScope()
val navController = rememberNavController()
val isFirstTimeFlow = remember {
context.dataStore.data.map { preferences ->
preferences[FIRST_TIME_KEY] ?: true
// Get user's signer status
val hasSecretFlow = remember {
flow {
emit(secretStore.has("user_signer"))
}
}
val isFirstTime by isFirstTimeFlow.collectAsState(initial = null)
val hasSecret by hasSecretFlow.collectAsState(initial = null)
if (isFirstTime == null) {
if (hasSecret == null) {
// Loading state
return@MaterialTheme
}
NavHost(
navController = navController,
startDestination = if (isFirstTime == true) Screen.Welcome else Screen.Home
startDestination = if (hasSecret == true) Screen.Onboarding else Screen.Home
) {
composable<Screen.Welcome> { backStackEntry ->
WelcomeScreen(onContinue = {
scope.launch {
context.dataStore.edit { settings ->
settings[FIRST_TIME_KEY] = false
}
navController.navigate(Screen.Home) {
popUpTo<Screen.Welcome> { inclusive = true }
}
}
})
}
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)
}
composable<Screen.Onboarding> { backStackEntry ->
OnboardingScreen(
onOpenImport = { navController.navigate(Screen.Import) },
@@ -79,6 +60,15 @@ fun App() {
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)
}
}
}
}

View File

@@ -6,13 +6,9 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import java.io.File
class MainActivity : ComponentActivity() {
private val nostr = Nostr()
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
@@ -21,16 +17,8 @@ class MainActivity : ComponentActivity() {
val dbDir = File(filesDir, "nostr")
dbDir.mkdirs()
// Initialize nostr client
nostr.init(dbDir.absolutePath)
// Connect to bootstrap relays
lifecycleScope.launch {
nostr.connect()
}
setContent {
App()
App(dbDir.absolutePath)
}
}
}

View File

@@ -14,9 +14,6 @@ import androidx.compose.ui.unit.dp
import kotlinx.serialization.Serializable
sealed interface Screen {
@Serializable
data object Welcome : Screen
@Serializable
data object Home : Screen
@@ -33,19 +30,6 @@ sealed interface Screen {
data object New : Screen
}
@Composable
fun WelcomeScreen(onContinue: () -> Unit) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Welcome Screen")
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = onContinue) {
Text("Get Started")
}
}
}
}
@Composable
fun HomeScreen(onOpenChat: (String) -> Unit) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {

View File

@@ -5,13 +5,14 @@ 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) {
class SecretStore(private val context: Context) : SecretStorage {
private val crypto = SecretCrypto()
suspend fun set(key: String, value: String) {
override suspend fun set(key: String, value: String) {
val entry = crypto.encrypt(value)
context.dataStore.edit { prefs ->
@@ -20,7 +21,7 @@ class SecretStore(private val context: Context) {
}
}
suspend fun get(key: String): String? {
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
@@ -28,15 +29,15 @@ class SecretStore(private val context: Context) {
return crypto.decrypt(SecretEntry(encrypted, iv))
}
suspend fun clear(name: String) {
override suspend fun clear(key: String) {
context.dataStore.edit { prefs ->
prefs.remove(stringPreferencesKey("${name}_encrypted"))
prefs.remove(stringPreferencesKey("${name}_iv"))
prefs.remove(stringPreferencesKey("${key}_encrypted"))
prefs.remove(stringPreferencesKey("${key}_iv"))
}
}
suspend fun has(name: String): Boolean {
override suspend fun has(key: String): Boolean {
val prefs = context.dataStore.data.first()
return prefs[stringPreferencesKey("${name}_encrypted")] != null
return prefs[stringPreferencesKey("${key}_encrypted")] != null
}
}

View File

@@ -24,6 +24,8 @@ kotlin {
sourceSets {
commonMain.dependencies {
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 {

View File

@@ -1,9 +0,0 @@
package su.reya.coop
class Greeting {
private val platform = getPlatform()
fun greet(): String {
return "Hello, ${platform.name}!"
}
}

View File

@@ -25,4 +25,8 @@ class Nostr {
this.client?.addRelay(RelayUrl.parse("wss://user.kindpag.es"))
this.client?.connect()
}
suspend fun disconnect() {
this.client?.shutdown()
}
}

View 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()
}
}
}
}

View File

@@ -1,7 +0,0 @@
package su.reya.coop
interface Platform {
val name: String
}
expect fun getPlatform(): Platform

View File

@@ -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
}