From 26a3dcbd85354e368e6488e66ed6c900b8c38f5e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 22 Mar 2026 12:35:19 -0400 Subject: [PATCH] Adds upload cover url --- .../nip23LongForm/LongFormPostScreen.kt | 18 +++++ .../nip23LongForm/LongFormPostViewModel.kt | 76 +++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt index aaaa385b2..a56dd41af 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt @@ -65,6 +65,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery +import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown import com.vitorpamplona.amethyst.ui.navigation.navs.Nav @@ -226,6 +227,8 @@ private fun MarkdownPostScreenBody( ), ) + val context = LocalContext.current + // Cover image URL field OutlinedTextField( value = postViewModel.coverImageUrl, @@ -239,6 +242,21 @@ private fun MarkdownPostScreenBody( .fillMaxWidth() .padding(horizontal = Size10dp, vertical = Size5dp), singleLine = true, + placeholder = { + Text( + text = "https://mywebsite.com/mypost.jpg", + color = MaterialTheme.colorScheme.placeholderText, + ) + }, + leadingIcon = { + SelectSingleFromGallery( + isUploading = postViewModel.isUploadingCoverImage, + tint = MaterialTheme.colorScheme.placeholderText, + modifier = Modifier.padding(start = 5.dp), + ) { + postViewModel.uploadCoverImage(it, context, onError = accountViewModel.toastManager::toast) + } + }, ) HorizontalDivider(modifier = Modifier.padding(vertical = Size5dp)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt index 803e5e7f3..a9da009eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt @@ -40,11 +40,15 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.location.LocationState +import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader +import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName +import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState @@ -105,6 +109,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import kotlin.coroutines.cancellation.CancellationException import kotlin.uuid.ExperimentalUuidApi @Stable @@ -138,6 +143,8 @@ class LongFormPostViewModel : var coverImageUrl by mutableStateOf("") var publishedAt by mutableLongStateOf(TimeUtils.now()) + var isUploadingCoverImage by mutableStateOf(false) + override var message by mutableStateOf(TextFieldValue("")) var showPreview by mutableStateOf(false) @@ -383,6 +390,75 @@ class LongFormPostViewModel : } } + fun uploadCoverImage( + uri: SelectedMedia, + context: Context, + onError: (String, String) -> Unit, + ) { + accountViewModel.launchSigner { + isUploadingCoverImage = true + try { + directUpload( + galleryUri = uri, + context = context, + onError = onError, + )?.let { + coverImageUrl = it + } + } finally { + isUploadingCoverImage = false + } + } + } + + private suspend fun directUpload( + galleryUri: SelectedMedia, + context: Context, + onError: (String, String) -> Unit, + ): String? { + val compResult = MediaCompressor().compress(galleryUri.uri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext) + + return try { + val result = + if (account.settings.defaultFileServer.type == ServerType.NIP96) { + Nip96Uploader().upload( + uri = compResult.uri, + contentType = compResult.contentType, + size = compResult.size, + alt = null, + sensitiveContent = null, + serverBaseUrl = account.settings.defaultFileServer.baseUrl, + okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, + onProgress = {}, + httpAuth = account::createHTTPAuthorization, + context = context, + ) + } else { + BlossomUploader().upload( + uri = compResult.uri, + contentType = compResult.contentType, + size = compResult.size, + alt = null, + sensitiveContent = null, + serverBaseUrl = account.settings.defaultFileServer.baseUrl, + okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, + httpAuth = account::createBlossomUploadAuth, + context = context, + ) + } + + if (result.url == null) { + onError(stringRes(context, R.string.failed_to_upload_media_no_details), stringRes(context, R.string.server_did_not_provide_a_url_after_uploading)) + } + + result.url + } catch (e: Exception) { + if (e is CancellationException) throw e + onError(stringRes(context, R.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName) + null + } + } + fun upload( alt: String?, contentWarningReason: String?,