add nostr view model
This commit is contained in:
@@ -23,6 +23,7 @@ kotlin {
|
|||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
|
||||||
implementation("androidx.datastore:datastore-preferences:1.2.1")
|
implementation("androidx.datastore:datastore-preferences:1.2.1")
|
||||||
implementation("androidx.datastore:datastore-preferences-core: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,72 +1,53 @@
|
|||||||
package su.reya.coop
|
package su.reya.coop
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
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.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import androidx.navigation.toRoute
|
import androidx.navigation.toRoute
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.launch
|
import su.reya.coop.coop.storage.SecretStore
|
||||||
|
|
||||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
|
|
||||||
private val FIRST_TIME_KEY = booleanPreferencesKey("first_time")
|
|
||||||
|
|
||||||
@Composable
|
@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 {
|
MaterialTheme {
|
||||||
val context = LocalContext.current
|
rememberCoroutineScope()
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
val isFirstTimeFlow = remember {
|
// Get user's signer status
|
||||||
context.dataStore.data.map { preferences ->
|
val hasSecretFlow = remember {
|
||||||
preferences[FIRST_TIME_KEY] ?: true
|
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
|
// Loading state
|
||||||
return@MaterialTheme
|
return@MaterialTheme
|
||||||
}
|
}
|
||||||
|
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
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 ->
|
composable<Screen.Onboarding> { backStackEntry ->
|
||||||
OnboardingScreen(
|
OnboardingScreen(
|
||||||
onOpenImport = { navController.navigate(Screen.Import) },
|
onOpenImport = { navController.navigate(Screen.Import) },
|
||||||
@@ -79,6 +60,15 @@ fun App() {
|
|||||||
composable<Screen.New> { backStackEntry ->
|
composable<Screen.New> { backStackEntry ->
|
||||||
NewScreen()
|
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,13 +6,9 @@ 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 androidx.lifecycle.lifecycleScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
private val nostr = Nostr()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -21,16 +17,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
val dbDir = File(filesDir, "nostr")
|
val dbDir = File(filesDir, "nostr")
|
||||||
dbDir.mkdirs()
|
dbDir.mkdirs()
|
||||||
|
|
||||||
// Initialize nostr client
|
|
||||||
nostr.init(dbDir.absolutePath)
|
|
||||||
|
|
||||||
// Connect to bootstrap relays
|
|
||||||
lifecycleScope.launch {
|
|
||||||
nostr.connect()
|
|
||||||
}
|
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
App()
|
App(dbDir.absolutePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ import androidx.compose.ui.unit.dp
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
sealed interface Screen {
|
sealed interface Screen {
|
||||||
@Serializable
|
|
||||||
data object Welcome : Screen
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Home : Screen
|
data object Home : Screen
|
||||||
|
|
||||||
@@ -33,19 +30,6 @@ sealed interface Screen {
|
|||||||
data object New : 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
|
@Composable
|
||||||
fun HomeScreen(onOpenChat: (String) -> Unit) {
|
fun HomeScreen(onOpenChat: (String) -> Unit) {
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ import androidx.datastore.preferences.core.edit
|
|||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import su.reya.coop.storage.SecretStorage
|
||||||
|
|
||||||
private val Context.dataStore by preferencesDataStore("secret_store")
|
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()
|
private val crypto = SecretCrypto()
|
||||||
|
|
||||||
suspend fun set(key: String, value: String) {
|
override suspend fun set(key: String, value: String) {
|
||||||
val entry = crypto.encrypt(value)
|
val entry = crypto.encrypt(value)
|
||||||
|
|
||||||
context.dataStore.edit { prefs ->
|
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 prefs = context.dataStore.data.first()
|
||||||
val encrypted = prefs[stringPreferencesKey("${key}_encrypted")] ?: return null
|
val encrypted = prefs[stringPreferencesKey("${key}_encrypted")] ?: return null
|
||||||
val iv = prefs[stringPreferencesKey("${key}_iv")] ?: 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))
|
return crypto.decrypt(SecretEntry(encrypted, iv))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun clear(name: String) {
|
override suspend fun clear(key: String) {
|
||||||
context.dataStore.edit { prefs ->
|
context.dataStore.edit { prefs ->
|
||||||
prefs.remove(stringPreferencesKey("${name}_encrypted"))
|
prefs.remove(stringPreferencesKey("${key}_encrypted"))
|
||||||
prefs.remove(stringPreferencesKey("${name}_iv"))
|
prefs.remove(stringPreferencesKey("${key}_iv"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun has(name: String): Boolean {
|
override suspend fun has(key: String): Boolean {
|
||||||
val prefs = context.dataStore.data.first()
|
val prefs = context.dataStore.data.first()
|
||||||
return prefs[stringPreferencesKey("${name}_encrypted")] != null
|
return prefs[stringPreferencesKey("${key}_encrypted")] != null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,6 +24,8 @@ kotlin {
|
|||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain.dependencies {
|
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")
|
implementation("org.rust-nostr:nostr-sdk-kmp:0.44.3")
|
||||||
}
|
}
|
||||||
commonTest.dependencies {
|
commonTest.dependencies {
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package su.reya.coop
|
|
||||||
|
|
||||||
class Greeting {
|
|
||||||
private val platform = getPlatform()
|
|
||||||
|
|
||||||
fun greet(): String {
|
|
||||||
return "Hello, ${platform.name}!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,4 +25,8 @@ class Nostr {
|
|||||||
this.client?.addRelay(RelayUrl.parse("wss://user.kindpag.es"))
|
this.client?.addRelay(RelayUrl.parse("wss://user.kindpag.es"))
|
||||||
this.client?.connect()
|
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