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( private suspend fun upload(
galleryUri: SelectedMedia, galleryUri: SelectedMedia,
context: Context, context: Context,
@@ -21,6 +21,8 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
import android.content.ClipData 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.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddPhotoAlternate
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
@@ -38,6 +42,7 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -45,13 +50,20 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard 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.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.R
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.model.User 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.components.ZoomableImageDialog
import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
@@ -126,7 +138,7 @@ fun ProfileHeader(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Bottom, verticalAlignment = Alignment.Bottom,
) { ) {
ZoomableUserPicture( ProfilePictureWithUploadOverlay(
baseUser = baseUser, baseUser = baseUser,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
size = Size100dp, 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,
)
}
}
}