chore: merge the develop branch into master #1

Merged
reya merged 43 commits from develop into master 2026-05-23 00:50:13 +00:00
3 changed files with 129 additions and 29 deletions
Showing only changes of commit 76050b5410 - Show all commits

View File

@@ -14,7 +14,6 @@ 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.flow
import su.reya.coop.coop.storage.SecretStore import su.reya.coop.coop.storage.SecretStore
import su.reya.coop.screens.ChatScreen import su.reya.coop.screens.ChatScreen
import su.reya.coop.screens.HomeScreen import su.reya.coop.screens.HomeScreen
@@ -37,19 +36,23 @@ fun App(dbPath: String) {
MaterialExpressiveTheme { MaterialExpressiveTheme {
rememberCoroutineScope() rememberCoroutineScope()
val navController = rememberNavController() val navController = rememberNavController()
val hasSecret by viewModel.hasSecret.collectAsState(initial = null)
// Get user's signer status LaunchedEffect(hasSecret) {
val hasSecretFlow = remember { // Navigate to the home screen if the secret is already set
flow { if (hasSecret == true) {
emit(secretStore.has("user_signer")) // Start a background notification handler
} viewModel.startNotificationHandler()
}
val hasSecret by hasSecretFlow.collectAsState(initial = null)
if (hasSecret == null) { // Navigate to the home screen
// Loading state navController.navigate(Screen.Home) {
return@MaterialExpressiveTheme popUpTo(Screen.Onboarding) { inclusive = true }
} }
}
}
// Show loading screen while initializing
if (hasSecret == null) return@MaterialExpressiveTheme
NavHost( NavHost(
navController = navController, navController = navController,
@@ -71,7 +74,6 @@ fun App(dbPath: String) {
isLoading = isCreating, isLoading = isCreating,
onSave = { name, bio, uri -> onSave = { name, bio, uri ->
viewModel.createIdentity(name, bio, uri?.toString()) viewModel.createIdentity(name, bio, uri?.toString())
navController.navigate(Screen.Home)
} }
) )
} }

View File

