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}")
}