feat: add profile picture upload button when user has no picture
When a user visits their own profile and has no profile picture set, show a semi-transparent upload button overlay directly on the profile picture area. Tapping it opens the gallery picker, uploads the image to the configured server (NIP-96 or Blossom), and immediately saves the metadata — without requiring the user to navigate to the edit screen. - Add uploadPictureAndSave() to NewUserMetadataViewModel: loads current metadata, uploads the image, then saves all metadata with the new picture - Add ProfilePictureWithUploadOverlay composable that wraps ZoomableUserPicture and conditionally shows the upload overlay when isMe && no picture - Add ProfilePictureUploadButton composable for the overlay UI with loading state support - Use ProfilePictureWithUploadOverlay in ProfileHeader instead of ZoomableUserPicture directly https://claude.ai/code/session_01XemJ8Hx9q4mMd1StZNY9CE
This commit is contained in:
+20
@@ -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,
|
||||||
|
|||||||
+86
-1
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user