fix navigation after login

This commit is contained in:
2026-06-01 20:25:32 +07:00
parent 0da1371345
commit b1d9135394
4 changed files with 108 additions and 100 deletions

View File

@@ -189,25 +189,10 @@ fun App(viewModel: NostrViewModel) {
OnboardingScreen() OnboardingScreen()
} }
entry<Screen.Import> { entry<Screen.Import> {
ImportScreen( ImportScreen()
onSave = { secret ->
viewModel.importIdentity(secret)
}
)
} }
entry<Screen.NewIdentity> { entry<Screen.NewIdentity> {
NewIdentityScreen( NewIdentityScreen()
onSave = { name, bio, uri ->
val contentType =
uri?.let { context.contentResolver.getType(it) }
val picture = uri?.let {
context.contentResolver.openInputStream(it)?.use { input ->
input.readBytes()
}
}
viewModel.createIdentity(name, bio, picture, contentType)
}
)
} }
entry<Screen.Chat> { key -> entry<Screen.Chat> { key ->
ChatScreen(id = key.id) ChatScreen(id = key.id)

View File

@@ -33,7 +33,6 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.toShape import androidx.compose.material3.toShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -49,10 +48,11 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coop.composeapp.generated.resources.Res import coop.composeapp.generated.resources.Res
import coop.composeapp.generated.resources.ic_arrow_back import coop.composeapp.generated.resources.ic_arrow_back
import coop.composeapp.generated.resources.ic_scanner import coop.composeapp.generated.resources.ic_scanner
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
import rust.nostr.sdk.Keys import rust.nostr.sdk.Keys
@@ -69,32 +69,28 @@ import su.reya.coop.short
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun ImportScreen( fun ImportScreen() {
onSave: (secret: String) -> Unit
) {
val snackbarHostState = LocalSnackbarHostState.current val snackbarHostState = LocalSnackbarHostState.current
val navigator = LocalNavigator.current val navigator = LocalNavigator.current
val qrScanResult = LocalScanResult.current val qrScanResult = LocalScanResult.current
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val viewModel = LocalNostrViewModel.current val viewModel = LocalNostrViewModel.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val isLoggedIn by viewModel.isLoggedIn.collectAsStateWithLifecycle(false)
var secret by remember { mutableStateOf("") } var secret by remember { mutableStateOf("") }
var pubkey by remember { mutableStateOf<PublicKey?>(null) } var pubkey by remember { mutableStateOf<PublicKey?>(null) }
// Get metadata when pubkey changes
val metadata by remember(pubkey) { val metadata by remember(pubkey) {
if (pubkey != null) { pubkey?.let(viewModel::getMetadata) ?: flowOf(null)
viewModel.getMetadata(pubkey!!) }.collectAsStateWithLifecycle(null)
} else {
MutableStateFlow(null)
}
}.collectAsState(null)
val profile = metadata?.asRecord() val profile = metadata?.asRecord()
val displayName = profile?.displayName ?: profile?.name ?: pubkey?.short() ?: "Unknown" val displayName = profile?.displayName ?: profile?.name ?: pubkey?.short() ?: "Unknown"
val picture = profile?.picture val picture = profile?.picture
val isLoading by viewModel.isCreating.collectAsState()
LaunchedEffect(qrScanResult.content) { LaunchedEffect(qrScanResult.content) {
qrScanResult.content?.let { result -> qrScanResult.content?.let { result ->
runCatching { runCatching {
@@ -246,20 +242,24 @@ fun ImportScreen(
Spacer(modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.size(16.dp))
Button( Button(
onClick = { onClick = {
if (pubkey == null) {
scope.launch { scope.launch {
if (pubkey == null) {
viewModel.verifyIdentity(secret).let { pubkey = it } viewModel.verifyIdentity(secret).let { pubkey = it }
}
} else { } else {
onSave(secret) // Import the identity
viewModel.importIdentity(secret)
// Navigate to the home screen
navigator.navigate(Screen.Home)
} }
}
}, },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(ButtonDefaults.MediumContainerHeight), .height(ButtonDefaults.MediumContainerHeight),
enabled = secret.isNotBlank() && !isLoading, enabled = secret.isNotBlank() && !isLoggedIn,
) { ) {
if (isLoading) { if (isLoggedIn) {
LoadingIndicator() LoadingIndicator()
} else { } else {
Text( Text(

View File

@@ -36,45 +36,50 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.toShape import androidx.compose.material3.toShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import coop.composeapp.generated.resources.Res import coop.composeapp.generated.resources.Res
import coop.composeapp.generated.resources.ic_arrow_back import coop.composeapp.generated.resources.ic_arrow_back
import coop.composeapp.generated.resources.ic_plus import coop.composeapp.generated.resources.ic_plus
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
import su.reya.coop.LocalNavigator import su.reya.coop.LocalNavigator
import su.reya.coop.LocalNostrViewModel import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalSnackbarHostState import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.Screen
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun NewIdentityScreen( fun NewIdentityScreen() {
onSave: (name: String, bio: String?, picture: Uri?) -> Unit val context = LocalContext.current
) {
val snackbarHostState = LocalSnackbarHostState.current val snackbarHostState = LocalSnackbarHostState.current
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val navigator = LocalNavigator.current val navigator = LocalNavigator.current
val viewModel = LocalNostrViewModel.current val viewModel = LocalNostrViewModel.current
val isLoggedIn by viewModel.isLoggedIn.collectAsStateWithLifecycle(false)
var name by remember { mutableStateOf("") } var name by remember { mutableStateOf("") }
var bio by remember { mutableStateOf("") } var bio by remember { mutableStateOf("") }
var picture by remember { mutableStateOf<Uri?>(null) } var picture by remember { mutableStateOf<Uri?>(null) }
val isLoading by viewModel.isCreating.collectAsState() val scope = rememberCoroutineScope()
val launcher = rememberLauncherForActivityResult( val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent() contract = ActivityResultContracts.GetContent()
@@ -256,14 +261,35 @@ fun NewIdentityScreen(
Spacer(modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.size(16.dp))
Button( Button(
onClick = { onClick = {
onSave(name, bio, picture) scope.launch {
try {
val imageBytes = withContext(Dispatchers.IO) {
picture?.let { uri ->
context.contentResolver.openInputStream(
uri
)?.use { input -> input.readBytes() }
}
}
val contentType =
picture?.let { context.contentResolver.getType(it) }
// Create the identity
viewModel.createIdentity(name, bio, imageBytes, contentType)
// Navigate to the home screen if successful
navigator.navigate(Screen.Home)
} catch (e: Exception) {
// Error is handled by viewModel.showError inside createIdentity
}
}
}, },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(ButtonDefaults.MediumContainerHeight), .height(ButtonDefaults.MediumContainerHeight),
enabled = name.isNotBlank() && !isLoading, enabled = name.isNotBlank() && !isLoggedIn,
) { ) {
if (isLoading) { if (isLoggedIn) {
LoadingIndicator() LoadingIndicator()
} else { } else {
Text( Text(

View File

@@ -43,8 +43,8 @@ class NostrViewModel(
private val _signerRequired = MutableStateFlow<Boolean?>(null) private val _signerRequired = MutableStateFlow<Boolean?>(null)
val signerRequired = _signerRequired.asStateFlow() val signerRequired = _signerRequired.asStateFlow()
private val _isCreating = MutableStateFlow(false) private val _isLoggedIn = MutableStateFlow(false)
val isCreating = _isCreating.asStateFlow() val isLoggedIn = _isLoggedIn.asStateFlow()
private val _chatRooms = MutableStateFlow<Set<Room>>(emptySet()) private val _chatRooms = MutableStateFlow<Set<Room>>(emptySet())
val chatRooms = _chatRooms.asStateFlow() val chatRooms = _chatRooms.asStateFlow()
@@ -61,7 +61,7 @@ class NostrViewModel(
private val _newEvents = MutableSharedFlow<UnsignedEvent>(extraBufferCapacity = 100) private val _newEvents = MutableSharedFlow<UnsignedEvent>(extraBufferCapacity = 100)
val newEvents = _newEvents.asSharedFlow() val newEvents = _newEvents.asSharedFlow()
private val _sentReports = MutableStateFlow<Map<EventId, List<RelayUrl>>>(emptyMap()) private val _sentReports = MutableSharedFlow<Map<EventId, List<RelayUrl>>>()
val sentReport = _sentReports.asSharedFlow() val sentReport = _sentReports.asSharedFlow()
private val _errorEvents = Channel<String>(Channel.BUFFERED) private val _errorEvents = Channel<String>(Channel.BUFFERED)
@@ -101,7 +101,6 @@ class NostrViewModel(
private fun showError(message: String) { private fun showError(message: String) {
viewModelScope.launch { viewModelScope.launch {
_errorEvents.send(message) _errorEvents.send(message)
if (isCreating.value) _isCreating.value = false
} }
} }
@@ -324,21 +323,18 @@ class NostrViewModel(
} }
} }
fun createIdentity( suspend fun createIdentity(
name: String, name: String,
bio: String?, bio: String?,
picture: ByteArray?, picture: ByteArray?,
contentType: String? = null contentType: String? = null
) { ) {
viewModelScope.launch { _isLoggedIn.value = true
try { try {
val keys = Keys.generate() val keys = Keys.generate()
val secret = keys.secretKey().toBech32() val secret = keys.secretKey().toBech32()
var avatarUrl = "" var avatarUrl = ""
// Set loading state
_isCreating.value = true
// Upload picture to Blossom // Upload picture to Blossom
if (picture != null) { if (picture != null) {
val blossom = BlossomClient( val blossom = BlossomClient(
@@ -373,7 +369,8 @@ class NostrViewModel(
_signerRequired.value = false _signerRequired.value = false
} catch (e: Exception) { } catch (e: Exception) {
showError("Error: ${e.message}") showError("Error: ${e.message}")
} } finally {
_isLoggedIn.value = true
} }
} }
@@ -387,17 +384,17 @@ class NostrViewModel(
}.getOrNull() }.getOrNull()
} }
fun importIdentity(secret: String) { suspend fun importIdentity(secret: String) {
viewModelScope.launch { _isLoggedIn.value = true
runCatching { try {
val signer = createSigner(secret) val signer = createSigner(secret)
nostr.setSigner(signer) nostr.setSigner(signer)
secretStore.set("user_signer", secret) secretStore.set("user_signer", secret)
}.onSuccess { } catch (e: Exception) {
showError("Error: ${e.message}")
} finally {
_signerRequired.value = false _signerRequired.value = false
}.onFailure { e -> _isLoggedIn.value = true
showError(e.message ?: "Invalid Secret or Bunker URI")
}
} }
} }