From 5c31f7a0d633b7b69e8642e3ced2e5254d60dda9 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Fri, 8 May 2026 12:17:30 +0700 Subject: [PATCH] fix create identity flow --- .../drawable/ic_arrow_back.xml | 10 ++++ .../androidMain/kotlin/su/reya/coop/App.kt | 1 + .../su/reya/coop/screens/NewIdentityScreen.kt | 54 ++++++++++++++++--- .../su/reya/coop/screens/OnboardingScreen.kt | 48 +++++------------ .../commonMain/kotlin/su/reya/coop/Nostr.kt | 18 ++++--- .../kotlin/su/reya/coop/NostrViewModel.kt | 14 ++++- 6 files changed, 95 insertions(+), 50 deletions(-) create mode 100644 composeApp/src/androidMain/composeResources/drawable/ic_arrow_back.xml diff --git a/composeApp/src/androidMain/composeResources/drawable/ic_arrow_back.xml b/composeApp/src/androidMain/composeResources/drawable/ic_arrow_back.xml new file mode 100644 index 0000000..fd02352 --- /dev/null +++ b/composeApp/src/androidMain/composeResources/drawable/ic_arrow_back.xml @@ -0,0 +1,10 @@ + + + diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/App.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/App.kt index e0af954..ef39dc8 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/App.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/App.kt @@ -121,6 +121,7 @@ fun App(dbPath: String) { NewIdentityScreen( isLoading = isCreating, + onBack = { navController.popBackStack() }, onSave = { name, bio, uri -> viewModel.createIdentity(name, bio, uri?.toString()) } diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/NewIdentityScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/NewIdentityScreen.kt index a7ee9a4..2566071 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/NewIdentityScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/NewIdentityScreen.kt @@ -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, + ) } } } diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/OnboardingScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/OnboardingScreen.kt index bf25683..9f98964 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/OnboardingScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/OnboardingScreen.kt @@ -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, + ) } } } diff --git a/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt b/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt index 2b64f3a..bda2061 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt @@ -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) { diff --git a/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt b/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt index c6cca12..82e55f5 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt @@ -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}") }