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(
isLoading = isCreating,
onBack = { navController.popBackStack() },
onSave = { name, bio, uri ->
viewModel.createIdentity(name, bio, uri?.toString())
}

View File

@@ -3,6 +3,7 @@ package su.reya.coop.screens
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LoadingIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
@@ -24,6 +29,8 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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.unit.dp
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
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun NewIdentityScreen(
isLoading: Boolean,
onBack: () -> Unit,
onSave: (name: String, bio: String, picture: Uri?) -> Unit
) {
val snackbarHostState = LocalSnackbarHostState.current
@@ -56,17 +68,34 @@ fun NewIdentityScreen(
Scaffold(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
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 ->
Surface(
modifier = Modifier
.fillMaxSize()
.padding(top = innerPadding.calculateTopPadding()),
color = MaterialTheme.colorScheme.surfaceContainer,
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp),
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.padding(24.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
@@ -74,7 +103,8 @@ fun NewIdentityScreen(
Box(
modifier = Modifier
.size(120.dp)
.clip(CircleShape),
.clip(CircleShape)
.clickable { launcher.launch("image/*") },
contentAlignment = Alignment.Center
) {
if (picture != null) {
@@ -90,7 +120,14 @@ fun NewIdentityScreen(
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 = {
onSave(name, bio, picture)
},
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.height(ButtonDefaults.LargeContainerHeight),
enabled = name.isNotBlank() && !isLoading,
) {
if (isLoading) {
LoadingIndicator()
} 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.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
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.Scaffold
import androidx.compose.material3.SnackbarHost
@@ -34,7 +29,6 @@ import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.dp
import coop.composeapp.generated.resources.Res
import coop.composeapp.generated.resources.coop
import coop.composeapp.generated.resources.ic_scanner
import org.jetbrains.compose.resources.painterResource
import su.reya.coop.LocalSnackbarHostState
@@ -92,36 +86,20 @@ fun OnboardingScreen(onOpenImport: () -> Unit, onOpenNew: () -> Unit) {
)
}
Spacer(modifier = Modifier.size(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
FilledTonalButton(
onClick = onOpenImport,
modifier = Modifier
.fillMaxWidth()
.height(ButtonDefaults.LargeContainerHeight),
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
contentColor = MaterialTheme.colorScheme.onTertiaryContainer
),
) {
FilledTonalButton(
onClick = onOpenImport,
modifier = Modifier
.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"
)
}
Text(
text = "Import identity",
style = MaterialTheme.typography.titleLargeEmphasized,
)
}
}
}

View File

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

View File

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