diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/UpdateProfile.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/UpdateProfileScreen.kt similarity index 100% rename from composeApp/src/androidMain/kotlin/su/reya/coop/screens/UpdateProfile.kt rename to composeApp/src/androidMain/kotlin/su/reya/coop/screens/UpdateProfileScreen.kt diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/shared/ProfileEditor.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/shared/ProfileEditor.kt index cfb456d..2267b2c 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/shared/ProfileEditor.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/shared/ProfileEditor.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions 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 @@ -27,9 +28,11 @@ import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.MaterialShapes import androidx.compose.material3.MaterialTheme 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.material3.toShape import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -55,6 +58,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.compose.resources.painterResource +import su.reya.coop.LocalSnackbarHostState @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable @@ -69,16 +73,27 @@ fun ProfileEditor( onConfirm: (name: String, bio: String, pictureBytes: ByteArray?, contentType: String?) -> Unit ) { val context = LocalContext.current + val snackbarHostState = LocalSnackbarHostState.current val focusManager = LocalFocusManager.current var name by remember(initialName) { mutableStateOf(initialName) } var bio by remember(initialBio) { mutableStateOf(initialBio) } 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 -> picture = uri } Scaffold( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + snackbarHost = { SnackbarHost(snackbarHostState) }, topBar = { TopAppBar( title = { Text(title, style = MaterialTheme.typography.titleMediumEmphasized) }, @@ -89,7 +104,10 @@ fun ProfileEditor( contentDescription = "Back" ) } - } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ) ) } ) { innerPadding -> @@ -112,7 +130,7 @@ fun ProfileEditor( .clickable { launcher.launch("image/*") }, contentAlignment = Alignment.Center ) { - if (picture != null) { + if (hasPicture) { AsyncImage( model = picture, contentDescription = "Profile picture", @@ -121,16 +139,15 @@ fun ProfileEditor( ) } else { Surface( - color = MaterialTheme.colorScheme.surfaceVariant, + color = MaterialTheme.colorScheme.tertiaryContainer, modifier = Modifier.fillMaxSize() - ) { Box(contentAlignment = Alignment.Center) { Icon( painter = painterResource(Res.drawable.ic_plus), contentDescription = "Pick avatar", 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)) Button( + modifier = Modifier + .fillMaxWidth() + .size(ButtonDefaults.MediumContainerHeight), onClick = { val scope = CoroutineScope(Dispatchers.Main) scope.launch { @@ -258,7 +278,14 @@ fun ProfileEditor( }, enabled = name.isNotBlank() && !isBusy ) { - if (isBusy) LoadingIndicator() else Text(buttonLabel) + if (isBusy) { + LoadingIndicator() + } else { + Text( + text = buttonLabel, + style = MaterialTheme.typography.titleMediumEmphasized, + ) + } } } } diff --git a/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt b/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt index 9874fd2..ee5a364 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt @@ -502,7 +502,7 @@ class Nostr { name: String? = null, bio: String? = null, picture: String? = null - ) { + ): Metadata { val currentUser = signer.currentUser ?: throw IllegalStateException("User not signed in") try { @@ -512,13 +512,16 @@ class Nostr { about = bio ?: record.about, 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( event = event, target = SendEventTarget.broadcast(), ackPolicy = AckPolicy.none() ) + + return newMetadata } catch (e: Exception) { throw IllegalStateException("Failed to update identity: ${e.message}", e) } diff --git a/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt b/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt index 75f3c37..fcba31b 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt @@ -346,8 +346,6 @@ class NostrViewModel( private suspend fun blossomUpload(file: ByteArray, contentType: String): String? { try { - var avatarUrl: String? = null - // Upload picture to Blossom val blossom = BlossomClient( url = "https://blossom.band", @@ -365,12 +363,10 @@ class NostrViewModel( val descriptor = blossom.upload( file = file, contentType = contentType, - signer = nostr.signer + signer = nostr.signer.get() ) - avatarUrl = descriptor?.url - - return avatarUrl + return descriptor?.url } catch (e: Exception) { showError("Error: ${e.message}") return null @@ -383,11 +379,16 @@ class NostrViewModel( picture: ByteArray? = null, contentType: String? = null ) { + _isLoggedIn.value = true try { 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) { showError("Error: ${e.message}") + } finally { + _isLoggedIn.value = false } } @@ -414,7 +415,7 @@ class NostrViewModel( } catch (e: Exception) { showError("Error: ${e.message}") } finally { - _isLoggedIn.value = true + _isLoggedIn.value = false } }