feat: add update profile screen #14

Merged
reya merged 3 commits from feat/update-profile-screen into master 2026-06-06 05:50:33 +00:00
4 changed files with 47 additions and 16 deletions
Showing only changes of commit eb10e715d9 - Show all commits

View File

@@ -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,
)
}
}
}
}

View File

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

View File

@@ -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
}
}