fix create identity flow

This commit is contained in:
2026-05-08 12:17:30 +07:00
parent 7acff87d9b
commit 5c31f7a0d6
6 changed files with 95 additions and 50 deletions

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M313,520L537,744L480,800L160,480L480,160L537,216L313,440L800,440L800,520L313,520Z" />
</vector>

View File

@@ -121,6 +121,7 @@ fun App(dbPath: String) {
NewIdentityScreen( NewIdentityScreen(
isLoading = isCreating, isLoading = isCreating,
onBack = { navController.popBackStack() },
onSave = { name, bio, uri -> onSave = { name, bio, uri ->
viewModel.createIdentity(name, bio, uri?.toString()) viewModel.createIdentity(name, bio, uri?.toString())
} }

View File

@@ -3,6 +3,7 @@ package su.reya.coop.screens
import android.net.Uri import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -14,9 +15,13 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.LoadingIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
@@ -24,6 +29,8 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -35,12 +42,17 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import coop.composeapp.generated.resources.Res
import coop.composeapp.generated.resources.ic_arrow_back
import coop.composeapp.generated.resources.ic_avatar
import org.jetbrains.compose.resources.painterResource
import su.reya.coop.LocalSnackbarHostState import su.reya.coop.LocalSnackbarHostState
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun NewIdentityScreen( fun NewIdentityScreen(
isLoading: Boolean, isLoading: Boolean,
onBack: () -> Unit,
onSave: (name: String, bio: String, picture: Uri?) -> Unit onSave: (name: String, bio: String, picture: Uri?) -> Unit
) { ) {
val snackbarHostState = LocalSnackbarHostState.current val snackbarHostState = LocalSnackbarHostState.current
@@ -56,17 +68,34 @@ fun NewIdentityScreen(
Scaffold( Scaffold(
containerColor = MaterialTheme.colorScheme.surfaceContainer, containerColor = MaterialTheme.colorScheme.surfaceContainer,
snackbarHost = { SnackbarHost(snackbarHostState) }, snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = {
TopAppBar(
title = { Text("Create a new identity") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
painter = painterResource(Res.drawable.ic_arrow_back),
contentDescription = "User"
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
)
)
},
content = { innerPadding -> content = { innerPadding ->
Surface( Surface(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(top = innerPadding.calculateTopPadding()), .padding(top = innerPadding.calculateTopPadding()),
color = MaterialTheme.colorScheme.surfaceContainer, color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp),
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(16.dp) .padding(24.dp)
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
@@ -74,7 +103,8 @@ fun NewIdentityScreen(
Box( Box(
modifier = Modifier modifier = Modifier
.size(120.dp) .size(120.dp)
.clip(CircleShape), .clip(CircleShape)
.clickable { launcher.launch("image/*") },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
if (picture != null) { if (picture != null) {
@@ -90,7 +120,14 @@ fun NewIdentityScreen(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
// Box(contentAlignment = Alignment.Center) {
Icon(
painter = painterResource(Res.drawable.ic_avatar),
contentDescription = "Pick avatar",
modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
} }
} }
} }
@@ -115,13 +152,18 @@ fun NewIdentityScreen(
onClick = { onClick = {
onSave(name, bio, picture) onSave(name, bio, picture)
}, },
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.height(ButtonDefaults.LargeContainerHeight),
enabled = name.isNotBlank() && !isLoading, enabled = name.isNotBlank() && !isLoading,
) { ) {
if (isLoading) { if (isLoading) {
LoadingIndicator() LoadingIndicator()
} else { } else {
Text("Save & Continue") Text(
text = "Save & Continue",
style = MaterialTheme.typography.titleLargeEmphasized,
)
} }
} }
} }

View File

@@ -3,21 +3,16 @@ package su.reya.coop.screens
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
@@ -34,7 +29,6 @@ import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coop.composeapp.generated.resources.Res import coop.composeapp.generated.resources.Res
import coop.composeapp.generated.resources.coop import coop.composeapp.generated.resources.coop
import coop.composeapp.generated.resources.ic_scanner
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
import su.reya.coop.LocalSnackbarHostState import su.reya.coop.LocalSnackbarHostState
@@ -92,36 +86,20 @@ fun OnboardingScreen(onOpenImport: () -> Unit, onOpenNew: () -> Unit) {
) )
} }
Spacer(modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.size(16.dp))
Row( FilledTonalButton(
modifier = Modifier.fillMaxWidth(), onClick = onOpenImport,
modifier = Modifier
.fillMaxWidth()
.height(ButtonDefaults.LargeContainerHeight),
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
contentColor = MaterialTheme.colorScheme.onTertiaryContainer
),
) { ) {
FilledTonalButton( Text(
onClick = onOpenImport, text = "Import identity",
modifier = Modifier style = MaterialTheme.typography.titleLargeEmphasized,
.weight(2f) )
.height(ButtonDefaults.MediumContainerHeight),
) {
Text(
text = "Import identity",
style = MaterialTheme.typography.titleMediumEmphasized,
)
}
Spacer(modifier = Modifier.width(8.dp))
FilledTonalIconButton(
onClick = onOpenImport,
modifier = Modifier
.weight(1f)
.height(ButtonDefaults.MediumContainerHeight),
colors = IconButtonDefaults.filledTonalIconButtonColors(
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
contentColor = MaterialTheme.colorScheme.onTertiaryContainer
)
) {
Icon(
painter = painterResource(Res.drawable.ic_scanner),
contentDescription = "Scan QR"
)
}
} }
} }
} }

View File

@@ -25,12 +25,14 @@ import rust.nostr.sdk.RelayMetadata
import rust.nostr.sdk.RelayUrl import rust.nostr.sdk.RelayUrl
import rust.nostr.sdk.ReqExitPolicy import rust.nostr.sdk.ReqExitPolicy
import rust.nostr.sdk.ReqTarget import rust.nostr.sdk.ReqTarget
import rust.nostr.sdk.SleepWhenIdle
import rust.nostr.sdk.SubscribeAutoCloseOptions import rust.nostr.sdk.SubscribeAutoCloseOptions
import rust.nostr.sdk.Tag import rust.nostr.sdk.Tag
import rust.nostr.sdk.Timestamp import rust.nostr.sdk.Timestamp
import rust.nostr.sdk.UnsignedEvent import rust.nostr.sdk.UnsignedEvent
import rust.nostr.sdk.UnwrappedGift import rust.nostr.sdk.UnwrappedGift
import rust.nostr.sdk.extractMessagingRelayList import rust.nostr.sdk.extractMessagingRelayList
import kotlin.time.Duration
class Nostr { class Nostr {
var client: Client? = null var client: Client? = null
@@ -47,6 +49,7 @@ class Nostr {
suspend fun init(dbPath: String) { suspend fun init(dbPath: String) {
val lmdb = NostrDatabase.lmdb(dbPath) val lmdb = NostrDatabase.lmdb(dbPath)
val gossip = NostrGossip.inMemory() val gossip = NostrGossip.inMemory()
val idleTimeout = Duration.parse("5m")
client = client =
ClientBuilder() ClientBuilder()
@@ -56,6 +59,7 @@ class Nostr {
.maxRelays(20u) .maxRelays(20u)
.verifySubscriptions(false) .verifySubscriptions(false)
.automaticAuthentication(false) .automaticAuthentication(false)
.sleepWhenIdle(SleepWhenIdle.Enabled(idleTimeout))
.build() .build()
} }
@@ -343,30 +347,30 @@ class Nostr {
} }
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)
// Send relay list event // Send relay list event
val relayList = getDefaultRelayList() val relayList = getDefaultRelayList()
val relayListEvent = EventBuilder.relayList(relayList).sign(signer!!); val relayListEvent = EventBuilder.relayList(relayList).signWithKeys(keys);
client?.sendEvent(relayListEvent) client?.sendEvent(relayListEvent)
// Send messaging relay list event // Send messaging relay list event
val msgRelayList = getMsgRelayList() val msgRelayList = getMsgRelayList()
val msgRelayListEvent = EventBuilder.nip17RelayList(msgRelayList).sign(signer!!) val msgRelayListEvent = EventBuilder.nip17RelayList(msgRelayList).signWithKeys(keys)
client?.sendEventNoWait(msgRelayListEvent) client?.sendEventNoWait(msgRelayListEvent)
// Send metadata event // Send metadata event
val metadata = val metadata =
Metadata.fromRecord(MetadataRecord(name = name, about = bio, picture = picture)) Metadata.fromRecord(MetadataRecord(name = name, about = bio, picture = picture))
val metadataEvent = EventBuilder.metadata(metadata).sign(signer!!) val metadataEvent = EventBuilder.metadata(metadata).signWithKeys(keys)
client?.sendEventNoWait(metadataEvent) client?.sendEventNoWait(metadataEvent)
// Send contact list event // Send contact list event
val defaultContact = val defaultContact =
listOf(Contact(publicKey = PublicKey.parse("npub1j3rz3ndl902lya6ywxvy5c983lxs8mpukqnx4pa4lt5wrykwl5ys7wpw3x"))) listOf(Contact(publicKey = PublicKey.parse("npub1j3rz3ndl902lya6ywxvy5c983lxs8mpukqnx4pa4lt5wrykwl5ys7wpw3x")))
val contactListEvent = EventBuilder.contactList(defaultContact).sign(signer!!) val contactListEvent = EventBuilder.contactList(defaultContact).signWithKeys(keys)
client?.sendEventNoWait(contactListEvent) client?.sendEventNoWait(contactListEvent)
// Set signer
setKeySigner(keys)
} }
suspend fun fetchMetadataBatch(keys: List<PublicKey>) { suspend fun fetchMetadataBatch(keys: List<PublicKey>) {

View File

@@ -158,7 +158,7 @@ class NostrViewModel(
try { try {
val appKeys = getOrInitAppKeys() val appKeys = getOrInitAppKeys()
val bunker = NostrConnectUri.parse(secret) val bunker = NostrConnectUri.parse(secret)
val timeout = Duration.parse("50") // 50 seconds timeout val timeout = Duration.parse("50s") // 50 seconds timeout
val remote = NostrConnect(uri = bunker, appKeys = appKeys, timeout = timeout, null) val remote = NostrConnect(uri = bunker, appKeys = appKeys, timeout = timeout, null)
nostr.setRemoteSigner(remote) nostr.setRemoteSigner(remote)
} catch (e: Exception) { } catch (e: Exception) {
@@ -189,12 +189,18 @@ class NostrViewModel(
try { try {
val keys = Keys.generate() val keys = Keys.generate()
val secret = keys.secretKey().toBech32() val secret = keys.secretKey().toBech32()
// Set loading state // Set loading state
_isCreating.value = true _isCreating.value = true
// Create identity // Create identity
nostr.createIdentity(keys, name, bio, picture) nostr.createIdentity(keys, name, bio, picture)
// Save secret to the secret storage // Save secret to the secret storage
secretStore.set("user_signer", secret) secretStore.set("user_signer", secret)
// Set an empty secret state
_emptySecret.value = false
} catch (e: Exception) { } catch (e: Exception) {
showError("Error: ${e.message}") showError("Error: ${e.message}")
} }
@@ -207,15 +213,19 @@ class NostrViewModel(
val keys = Keys.parse(secret) val keys = Keys.parse(secret)
nostr.setKeySigner(keys) nostr.setKeySigner(keys)
secretStore.set("user_signer", secret) secretStore.set("user_signer", secret)
// Set an empty secret state
_emptySecret.value = false
} else if (secret.startsWith("bunker://")) { } else if (secret.startsWith("bunker://")) {
try { try {
val appKeys = getOrInitAppKeys() val appKeys = getOrInitAppKeys()
val bunker = NostrConnectUri.parse(secret) val bunker = NostrConnectUri.parse(secret)
val timeout = Duration.parse("50") // 50 seconds timeout val timeout = Duration.parse("50s") // 50 seconds timeout
val remote = val remote =
NostrConnect(uri = bunker, appKeys = appKeys, timeout = timeout, null) NostrConnect(uri = bunker, appKeys = appKeys, timeout = timeout, null)
nostr.setRemoteSigner(remote) nostr.setRemoteSigner(remote)
secretStore.set("user_signer", secret) secretStore.set("user_signer", secret)
// Set an empty secret state
_emptySecret.value = false
} catch (e: Exception) { } catch (e: Exception) {
showError("Error: ${e.message}") showError("Error: ${e.message}")
} }