@@ -3,22 +3,30 @@ package su.reya.coop
import rust.nostr.sdk.Client import rust.nostr.sdk.Client
import rust.nostr.sdk.ClientBuilder import rust.nostr.sdk.ClientBuilder
import rust.nostr.sdk.ClientOptions import rust.nostr.sdk.ClientOptions
import rust.nostr.sdk.Event
import rust.nostr.sdk.EventBuilder import rust.nostr.sdk.EventBuilder
import rust.nostr.sdk.Filter
import rust.nostr.sdk.HandleNotification
import rust.nostr.sdk.Keys import rust.nostr.sdk.Keys
import rust.nostr.sdk.Kind
import rust.nostr.sdk.KindStandard
import rust.nostr.sdk.Metadata import rust.nostr.sdk.Metadata
import rust.nostr.sdk.MetadataRecord import rust.nostr.sdk.MetadataRecord
import rust.nostr.sdk.NostrConnect
import rust.nostr.sdk.NostrDatabase import rust.nostr.sdk.NostrDatabase
import rust.nostr.sdk.NostrGossip import rust.nostr.sdk.NostrGossip
import rust.nostr.sdk.NostrSigner import rust.nostr.sdk.NostrSigner
import rust.nostr.sdk.RelayMessage
import rust.nostr.sdk.RelayUrl import rust.nostr.sdk.RelayUrl
import rust.nostr.sdk.ReqExitPolicy
import rust.nostr.sdk.SubscribeAutoCloseOptions
import rust.nostr.sdk.Timestamp
class Nostr { class Nostr {
var client: Client? = null var client: Client? = null
private set private set
var signer: NostrSigner? = null var signer: NostrSigner? = null
private set private set
var deviceSigner: NostrSigner? = null
private set
fun init(dbPath: String) { fun init(dbPath: String) {
val lmdb = NostrDatabase.lmdb(dbPath) val lmdb = NostrDatabase.lmdb(dbPath)
@@ -39,20 +47,61 @@ class Nostr {
this.client?.shutdown() this.client?.shutdown()
} }
suspend fun setKeySigner(keys: Keys) {
signer = NostrSigner.keys(keys)
this.getMetadata()
}
suspend fun setRemoteSigner(signer: NostrConnect) {
this.signer = NostrSigner.nostrConnect(signer)
this.getMetadata()
}
suspend fun getMetadata() {
val currentUserPubKey = this.signer?.getPublicKey() ?: return
val opts = SubscribeAutoCloseOptions().exitPolicy(ReqExitPolicy.ExitOnEose)
val filter = Filter().author(currentUserPubKey).limit(10u).kinds(
listOf(
Kind.fromStd(KindStandard.METADATA),
Kind.fromStd(KindStandard.CONTACT_LIST),
Kind.fromStd(KindStandard.INBOX_RELAYS)
)
)
this.client?.subscribe(filter, opts)
}
suspend fun handleNotifications() {
val now = Timestamp.now()
this.client?.handleNotifications(object : HandleNotification {
override suspend fun handle(relayUrl: RelayUrl, subscriptionId: String, event: Event) {
TODO("Not yet implemented")
}
override suspend fun handleMsg(
relayUrl: RelayUrl,
msg: RelayMessage
) {
TODO("Not yet implemented")
}
})
}
suspend fun createIdentity(keys: Keys, name: String, bio: String, picture: String?) { suspend fun createIdentity(keys: Keys, name: String, bio: String, picture: String?) {
// Set signer
signer = NostrSigner.keys(keys) signer = NostrSigner.keys(keys)
// Construct metadata // Construct metadata records
val metadata = Metadata.fromRecord( val records = MetadataRecord(
MetadataRecord(
name = name, name = name,
displayName = name, displayName = name,
about = bio, about = bio,
picture = picture picture = picture
) )
)
// Construct event and sign it // Construct a nostr event and sign it
val metadata = Metadata.fromRecord(records)
val builder = EventBuilder.metadata(metadata).build(keys.publicKey()) val builder = EventBuilder.metadata(metadata).build(keys.publicKey())
val event = this.signer?.signEvent(builder) ?: return val event = this.signer?.signEvent(builder) ?: return

View File

@@ -8,14 +8,17 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import rust.nostr.sdk.Keys import rust.nostr.sdk.Keys
import rust.nostr.sdk.NostrConnect
import rust.nostr.sdk.NostrConnectUri
import su.reya.coop.storage.SecretStorage import su.reya.coop.storage.SecretStorage
import kotlin.time.Duration
class NostrViewModel( class NostrViewModel(
private val nostr: Nostr, private val nostr: Nostr,
private val secretStore: SecretStorage private val secretStore: SecretStorage
) : ViewModel() { ) : ViewModel() {
private val _isConnected = MutableStateFlow(false) private val _hasSecret = MutableStateFlow<Boolean?>(null)
val isConnected = _isConnected.asStateFlow() val hasSecret = _hasSecret.asStateFlow()
private val _isCreating = MutableStateFlow(false) private val _isCreating = MutableStateFlow(false)
val isCreating = _isCreating.asStateFlow() val isCreating = _isCreating.asStateFlow()
@@ -28,14 +31,61 @@ class NostrViewModel(
try { try {
// Connect to bootstrap relays // Connect to bootstrap relays
nostr.connect() nostr.connect()
_isConnected.value = true
// Get user's signer secret
val secret = secretStore.get("user_signer")
// If no secret is found, show onboarding screen
if (secret == null) {
_hasSecret.value = false
return@launch
}
_hasSecret.value = true
// Handle different signer types
if (secret.startsWith("nsec1")) {
val keys = Keys.parse(secret)
nostr.setKeySigner(keys)
} else if (secret.startsWith("bunker://")) {
val appKeys = getOrInitAppKeys()
val bunker = NostrConnectUri.parse(secret)
val remote = NostrConnect(
uri = bunker,
appKeys = appKeys,
timeout = Duration.parse("5"),
opts = null
)
nostr.setRemoteSigner(remote)
} else {
throw IllegalArgumentException("Invalid secret format: $secret")
}
} catch (e: Exception) { } catch (e: Exception) {
_isConnected.value = false println("Failed to connect: ${e.message}")
println(e)
} }
} }
} }
fun startNotificationHandler() {
viewModelScope.launch {
nostr.handleNotifications()
}
}
suspend fun getOrInitAppKeys(): Keys {
val secret = secretStore.get("app_keys")
// If app keys are already stored, use them
if (secret != null) {
return Keys.parse(secret)
}
// Generate new app keys and save to the secret storage
val keys = Keys.generate()
secretStore.set("app_keys", keys.secretKey().toBech32())
return keys
}
fun createIdentity(name: String, bio: String, picture: String?) { fun createIdentity(name: String, bio: String, picture: String?) {
viewModelScope.launch { viewModelScope.launch {
try { try {
@@ -48,8 +98,7 @@ class NostrViewModel(
// Save secret to the secret storage // Save secret to the secret storage
secretStore.set("user_signer", secret) secretStore.set("user_signer", secret)
} catch (e: Exception) { } catch (e: Exception) {
_isCreating.value = false println("Create identity failed: $e")
println(e)
} }
} }
} }