Merge pull request #1720 from vitorpamplona/claude/add-profile-upload-button-RzGz4

feat: add profile picture upload button when user has no picture
This commit is contained in:
Vitor Pamplona
2026-02-28 10:53:48 -05:00
committed by GitHub
2 changed files with 106 additions and 1 deletions
@@ -169,6 +169,26 @@ class NewUserMetadataViewModel : ViewModel() {
}
}
fun uploadPictureAndSave(
uri: SelectedMedia,
context: Context,
onError: (String, String) -> Unit,
) {
load()
accountViewModel.launchSigner {
upload(
uri,
context,
onUploading = { isUploadingImageForPicture = it },
onUploaded = {
picture.value = it
create()
},
onError = onError,
)
}
}
private suspend fun upload(
galleryUri: SelectedMedia,
context: Context,
@@ -21,6 +21,8 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
import android.content.ClipData
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -30,7 +32,9 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddPhotoAlternate
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
@@ -38,6 +42,7 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -45,13 +50,20 @@ 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.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture
import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataViewModel
import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelectSingle
import com.vitorpamplona.amethyst.ui.components.LoadingAnimation
import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
@@ -126,7 +138,7 @@ fun ProfileHeader(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Bottom,
) {
ZoomableUserPicture(
ProfilePictureWithUploadOverlay(
baseUser = baseUser,
accountViewModel = accountViewModel,
size = Size100dp,
@@ -189,3 +201,76 @@ fun ZoomableUserPicture(
)
}
}
@Composable
fun ProfilePictureWithUploadOverlay(
baseUser: User,
accountViewModel: AccountViewModel,
size: Dp,
) {
val isMe by remember(accountViewModel) { derivedStateOf { accountViewModel.userProfile() == baseUser } }
val picture by observeUserPicture(baseUser, accountViewModel)
Box(contentAlignment = Alignment.Center) {
ZoomableUserPicture(
baseUser = baseUser,
accountViewModel = accountViewModel,
size = size,
)
if (isMe && picture.isNullOrBlank()) {
ProfilePictureUploadButton(accountViewModel, size)
}
}
}
@Composable
private fun ProfilePictureUploadButton(
accountViewModel: AccountViewModel,
size: Dp,
) {
val postViewModel: NewUserMetadataViewModel = viewModel()
postViewModel.init(accountViewModel)
val context = LocalContext.current
var showGallerySelect by remember { mutableStateOf(false) }
if (showGallerySelect) {
GallerySelectSingle(
onImageUri = { media ->
showGallerySelect = false
if (media != null) {
postViewModel.uploadPictureAndSave(media, context, accountViewModel.toastManager::toast)
}
},
)
}
if (postViewModel.isUploadingImageForPicture) {
Box(
modifier =
Modifier
.size(size)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.7f)),
contentAlignment = Alignment.Center,
) {
LoadingAnimation()
}
} else {
Box(
modifier =
Modifier
.size(size)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.7f))
.clickable { showGallerySelect = true },
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Default.AddPhotoAlternate,
contentDescription = stringRes(R.string.upload_image),
modifier = Modifier.size(40.dp),
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
}