This commit is contained in:
2026-06-06 12:50:15 +07:00
parent 6c923a1b68
commit eb10e715d9
4 changed files with 47 additions and 16 deletions

View File

@@ -20,6 +20,7 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@@ -27,9 +28,11 @@ import androidx.compose.material3.LoadingIndicator
import androidx.compose.material3.MaterialShapes import androidx.compose.material3.MaterialShapes
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
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.getValue import androidx.compose.runtime.getValue
@@ -55,6 +58,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
import su.reya.coop.LocalSnackbarHostState
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
@@ -69,16 +73,27 @@ fun ProfileEditor(
onConfirm: (name: String, bio: String, pictureBytes: ByteArray?, contentType: String?) -> Unit onConfirm: (name: String, bio: String, pictureBytes: ByteArray?, contentType: String?) -> Unit
) { ) {
val context = LocalContext.current val context = LocalContext.current
val snackbarHostState = LocalSnackbarHostState.current
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
var name by remember(initialName) { mutableStateOf(initialName) } var name by remember(initialName) { mutableStateOf(initialName) }
var bio by remember(initialBio) { mutableStateOf(initialBio) } var bio by remember(initialBio) { mutableStateOf(initialBio) }
var picture by remember(initialPicture) { mutableStateOf(initialPicture) } var picture by remember(initialPicture) { mutableStateOf(initialPicture) }
val hasPicture = remember(picture) {
when (picture) {
null -> false
is String -> (picture as CharSequence).isNotBlank()
else -> true
}
}
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
picture = uri picture = uri
} }
Scaffold( Scaffold(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { Text(title, style = MaterialTheme.typography.titleMediumEmphasized) }, title = { Text(title, style = MaterialTheme.typography.titleMediumEmphasized) },
@@ -89,7 +104,10 @@ fun ProfileEditor(
contentDescription = "Back" contentDescription = "Back"
) )
} }
} },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
)
) )
} }
) { innerPadding -> ) { innerPadding ->
@@ -112,7 +130,7 @@ fun ProfileEditor(
.clickable { launcher.launch("image/*") }, .clickable { launcher.launch("image/*") },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
if (picture != null) { if (hasPicture) {
AsyncImage( AsyncImage(
model = picture, model = picture,
contentDescription = "Profile picture", contentDescription = "Profile picture",
@@ -121,16 +139,15 @@ fun ProfileEditor(
) )
} else { } else {
Surface( Surface(
color = MaterialTheme.colorScheme.surfaceVariant, color = MaterialTheme.colorScheme.tertiaryContainer,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
Icon( Icon(
painter = painterResource(Res.drawable.ic_plus), painter = painterResource(Res.drawable.ic_plus),
contentDescription = "Pick avatar", contentDescription = "Pick avatar",
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant tint = MaterialTheme.colorScheme.onTertiaryFixed
) )
} }
} }
@@ -243,6 +260,9 @@ fun ProfileEditor(
} }
Spacer(modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.size(16.dp))
Button( Button(
modifier = Modifier
.fillMaxWidth()
.size(ButtonDefaults.MediumContainerHeight),
onClick = { onClick = {
val scope = CoroutineScope(Dispatchers.Main) val scope = CoroutineScope(Dispatchers.Main)
scope.launch { scope.launch {
@@ -258,7 +278,14 @@ fun ProfileEditor(
}, },
enabled = name.isNotBlank() && !isBusy enabled = name.isNotBlank() && !isBusy
) { ) {
if (isBusy) LoadingIndicator() else Text(buttonLabel) if (isBusy) {
LoadingIndicator()
} else {
Text(
text = buttonLabel,
style = MaterialTheme.typography.titleMediumEmphasized,
)
}
} }
} }
} }

View File

@@ -502,7 +502,7 @@ class Nostr {
name: String? = null, name: String? = null,
bio: String? = null, bio: String? = null,
picture: String? = null picture: String? = null
) { ): Metadata {
val currentUser = signer.currentUser ?: throw IllegalStateException("User not signed in") val currentUser = signer.currentUser ?: throw IllegalStateException("User not signed in")
try { try {
@@ -512,13 +512,16 @@ class Nostr {
about = bio ?: record.about, about = bio ?: record.about,
picture = picture ?: record.picture picture = picture ?: record.picture
) )
val event = EventBuilder.metadata(Metadata.fromRecord(newRecord)).signAsync(signer) val newMetadata = Metadata.fromRecord(newRecord)
val event = EventBuilder.metadata(newMetadata).signAsync(signer)
client?.sendEvent( client?.sendEvent(
event = event, event = event,
target = SendEventTarget.broadcast(), target = SendEventTarget.broadcast(),
ackPolicy = AckPolicy.none() ackPolicy = AckPolicy.none()
) )
return newMetadata
} catch (e: Exception) { } catch (e: Exception) {
throw IllegalStateException("Failed to update identity: ${e.message}", e) throw IllegalStateException("Failed to update identity: ${e.message}", e)
} }

View File

@@ -346,8 +346,6 @@ class NostrViewModel(
private suspend fun blossomUpload(file: ByteArray, contentType: String): String? { private suspend fun blossomUpload(file: ByteArray, contentType: String): String? {
try { try {
var avatarUrl: String? = null
// Upload picture to Blossom // Upload picture to Blossom
val blossom = BlossomClient( val blossom = BlossomClient(
url = "https://blossom.band", url = "https://blossom.band",
@@ -365,12 +363,10 @@ class NostrViewModel(
val descriptor = blossom.upload( val descriptor = blossom.upload(
file = file, file = file,
contentType = contentType, contentType = contentType,
signer = nostr.signer signer = nostr.signer.get()
) )
avatarUrl = descriptor?.url return descriptor?.url
return avatarUrl
} catch (e: Exception) { } catch (e: Exception) {
showError("Error: ${e.message}") showError("Error: ${e.message}")
return null return null
@@ -383,11 +379,16 @@ class NostrViewModel(
picture: ByteArray? = null, picture: ByteArray? = null,
contentType: String? = null contentType: String? = null
) { ) {
_isLoggedIn.value = true
try { try {
val avatarUrl = picture?.let { blossomUpload(it, contentType ?: "image/jpeg") } val avatarUrl = picture?.let { blossomUpload(it, contentType ?: "image/jpeg") }
nostr.updateProfile(name, bio, avatarUrl) val newMetadata = nostr.updateProfile(name, bio, avatarUrl)
// Update the metadata state after successfully published
updateMetadata(nostr.signer.currentUser!!, newMetadata)
} catch (e: Exception) { } catch (e: Exception) {
showError("Error: ${e.message}") showError("Error: ${e.message}")
} finally {
_isLoggedIn.value = false
} }
} }
@@ -414,7 +415,7 @@ class NostrViewModel(
} catch (e: Exception) { } catch (e: Exception) {
showError("Error: ${e.message}") showError("Error: ${e.message}")
} finally { } finally {
_isLoggedIn.value = true _isLoggedIn.value = false
} }
} }