diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt index 0dc92bd0ad..9098ab7834 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt @@ -46,7 +46,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.unit.dp @@ -91,7 +90,6 @@ fun NewUserMetadataScreen( ) { val postViewModel: NewUserMetadataViewModel = viewModel() postViewModel.init(accountViewModel) - val context = LocalContext.current val socialExpanded = rememberSaveable { mutableStateOf(false) } @@ -213,7 +211,7 @@ fun NewUserMetadataScreen( tint = MaterialTheme.colorScheme.placeholderText, modifier = Modifier.padding(start = 5.dp), ) { - postViewModel.uploadForPicture(it, context, onError = accountViewModel.toastManager::toast) + postViewModel.uploadForPicture(it, accountViewModel.host.mediaUploader, onError = accountViewModel.toastManager::toast) } }, singleLine = true, @@ -238,7 +236,7 @@ fun NewUserMetadataScreen( tint = MaterialTheme.colorScheme.placeholderText, modifier = Modifier.padding(start = 5.dp), ) { - postViewModel.uploadForBanner(it, context, onError = accountViewModel.toastManager::toast) + postViewModel.uploadForBanner(it, accountViewModel.host.mediaUploader, onError = accountViewModel.toastManager::toast) } }, singleLine = true, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt index 31c146955c..d1de32b34a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt @@ -20,33 +20,19 @@ */ package com.vitorpamplona.amethyst.ui.actions -import android.content.Context import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.commons.model.Account -import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType -import com.vitorpamplona.amethyst.commons.resources.Res -import com.vitorpamplona.amethyst.commons.resources.avif_metadata_strip_failed -import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details -import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_title -import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_upload_cancelled -import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading -import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia -import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.ui.uploads.uploadToDefaultServer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MetadataStripper -import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader -import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader import com.vitorpamplona.quartz.nip39ExtIdentities.GitHubIdentity import com.vitorpamplona.quartz.nip39ExtIdentities.MastodonIdentity import com.vitorpamplona.quartz.nip39ExtIdentities.TwitterIdentity import com.vitorpamplona.quartz.nip39ExtIdentities.identityClaims -import kotlin.coroutines.cancellation.CancellationException class NewUserMetadataViewModel : ViewModel() { private lateinit var accountViewModel: AccountViewModel @@ -164,13 +150,13 @@ class NewUserMetadataViewModel : ViewModel() { fun uploadForPicture( uri: SelectedMedia, - context: Context, + uploader: MediaUploader, onError: (String, String) -> Unit, ) { accountViewModel.launchSigner { upload( galleryUri = uri, - context = context, + uploader = uploader, onError = onError, )?.let { picture.value = it @@ -180,13 +166,13 @@ class NewUserMetadataViewModel : ViewModel() { fun uploadForBanner( uri: SelectedMedia, - context: Context, + uploader: MediaUploader, onError: (String, String) -> Unit, ) { accountViewModel.launchSigner { upload( galleryUri = uri, - context = context, + uploader = uploader, onError = onError, )?.let { banner.value = it @@ -196,14 +182,14 @@ class NewUserMetadataViewModel : ViewModel() { fun uploadPictureAndSave( uri: SelectedMedia, - context: Context, + uploader: MediaUploader, onError: (String, String) -> Unit, ) { load() accountViewModel.launchSigner { upload( galleryUri = uri, - context = context, + uploader = uploader, onError = onError, )?.let { picture.value = it @@ -215,81 +201,12 @@ class NewUserMetadataViewModel : ViewModel() { private suspend fun upload( galleryUri: SelectedMedia, - context: Context, + uploader: MediaUploader, onError: (String, String) -> Unit, ): String? { isUploadingImageForPicture = true - - val strippingResult = - if (account.settings.stripLocationOnUpload) { - try { - MetadataStripper.strip(galleryUri.uri, galleryUri.mimeType, context.applicationContext) - } catch (e: AvifMetadataNotVerifiableException) { - isUploadingImageForPicture = false - onError( - loadStringRes(Res.string.metadata_strip_failed_title), - loadStringRes(Res.string.avif_metadata_strip_failed, e.message ?: e.javaClass.simpleName), - ) - return null - } - } else { - null - } - - val sourceUri = - if (account.settings.stripLocationOnUpload && - strippingResult != null && - !strippingResult.stripped - ) { - onError( - loadStringRes(Res.string.metadata_strip_failed_title), - loadStringRes(Res.string.metadata_strip_failed_upload_cancelled), - ) - return null - } else { - strippingResult?.uri ?: galleryUri.uri - } - - val compResult = MediaCompressor().compress(sourceUri, 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 = accountViewModel.httpClientBuilder::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 = accountViewModel.httpClientBuilder::okHttpClientForUploads, - httpAuth = account::createBlossomUploadAuth, - context = context, - ) - } - - if (result.url == null) { - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.server_did_not_provide_a_url_after_uploading)) - } - - result.url - } catch (e: Exception) { - if (e is CancellationException) throw e - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName) - null + uploadToDefaultServer(galleryUri, account, uploader, onError) } finally { isUploadingImageForPicture = false } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt index b19f9b0bfb..e22a0582ec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt @@ -60,7 +60,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -211,7 +210,6 @@ private fun RelayGroupMetadataScaffold( // existing group already implies a working relay, so callers pass true. nip29Support: Boolean? = true, ) { - val context = LocalContext.current val scrollState = rememberScrollState() // NIP-29 §Subgroups relay support detection: `"nip29": { "subgroups": true }` in the NIP-11. @@ -236,7 +234,7 @@ private fun RelayGroupMetadataScaffold( val onSubmit: () -> Unit = { viewModel.submit( - context = context, + uploader = accountViewModel.host.mediaUploader, onSuccess = onSuccess, onError = accountViewModel.toastManager::toast, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataViewModel.kt index ab1c218a90..a5ded4051f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataViewModel.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup -import android.content.Context import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -29,29 +28,17 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect -import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.amethyst.commons.resources.Res -import com.vitorpamplona.amethyst.commons.resources.avif_metadata_strip_failed -import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_sign_events -import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_upload -import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_title -import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_upload_cancelled import com.vitorpamplona.amethyst.commons.resources.read_only_user -import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading -import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.ui.uploads.uploadToDefaultServer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MetadataStripper -import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader -import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader import com.vitorpamplona.quartz.buzz.workspace.BUZZ_CHANNEL_TYPE_FORUM import com.vitorpamplona.quartz.buzz.workspace.BUZZ_CHANNEL_TYPE_STREAM import com.vitorpamplona.quartz.buzz.workspace.newBuzzChannelId @@ -61,7 +48,6 @@ import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.utils.RandomInstance import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlin.coroutines.cancellation.CancellationException /** * Backs the create/edit NIP-29 group metadata screens. Holds the full editable metadata @@ -221,7 +207,7 @@ class RelayGroupMetadataViewModel : ViewModel() { } fun submit( - context: Context, + uploader: MediaUploader, onSuccess: () -> Unit, onError: (String, String) -> Unit, ) { @@ -231,7 +217,7 @@ class RelayGroupMetadataViewModel : ViewModel() { try { val local = pickedMedia if (local != null) { - val uploadedUrl = uploadImage(local, context, onError) ?: return@launch + val uploadedUrl = uploadImage(local, uploader, onError) ?: return@launch picture.value = TextFieldValue(uploadedUrl) pickedMedia = null } @@ -311,79 +297,7 @@ class RelayGroupMetadataViewModel : ViewModel() { private suspend fun uploadImage( galleryUri: SelectedMedia, - context: Context, + uploader: MediaUploader, onError: (String, String) -> Unit, - ): String? { - val sourceUri = - try { - if (account.settings.stripLocationOnUpload) { - val result = MetadataStripper.strip(galleryUri.uri, galleryUri.mimeType, context.applicationContext) - if (!result.stripped) { - onError( - loadStringRes(Res.string.metadata_strip_failed_title), - loadStringRes(Res.string.metadata_strip_failed_upload_cancelled), - ) - return null - } - result.uri - } else { - galleryUri.uri - } - } catch (e: AvifMetadataNotVerifiableException) { - onError( - loadStringRes(Res.string.metadata_strip_failed_title), - loadStringRes(Res.string.avif_metadata_strip_failed, e.message ?: e.javaClass.simpleName), - ) - return null - } - val compResult = MediaCompressor().compress(sourceUri, 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, - ) - } - - result.url ?: run { - onError( - loadStringRes(Res.string.failed_to_upload_media_no_details), - loadStringRes(Res.string.server_did_not_provide_a_url_after_uploading), - ) - null - } - } catch (_: SignerExceptions.ReadOnlyException) { - onError( - loadStringRes(Res.string.failed_to_upload_media_no_details), - loadStringRes(Res.string.login_with_a_private_key_to_be_able_to_upload), - ) - null - } catch (e: Exception) { - if (e is CancellationException) throw e - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName) - null - } - } + ): String? = uploadToDefaultServer(galleryUri, account, uploader, onError) } 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 5514129da4..600be530c4 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 @@ -51,7 +51,6 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_sign_events import com.vitorpamplona.amethyst.commons.resources.read_only_user -import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading import com.vitorpamplona.amethyst.commons.service.pow.PoWReplay import com.vitorpamplona.amethyst.commons.service.upload.MediaUploadTracker import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation @@ -61,7 +60,6 @@ import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState @@ -72,6 +70,7 @@ import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource +import com.vitorpamplona.amethyst.commons.ui.uploads.uploadToDefaultServer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber @@ -487,36 +486,7 @@ class LongFormPostViewModel : galleryUri: SelectedMedia, uploader: MediaUploader, onError: (String, String) -> Unit, - ): String? { - val state = - UploadOrchestrator().upload( - uri = galleryUri.uri, - mimeType = galleryUri.mimeType, - alt = null, - contentWarningReason = null, - compressionQuality = CompressorQuality.MEDIUM, - server = account.settings.defaultFileServer, - account = account, - uploader = uploader, - ) - - return when (state) { - is UploadingState.Finished -> { - val uploaded = state.result - if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) { - uploaded.url - } else { - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.server_did_not_provide_a_url_after_uploading)) - null - } - } - is UploadingState.Error -> { - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(state.errorResource, *state.params)) - null - } - else -> null - } - } + ): String? = uploadToDefaultServer(galleryUri, account, uploader, onError) fun upload( alt: String?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/AddEmojiDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/AddEmojiDialog.kt index 4b1b0f326d..d3ec2ff3ac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/AddEmojiDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/AddEmojiDialog.kt @@ -41,7 +41,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -119,7 +118,6 @@ fun AddEmojiDialog( onValueChange = { url = it }, label = { Text(stringRes(Res.string.emoji_url_label)) }, leadingIcon = { - val context = LocalContext.current SelectSingleFromGallery( isUploading = viewModel.isUploadingEmojiImage, tint = MaterialTheme.colorScheme.onSurfaceVariant, @@ -127,7 +125,7 @@ fun AddEmojiDialog( ) { selected -> viewModel.uploadEmojiImage( uri = selected, - context = context, + uploader = accountViewModel.host.mediaUploader, onUploaded = { uploadedUrl -> url = uploadedUrl }, onError = accountViewModel.toastManager::toast, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/EmojiPackViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/EmojiPackViewModel.kt index 751bce6682..7337b98241 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/EmojiPackViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/EmojiPackViewModel.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.display -import android.content.Context import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -28,31 +27,15 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.Account -import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType -import com.vitorpamplona.amethyst.commons.resources.Res -import com.vitorpamplona.amethyst.commons.resources.avif_metadata_strip_failed -import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details -import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_upload -import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_title -import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_upload_cancelled -import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading -import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia -import com.vitorpamplona.amethyst.commons.ui.loadStringRes -import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MetadataStripper -import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader -import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader -import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.amethyst.commons.ui.uploads.uploadToDefaultServer import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import kotlin.coroutines.cancellation.CancellationException @Stable class EmojiPackViewModel( @@ -86,14 +69,14 @@ class EmojiPackViewModel( fun uploadEmojiImage( uri: SelectedMedia, - context: Context, + uploader: MediaUploader, onUploaded: (String) -> Unit, onError: (String, String) -> Unit, ) { viewModelScope.launch(Dispatchers.IO) { upload( uri, - context, + uploader, onUploading = { isUploadingEmojiImage = it }, onUploaded = onUploaded, onError = onError, @@ -103,83 +86,15 @@ class EmojiPackViewModel( private suspend fun upload( galleryUri: SelectedMedia, - context: Context, + uploader: MediaUploader, onUploading: (Boolean) -> Unit, onUploaded: (String) -> Unit, onError: (String, String) -> Unit, ) { onUploading(true) - - val sourceUri = - try { - if (account.settings.stripLocationOnUpload) { - val result = MetadataStripper.strip(galleryUri.uri, galleryUri.mimeType, context.applicationContext) - if (!result.stripped) { - onError( - loadStringRes(Res.string.metadata_strip_failed_title), - loadStringRes(Res.string.metadata_strip_failed_upload_cancelled), - ) - onUploading(false) - return - } - result.uri - } else { - galleryUri.uri - } - } catch (e: AvifMetadataNotVerifiableException) { - onUploading(false) - onError( - loadStringRes(Res.string.metadata_strip_failed_title), - loadStringRes(Res.string.avif_metadata_strip_failed, e.message ?: e.javaClass.simpleName), - ) - return - } - val compResult = MediaCompressor().compress(sourceUri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext) - - 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) { - onUploading(false) - onUploaded(result.url) - } else { - onUploading(false) - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.server_did_not_provide_a_url_after_uploading)) - } - } catch (_: SignerExceptions.ReadOnlyException) { - onUploading(false) - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.login_with_a_private_key_to_be_able_to_upload)) - } catch (e: Exception) { - if (e is CancellationException) throw e - onUploading(false) - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName) - } + val url = uploadToDefaultServer(galleryUri, account, uploader, onError) + onUploading(false) + if (url != null) onUploaded(url) } @Suppress("UNCHECKED_CAST") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/metadata/EmojiPackMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/metadata/EmojiPackMetadataScreen.kt index dcce6ee4e3..432914c28d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/metadata/EmojiPackMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/metadata/EmojiPackMetadataScreen.kt @@ -53,7 +53,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextAlign @@ -109,7 +108,6 @@ private fun EmojiPackMetadataScaffold( accountViewModel: AccountViewModel, nav: INav, ) { - val context = LocalContext.current val scrollState = rememberScrollState() var wantsToPickImage by remember { mutableStateOf(false) } @@ -127,7 +125,7 @@ private fun EmojiPackMetadataScaffold( val onSubmit: () -> Unit = { viewModel.submit( - context = context, + uploader = accountViewModel.host.mediaUploader, onSuccess = { nav.popBack() }, onError = accountViewModel.toastManager::toast, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/metadata/EmojiPackMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/metadata/EmojiPackMetadataViewModel.kt index 964abeef53..71c5e6500d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/metadata/EmojiPackMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/metadata/EmojiPackMetadataViewModel.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.list.metadata -import android.content.Context import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -29,32 +28,19 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.Account -import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.OwnedEmojiPack import com.vitorpamplona.amethyst.commons.resources.Res -import com.vitorpamplona.amethyst.commons.resources.avif_metadata_strip_failed -import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_sign_events -import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_upload -import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_title -import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_upload_cancelled import com.vitorpamplona.amethyst.commons.resources.read_only_user -import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading -import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.ui.uploads.uploadToDefaultServer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MetadataStripper -import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader -import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlin.coroutines.cancellation.CancellationException @Stable class EmojiPackMetadataViewModel : ViewModel() { @@ -101,7 +87,7 @@ class EmojiPackMetadataViewModel : ViewModel() { } fun submit( - context: Context, + uploader: MediaUploader, onSuccess: () -> Unit, onError: (String, String) -> Unit, ) { @@ -111,7 +97,7 @@ class EmojiPackMetadataViewModel : ViewModel() { try { val local = pickedMedia if (local != null) { - val uploadedUrl = uploadImage(local, context, onError) + val uploadedUrl = uploadImage(local, uploader, onError) if (uploadedUrl == null) { isWorking = false return@launch @@ -165,81 +151,7 @@ class EmojiPackMetadataViewModel : ViewModel() { private suspend fun uploadImage( galleryUri: SelectedMedia, - context: Context, + uploader: MediaUploader, onError: (String, String) -> Unit, - ): String? { - val sourceUri = - try { - if (account.settings.stripLocationOnUpload) { - val result = MetadataStripper.strip(galleryUri.uri, galleryUri.mimeType, context.applicationContext) - if (!result.stripped) { - onError( - loadStringRes(Res.string.metadata_strip_failed_title), - loadStringRes(Res.string.metadata_strip_failed_upload_cancelled), - ) - return null - } - result.uri - } else { - galleryUri.uri - } - } catch (e: AvifMetadataNotVerifiableException) { - onError( - loadStringRes(Res.string.metadata_strip_failed_title), - loadStringRes(Res.string.avif_metadata_strip_failed, e.message ?: e.javaClass.simpleName), - ) - return null - } - val compResult = MediaCompressor().compress(sourceUri, 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) { - result.url - } else { - onError( - loadStringRes(Res.string.failed_to_upload_media_no_details), - loadStringRes(Res.string.server_did_not_provide_a_url_after_uploading), - ) - null - } - } catch (_: SignerExceptions.ReadOnlyException) { - onError( - loadStringRes(Res.string.failed_to_upload_media_no_details), - loadStringRes(Res.string.login_with_a_private_key_to_be_able_to_upload), - ) - null - } catch (e: Exception) { - if (e is CancellationException) throw e - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName) - null - } - } + ): String? = uploadToDefaultServer(galleryUri, account, uploader, onError) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistScreen.kt index a02d8290e2..c220bb01b7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistScreen.kt @@ -57,7 +57,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextOverflow @@ -116,7 +115,6 @@ fun NewMusicPlaylistScreen( nav: INav, ) { val vm: NewMusicPlaylistViewModel = viewModel() - val context = LocalContext.current LaunchedEffect(accountViewModel) { vm.init(accountViewModel, editDTag) @@ -155,7 +153,6 @@ fun NewMusicPlaylistScreen( onPost = { if (!vm.isValid() || isBusy) return@SendingTopBar vm.saveAndPublish( - context = context, accountViewModel = accountViewModel, ) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistViewModel.kt index 2c1d87d16d..6f637bed77 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistViewModel.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.music -import android.content.Context import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.commons.model.Account @@ -185,10 +184,7 @@ class NewMusicPlaylistViewModel : ViewModel() { * runs on [AccountViewModel.viewModelScope] via `launchSigner`, so it keeps running even if * the user leaves the screen — they get a toast notification when it finishes either way. */ - fun saveAndPublish( - context: Context, - accountViewModel: AccountViewModel, - ) { + fun saveAndPublish(accountViewModel: AccountViewModel) { if (isSending.value) return // double-tap guard if (!isValid()) return @@ -218,7 +214,6 @@ class NewMusicPlaylistViewModel : ViewModel() { quality = CompressorQuality.fromSlider(mediaQualitySlider.value), stripMetadata = stripMetadata.value, loadedEvent = loadedEvent, - appContext = context.applicationContext, uploader = accountViewModel.host.mediaUploader, ) @@ -266,7 +261,6 @@ class NewMusicPlaylistViewModel : ViewModel() { val quality: CompressorQuality, val stripMetadata: Boolean, val loadedEvent: MusicPlaylistEvent?, - val appContext: Context, val uploader: MediaUploader, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackScreen.kt index a8b4306067..ed2e74d639 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackScreen.kt @@ -154,7 +154,6 @@ fun NewMusicTrackScreen( onPost = { if (!vm.isValid() || isBusy) return@SendingTopBar vm.saveAndPublish( - context = context, accountViewModel = accountViewModel, ) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackViewModel.kt index e4b010cf45..98dceec6da 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackViewModel.kt @@ -259,10 +259,7 @@ class NewMusicTrackViewModel : ViewModel() { * Errors are reported through the global toast manager rather than a callback because * the screen may already be gone by the time we know the outcome. */ - fun saveAndPublish( - context: Context, - accountViewModel: AccountViewModel, - ) { + fun saveAndPublish(accountViewModel: AccountViewModel) { if (isSending.value) return // double-tap guard // Snapshot every input the upload needs into immutable locals BEFORE launching, so @@ -293,7 +290,6 @@ class NewMusicTrackViewModel : ViewModel() { quality = CompressorQuality.fromSlider(mediaQualitySlider.value), stripMetadata = stripMetadata.value, loadedEvent = loadedEvent, - appContext = context.applicationContext, uploader = accountViewModel.host.mediaUploader, ) @@ -365,7 +361,6 @@ class NewMusicTrackViewModel : ViewModel() { val quality: CompressorQuality, val stripMetadata: Boolean, val loadedEvent: MusicTrackEvent?, - val appContext: Context, val uploader: MediaUploader, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowScreen.kt index 0c21bcadec..c0ab56b896 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowScreen.kt @@ -44,7 +44,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp @@ -97,7 +96,6 @@ fun EditPodcastShowScreen( nav: INav, ) { val vm: EditPodcastShowViewModel = viewModel() - val context = LocalContext.current LaunchedEffect(accountViewModel) { vm.init(accountViewModel) } @@ -124,7 +122,7 @@ fun EditPodcastShowScreen( isActive = { vm.isValid() && !isBusy }, onPost = { if (!vm.isValid() || isBusy) return@SendingTopBar - vm.saveAndPublish(context, accountViewModel) + vm.saveAndPublish(accountViewModel) }, ) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowViewModel.kt index 4566c8f96b..2ea653ed89 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowViewModel.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring -import android.content.Context import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.commons.model.Account @@ -130,10 +129,7 @@ class EditPodcastShowViewModel : ViewModel() { fun isValid(): Boolean = title.value.isNotBlank() - fun saveAndPublish( - context: Context, - accountViewModel: AccountViewModel, - ) { + fun saveAndPublish(accountViewModel: AccountViewModel) { if (isSending.value) return val coverOrch = coverMedia.value @@ -171,7 +167,6 @@ class EditPodcastShowViewModel : ViewModel() { server = server, quality = CompressorQuality.fromSlider(mediaQualitySlider.value), stripMetadata = stripMetadata.value, - appContext = context.applicationContext, uploader = accountViewModel.host.mediaUploader, ) @@ -223,7 +218,6 @@ class EditPodcastShowViewModel : ViewModel() { val server: ServerName?, val quality: CompressorQuality, val stripMetadata: Boolean, - val appContext: Context, val uploader: MediaUploader, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeScreen.kt index 51cd3f9789..346b646aba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeScreen.kt @@ -152,7 +152,7 @@ fun NewPodcastEpisodeScreen( isActive = { vm.isValid() && !isBusy }, onPost = { if (!vm.isValid() || isBusy) return@SendingTopBar - vm.saveAndPublish(context, accountViewModel) + vm.saveAndPublish(accountViewModel) }, ) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeViewModel.kt index d19af8e576..f0e20f62ae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeViewModel.kt @@ -174,10 +174,7 @@ class NewPodcastEpisodeViewModel : ViewModel() { /** Valid with a title and a resolvable audio source (picked file or a pasted URL). */ fun isValid(): Boolean = title.value.isNotBlank() && (audioMedia.value != null || audioUrl.value.isNotBlank()) - fun saveAndPublish( - context: Context, - accountViewModel: AccountViewModel, - ) { + fun saveAndPublish(accountViewModel: AccountViewModel) { if (isSending.value) return val server = selectedServer.value @@ -208,7 +205,6 @@ class NewPodcastEpisodeViewModel : ViewModel() { server = server, quality = CompressorQuality.fromSlider(mediaQualitySlider.value), stripMetadata = stripMetadata.value, - appContext = context.applicationContext, uploader = accountViewModel.host.mediaUploader, ) @@ -269,7 +265,6 @@ class NewPodcastEpisodeViewModel : ViewModel() { val server: ServerName, val quality: CompressorQuality, val stripMetadata: Boolean, - val appContext: Context, val uploader: MediaUploader, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerScreen.kt index 3cf81e31a5..9d35569431 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerScreen.kt @@ -88,7 +88,6 @@ fun NewPodcastTrailerScreen( nav: INav, ) { val vm: NewPodcastTrailerViewModel = viewModel() - val context = LocalContext.current LaunchedEffect(accountViewModel) { vm.init(accountViewModel) } StrippingFailureDialog(vm.strippingFailureConfirmation) @@ -112,7 +111,7 @@ fun NewPodcastTrailerScreen( isActive = { vm.isValid() && !isBusy }, onPost = { if (!vm.isValid() || isBusy) return@SendingTopBar - vm.saveAndPublish(context, accountViewModel) + vm.saveAndPublish(accountViewModel) }, ) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerViewModel.kt index ba63ff107b..a57f1e1f57 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerViewModel.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring -import android.content.Context import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.commons.model.Account @@ -91,10 +90,7 @@ class NewPodcastTrailerViewModel : ViewModel() { fun isValid(): Boolean = title.value.isNotBlank() && (media.value != null || url.value.isNotBlank()) - fun saveAndPublish( - context: Context, - accountViewModel: AccountViewModel, - ) { + fun saveAndPublish(accountViewModel: AccountViewModel) { if (isSending.value) return val server = selectedServer.value if (server == null) { @@ -112,7 +108,6 @@ class NewPodcastTrailerViewModel : ViewModel() { val mediaSnap = media.value val quality = CompressorQuality.fromSlider(mediaQualitySlider.value) val strip = stripMetadata.value - val appContext = context.applicationContext isSending.value = true accountViewModel.launchSigner { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt index e88fa61fef..331148ac2e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt @@ -49,7 +49,6 @@ import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned 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 @@ -194,7 +193,6 @@ private fun ProfilePictureUploadButton( ) { val postViewModel: NewUserMetadataViewModel = viewModel() postViewModel.init(accountViewModel) - val context = LocalContext.current var showGallerySelect by remember { mutableStateOf(false) } if (showGallerySelect) { @@ -202,7 +200,7 @@ private fun ProfilePictureUploadButton( onImageUri = { media -> showGallerySelect = false if (media != null) { - postViewModel.uploadPictureAndSave(media, context, accountViewModel.toastManager::toast) + postViewModel.uploadPictureAndSave(media, accountViewModel.host.mediaUploader, accountViewModel.toastManager::toast) } }, ) diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/uploads/UploadToDefaultServer.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/uploads/UploadToDefaultServer.kt new file mode 100644 index 0000000000..3b6fa75390 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/uploads/UploadToDefaultServer.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.ui.uploads + +import com.vitorpamplona.amethyst.commons.model.Account +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details +import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_upload +import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState +import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import kotlinx.coroutines.CancellationException + +/** + * Compresses, strips (when the account asks for it) and uploads one picked file to the account's + * default media server, returning the hosted URL. Every failure, a metadata-stripping one + * included, is reported through [onError] with a user-facing title and message, and yields null. + * + * For single-image fields (avatars, banners, pack and cover images) that only need a URL back. + */ +suspend fun uploadToDefaultServer( + media: SelectedMedia, + account: Account, + uploader: MediaUploader, + onError: (title: String, message: String) -> Unit, + quality: CompressorQuality = CompressorQuality.MEDIUM, +): String? { + val state = + try { + UploadOrchestrator().upload( + uri = media.uri, + mimeType = media.mimeType, + alt = null, + contentWarningReason = null, + compressionQuality = quality, + server = account.settings.defaultFileServer, + account = account, + uploader = uploader, + stripMetadata = account.settings.stripLocationOnUpload, + onStrippingFailed = { false }, + ) + } catch (_: SignerExceptions.ReadOnlyException) { + onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.login_with_a_private_key_to_be_able_to_upload)) + return null + } catch (e: Exception) { + if (e is CancellationException) throw e + onError(loadStringRes(Res.string.failed_to_upload_media_no_details), e.message ?: e::class.simpleName ?: "") + return null + } + + return when (state) { + is UploadingState.Finished -> { + val uploaded = state.result + if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) { + uploaded.url + } else { + onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.server_did_not_provide_a_url_after_uploading)) + null + } + } + + is UploadingState.Error -> { + onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(state.errorResource, *state.params)) + null + } + } +}