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()
}
entry<Screen.Import> {
ImportScreen(
onSave = { secret ->
viewModel.importIdentity(secret)
}
)
ImportScreen()
}
entry<Screen.NewIdentity> {
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)
}
)
NewIdentityScreen()
}
entry<Screen.Chat> { key ->
ChatScreen(id = key.id)

View File

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

View File

@@ -36,45 +36,50 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.toShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage
import coop.composeapp.generated.resources.Res
import coop.composeapp.generated.resources.ic_arrow_back
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 su.reya.coop.LocalNavigator
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.Screen
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun NewIdentityScreen(
onSave: (name: String, bio: String?, picture: Uri?) -> Unit
) {
fun NewIdentityScreen() {
val context = LocalContext.current
val snackbarHostState = LocalSnackbarHostState.current
val focusManager = LocalFocusManager.current
val navigator = LocalNavigator.current
val viewModel = LocalNostrViewModel.current
val isLoggedIn by viewModel.isLoggedIn.collectAsStateWithLifecycle(false)
var name by remember { mutableStateOf("") }
var bio by remember { mutableStateOf("") }
var picture by remember { mutableStateOf<Uri?>(null) }
val isLoading by viewModel.isCreating.collectAsState()
val scope = rememberCoroutineScope()
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
@@ -256,14 +261,35 @@ fun NewIdentityScreen(
Spacer(modifier = Modifier.size(16.dp))
Button(
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
.fillMaxWidth()
.height(ButtonDefaults.MediumContainerHeight),
enabled = name.isNotBlank() && !isLoading,
enabled = name.isNotBlank() && !isLoggedIn,
) {
if (isLoading) {
if (isLoggedIn) {
LoadingIndicator()
} else {
Text(

View File

@@ -43,8 +43,8 @@ class NostrViewModel(
private val _signerRequired = MutableStateFlow<Boolean?>(null)
val signerRequired = _signerRequired.asStateFlow()
private val _isCreating = MutableStateFlow(false)
val isCreating = _isCreating.asStateFlow()
private val _isLoggedIn = MutableStateFlow(false)
val isLoggedIn = _isLoggedIn.asStateFlow()
private val _chatRooms = MutableStateFlow<Set<Room>>(emptySet())
val chatRooms = _chatRooms.asStateFlow()
@@ -61,7 +61,7 @@ class NostrViewModel(
private val _newEvents = MutableSharedFlow<UnsignedEvent>(extraBufferCapacity = 100)
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()
private val _errorEvents = Channel<String>(Channel.BUFFERED)
@@ -101,7 +101,6 @@ class NostrViewModel(
private fun showError(message: String) {
viewModelScope.launch {
_errorEvents.send(message)
if (isCreating.value) _isCreating.value = false
}
}
@@ -324,56 +323,54 @@ class NostrViewModel(
}
}
fun createIdentity(
suspend fun createIdentity(
name: String,
bio: String?,
picture: ByteArray?,
contentType: String? = null
) {
viewModelScope.launch {
try {
val keys = Keys.generate()
val secret = keys.secretKey().toBech32()
var avatarUrl = ""
_isLoggedIn.value = true
try {
val keys = Keys.generate()
val secret = keys.secretKey().toBech32()
var avatarUrl = ""
// Set loading state
_isCreating.value = true
// Upload picture to Blossom
if (picture != null) {
val blossom = BlossomClient(
url = "https://blossom.band",
client = HttpClient {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
prettyPrint = true
isLenient = true
})
}
// Upload picture to Blossom
if (picture != null) {
val blossom = BlossomClient(
url = "https://blossom.band",
client = HttpClient {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
prettyPrint = true
isLenient = true
})
}
)
}
)
val descriptor = blossom.upload(
file = picture,
contentType = contentType,
signer = keys
)
val descriptor = blossom.upload(
file = picture,
contentType = contentType,
signer = keys
)
avatarUrl = descriptor?.url ?: ""
}
// Create identity
nostr.createIdentity(keys = keys, name = name, bio, picture = avatarUrl)
// Save secret to the secret storage
secretStore.set("user_signer", secret)
// Set an empty secret state
_signerRequired.value = false
} catch (e: Exception) {
showError("Error: ${e.message}")
avatarUrl = descriptor?.url ?: ""
}
// Create identity
nostr.createIdentity(keys = keys, name = name, bio, picture = avatarUrl)
// Save secret to the secret storage
secretStore.set("user_signer", secret)
// Set an empty secret state
_signerRequired.value = false
} catch (e: Exception) {
showError("Error: ${e.message}")
} finally {
_isLoggedIn.value = true
}
}
@@ -387,17 +384,17 @@ class NostrViewModel(
}.getOrNull()
}
fun importIdentity(secret: String) {
viewModelScope.launch {
runCatching {
val signer = createSigner(secret)
nostr.setSigner(signer)
secretStore.set("user_signer", secret)
}.onSuccess {
_signerRequired.value = false
}.onFailure { e ->
showError(e.message ?: "Invalid Secret or Bunker URI")
}
suspend fun importIdentity(secret: String) {
_isLoggedIn.value = true
try {
val signer = createSigner(secret)
nostr.setSigner(signer)
secretStore.set("user_signer", secret)
} catch (e: Exception) {
showError("Error: ${e.message}")
} finally {
_signerRequired.value = false
_isLoggedIn.value = true
}
}