diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/service/uploads/AvifUploadPipelineInstrumentedTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/service/uploads/AvifUploadPipelineInstrumentedTest.kt index 1cbcf9677a..44ae559b7a 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/service/uploads/AvifUploadPipelineInstrumentedTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/service/uploads/AvifUploadPipelineInstrumentedTest.kt @@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.AvifInstrumentedTestSupport.appContext import com.vitorpamplona.amethyst.AvifInstrumentedTestSupport.contentUriFor import com.vitorpamplona.amethyst.AvifInstrumentedTestSupport.copyAssetToCache import com.vitorpamplona.amethyst.commons.service.uploads.AVIF_MIME +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/AndroidMediaUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/AndroidMediaUploader.kt new file mode 100644 index 0000000000..293b53d998 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/AndroidMediaUploader.kt @@ -0,0 +1,592 @@ +/* + * 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.service.uploads + +import android.content.Context +import android.net.Uri +import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.model.Account +import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName +import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType +import com.vitorpamplona.amethyst.commons.model.mediaServers.blossomUploadOrder +import com.vitorpamplona.amethyst.commons.service.upload.BlossomClient +import com.vitorpamplona.amethyst.commons.service.upload.BlossomPaymentException +import com.vitorpamplona.amethyst.commons.service.upload.FileHeader +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaCompressorResult +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUri +import com.vitorpamplona.amethyst.commons.service.uploads.UploadError +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState.UploadingFinalState +import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader +import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent +import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent +import com.vitorpamplona.quartz.nipB7Blossom.BlossomServerUrl +import com.vitorpamplona.quartz.utils.Log +import com.vitorpamplona.quartz.utils.ciphers.NostrCipher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import okhttp3.OkHttpClient +import java.io.File +import kotlin.coroutines.cancellation.CancellationException + +/** + * Android's [MediaUploader]: compresses with LightCompressor/Compressor, strips metadata, encrypts, + * and uploads over the app's upload-role OkHttp clients. Reads files through the application + * [appContext]'s content resolver, so it needs no Activity. + */ +class AndroidMediaUploader( + private val appContext: Context, +) : MediaUploader { + override suspend fun upload( + progress: UploadOrchestrator, + uri: MediaUri, + mimeType: String?, + alt: String?, + contentWarningReason: String?, + compressionQuality: CompressorQuality, + server: ServerName, + account: Account, + useH265: Boolean, + stripMetadata: Boolean, + onStrippingFailed: suspend () -> Boolean, + convertGifToMp4: Boolean, + forcedSigner: NostrSigner?, + ): UploadingFinalState = Job(progress).upload(uri, mimeType, alt, contentWarningReason, compressionQuality, server, account, appContext, useH265, stripMetadata, onStrippingFailed, convertGifToMp4, forcedSigner) + + override suspend fun uploadEncrypted( + progress: UploadOrchestrator, + uri: MediaUri, + mimeType: String?, + alt: String?, + contentWarningReason: String?, + compressionQuality: CompressorQuality, + encrypt: NostrCipher, + server: ServerName, + account: Account, + useH265: Boolean, + stripMetadata: Boolean, + onStrippingFailed: suspend () -> Boolean, + convertGifToMp4: Boolean, + forcedSigner: NostrSigner?, + ): UploadingFinalState = Job(progress).uploadEncrypted(uri, mimeType, alt, contentWarningReason, compressionQuality, encrypt, server, account, appContext, useH265, stripMetadata, onStrippingFailed, convertGifToMp4, forcedSigner) + + override suspend fun compressIfNeeded( + progress: UploadOrchestrator, + uri: MediaUri, + mimeType: String?, + compressionQuality: CompressorQuality, + useH265: Boolean, + convertGifToMp4: Boolean, + ): MediaCompressorResult = Job(progress).compressIfNeeded(uri, mimeType, compressionQuality, appContext, useH265, convertGifToMp4) + + /** One upload's run: the pipeline, reporting into [progress]. */ + private class Job( + val progress: UploadOrchestrator, + ) { + fun error( + error: UploadError, + vararg params: String, + ) = progress.error(error, *params) + + fun finish(result: UploadOrchestrator.OrchestratorResult) = progress.finish(result) + + fun updateState( + newProgress: Double, + newState: UploadingState, + ) = progress.updateState(newProgress, newState) + + private fun uploadNIP95( + fileUri: Uri, + contentType: String?, + originalContentType: String?, + originalHash: String?, + context: Context, + ): UploadingFinalState { + updateState(0.4, UploadingState.Uploading) + + val bytes = + context.contentResolver.openInputStream(fileUri)?.use { + it.readBytes() + } + + if (bytes != null) { + if (bytes.size > 80000) { + return error(UploadError.MEDIA_TOO_BIG_FOR_NIP95) + } + + updateState(0.8, UploadingState.Hashing) + + val result = + FileHeader.prepare( + bytes, + contentType, + null, + ) + + result.fold( + onSuccess = { + return finish(UploadOrchestrator.OrchestratorResult.NIP95Result(it, bytes, originalContentType, originalHash)) + }, + onFailure = { + return error(UploadError.COULD_NOT_CHECK_DOWNLOADED_FILE, it.message ?: it.javaClass.simpleName) + }, + ) + } else { + return error(UploadError.COULD_NOT_OPEN_COMPRESSED_FILE) + } + } + + private suspend fun uploadNIP96( + fileUri: Uri, + contentType: String?, + size: Long?, + alt: String?, + contentWarningReason: String?, + serverBaseUrl: String, + contentTypeForResult: String?, + originalHash: String?, + account: Account, + forcedSigner: NostrSigner?, + context: Context, + ): UploadingFinalState { + updateState(0.2, UploadingState.Uploading) + return try { + val result = + Nip96Uploader().upload( + uri = fileUri, + contentType = contentType, + size = size, + alt = alt, + sensitiveContent = contentWarningReason, + serverBaseUrl = serverBaseUrl, + okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, + onProgress = { percent: Float -> + updateState(0.2 + (0.2 * percent), UploadingState.Uploading) + }, + httpAuth = + if (forcedSigner != null) { + { url, method, body -> forcedSigner.sign(HTTPAuthorizationEvent.build(url, method, body)) } + } else { + account::createHTTPAuthorization + }, + context = context, + ) + + verifyHeader( + uploadResult = result, + localContentType = contentType, + originalContentType = contentTypeForResult, + originalHash = originalHash, + okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, + ) + } catch (_: SignerExceptions.ReadOnlyException) { + error(UploadError.LOGIN_WITH_PRIVATE_KEY) + } catch (e: Exception) { + if (e is CancellationException) throw e + error(UploadError.FAILED_TO_UPLOAD_MEDIA, e.message ?: e.javaClass.simpleName) + } + } + + /** + * Tries [selected], then the rest of the servers the picker offered (see + * [blossomUploadOrder]) until one stores the blob. Every server gets the same blob, so they + * share one signed upload token: the signer is asked once, and a signer that refused, timed + * out or is read-only stops the fallback instead of prompting again per server. Attempts + * don't publish their errors; when every server fails, the error shown is the selected + * server's, the one the user chose and will recognize. + */ + private suspend fun uploadBlossomWithFallback( + selected: ServerName, + account: Account, + uploadTo: suspend (serverBaseUrl: String, auth: SharedUploadAuth) -> UploadingFinalState, + ): UploadingFinalState { + val order = blossomUploadOrder(selected, account.blossomServers.hostNameFlow.value) + val auth = SharedUploadAuth() + var firstError: UploadingState.Error? = null + for (server in order) { + when (val result = uploadTo(server.baseUrl, auth)) { + is UploadingState.Finished -> return result + is UploadingState.Error -> { + if (firstError == null) firstError = result + if (auth.signerFailed) return result.also { updateState(0.0, it) } + Log.w("UploadOrchestrator", "Upload to ${server.baseUrl} failed, trying the next server") + } + } + } + return firstError!!.also { updateState(0.0, it) } + } + + /** + * The upload token for one blob, signed at most once and reused by every server tried. + * A signer that returned no token is asked again on the next server; only a token or a + * signer failure is kept. + */ + private class SharedUploadAuth { + private var outcome: Result? = null + + var signerFailed = false + private set + + suspend fun get(sign: suspend () -> BlossomAuthorizationEvent?): BlossomAuthorizationEvent? { + outcome?.let { return it.getOrThrow() } + return try { + sign()?.also { outcome = Result.success(it) } + } catch (e: SignerExceptions) { + signerFailed = true + outcome = Result.failure(e) + throw e + } + } + } + + private suspend fun uploadBlossom( + fileUri: Uri, + contentType: String?, + size: Long?, + alt: String?, + contentWarningReason: String?, + serverBaseUrl: String, + contentTypeForResult: String?, + originalHash: String?, + account: Account, + forcedSigner: NostrSigner?, + context: Context, + sharedAuth: SharedUploadAuth, + ): UploadingFinalState { + updateState(0.2, UploadingState.Uploading) + // BUD-05: route through /media (optimize) when the user opted in. The forced-signer + // path (e.g. NIP-46 draft signing) always uses the bit-exact /upload. + val useMedia = forcedSigner == null && account.settings.optimizeMediaOnUpload.value + return try { + val result = + BlossomUploader() + .upload( + uri = fileUri, + contentType = contentType, + size = size, + alt = alt, + sensitiveContent = contentWarningReason, + serverBaseUrl = serverBaseUrl, + okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, + // Use a t=media token when optimizing via /media, otherwise a plain + // t=upload token. Tokens are intentionally NOT server-scoped on the + // upload path: some servers reject an upload whose auth carries a + // `server` tag, and upload-token replay is not the threat scoping + // guards against (delete tokens are — those stay scoped). + httpAuth = { hash, size, alt -> + sharedAuth.get { + when { + forcedSigner != null -> BlossomAuthorizationEvent.createUploadAuth(hash, size, alt, forcedSigner) + useMedia -> account.createBlossomMediaAuth(hash, size, alt) + else -> account.createBlossomUploadAuth(hash, size, alt) + } + } + }, + context = context, + useMediaEndpoint = useMedia, + ) + + val finalState = + verifyHeader( + uploadResult = result, + localContentType = contentType, + okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, + originalHash = originalHash, + originalContentType = contentTypeForResult, + ) + + // BUD-04: replicate the blob to the user's other Blossom servers for redundancy. + // Fire-and-forget on the account scope AFTER the upload is finished: mirroring is + // pure background redundancy, so it must never delay, alter, or fail the upload the + // user already completed, and must not touch the on-screen progress state. + if (finalState is UploadingState.Finished && forcedSigner == null && account.settings.mirrorUploadsToAllServers.value) { + account.scope.launch(Dispatchers.IO) { + try { + mirrorToOtherServers(result, serverBaseUrl, account) + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.w("UploadOrchestrator", "Background mirror failed", e) + } + } + } + + finalState + } catch (_: SignerExceptions.ReadOnlyException) { + UploadingState.Error(UploadError.LOGIN_WITH_PRIVATE_KEY, emptyArray()) + } catch (e: BlossomPaymentException) { + // BUD-07: the server wants payment before it will store the blob. + UploadingState.Error(UploadError.BLOSSOM_PAYMENT_REQUIRED, arrayOf(e.payment.reason ?: serverBaseUrl)) + } catch (e: Exception) { + if (e is CancellationException) throw e + UploadingState.Error(UploadError.FAILED_TO_UPLOAD_MEDIA, arrayOf(e.message?.ifBlank { null } ?: e.javaClass.simpleName)) + } + } + + /** + * BUD-04 mirror fan-out: asks every *other* Blossom server in the account's + * kind-10063 list to pull the freshly-uploaded blob from [result]'s URL. Runs + * in the background after the upload is finished (see caller), so it never + * delays the user's post or touches the upload progress UI; per-server failures + * are swallowed. Requires the blob's sha256 so server B can verify the download. + */ + private suspend fun mirrorToOtherServers( + result: MediaUploadResult, + primaryServerBaseUrl: String, + account: Account, + ) { + val sourceUrl = result.url ?: return + val hash = result.sha256 ?: sourceUrl.substringAfterLast('/').substringBefore('.') + if (hash.length != 64) return + + // Only the user's *explicitly configured* kind-10063 servers (flow), NOT the + // DEFAULT_MEDIA_SERVERS fallback that hostNameFlow injects — we must never fan + // uploads out to public defaults the user never opted into. + val primaryDomain = BlossomServerUrl.domain(primaryServerBaseUrl) + val targets = + account.blossomServers.flow.value + .filter { BlossomServerUrl.domain(it) != primaryDomain } + .distinct() + + if (targets.isEmpty()) return + + val contentType = result.type ?: "application/octet-stream" + targets.forEach { target -> + try { + val auth = account.createBlossomUploadAuth(hash, result.size ?: 0L, "Mirror $hash").toAuthorizationHeader() + BlossomClient(Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForUploads(target)) + .mirrorOrUpload(sourceUrl, hash, contentType, target, auth) + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.w("UploadOrchestrator", "Failed to mirror $hash to $target", e) + } + } + } + + private suspend fun verifyHeader( + uploadResult: MediaUploadResult, + localContentType: String?, + originalContentType: String?, + originalHash: String?, + okHttpClient: (String) -> OkHttpClient, + ): UploadingFinalState { + if (uploadResult.url.isNullOrBlank()) { + return error(UploadError.SERVER_DID_NOT_PROVIDE_URL) + } + + updateState(0.6, UploadingState.Downloading) + + // Use streaming verification for memory efficiency with large files + val verification = + ImageDownloader().waitAndVerifyStream(uploadResult.url, okHttpClient) + ?: return error(UploadError.COULD_NOT_DOWNLOAD_FROM_SERVER) + + updateState(0.8, UploadingState.Hashing) + + // Create FileHeader with hash from streaming verification + // Note: We skip blurhash/dimensions since we already have them from upload + val fileHeader = + FileHeader( + mimeType = uploadResult.type ?: localContentType ?: verification.contentType, + hash = verification.hash, + size = verification.size.toInt(), + dim = uploadResult.dimension, + blurHash = uploadResult.blurHash, + ) + + return finish( + UploadOrchestrator.OrchestratorResult.ServerResult( + fileHeader, + uploadResult.url, + uploadResult.magnet, + uploadResult.sha256, + originalContentType, + originalHash, + ), + ) + } + + suspend fun compressIfNeeded( + uri: Uri, + mimeType: String?, + compressionQuality: CompressorQuality, + context: Context, + useH265: Boolean = false, + convertGifToMp4: Boolean = false, + ) = if (compressionQuality != CompressorQuality.UNCOMPRESSED || convertGifToMp4) { + updateState(0.02, UploadingState.Compressing) + MediaCompressor().compress(uri, mimeType, compressionQuality, context.applicationContext, useH265, convertGifToMp4) + } else { + MediaCompressorResult(uri, mimeType, null) + } + + private suspend fun stripAfterCompression( + originalUri: Uri, + compressed: MediaCompressorResult, + mimeType: String?, + compressionQuality: CompressorQuality, + stripMetadata: Boolean, + onStrippingFailed: suspend () -> Boolean, + context: Context, + ): Uri? { + if (!stripMetadata) return compressed.uri + + val effectiveMimeType = compressed.contentType ?: mimeType + val isVideo = effectiveMimeType?.startsWith("video/", ignoreCase = true) == true + val compressionRequested = compressionQuality != CompressorQuality.UNCOMPRESSED + val compressionApplied = compressionRequested && compressed.uri != originalUri + + val strippingResult = + if (isVideo && compressionApplied) { + // Compression was requested and actually applied to a video; + // assume it stripped metadata successfully. + StrippingResult(compressed.uri, true) + } else { + // AvifMetadataNotVerifiableException is allowed to propagate so the caller + // can emit a specific error rather than the generic "Upload cancelled". + MetadataStripper.strip(compressed.uri, effectiveMimeType, context.applicationContext) + } + + if (!strippingResult.stripped && !onStrippingFailed()) return null + + return strippingResult.uri + } + + /** + * Deletes a temporary file created during the upload pipeline if its URI + * differs from the original (meaning it's an intermediate temp file, not the user's content). + */ + internal fun deleteTempUri( + tempUri: Uri, + originalUri: Uri, + ) { + if (tempUri == originalUri) return + try { + val path = tempUri.path ?: return + val file = File(path) + if (file.delete()) { + Log.d("UploadOrchestrator") { "Deleted temp file: $path" } + } + } catch (e: Exception) { + Log.w("UploadOrchestrator", "Failed to delete temp file: ${tempUri.path}", e) + } + } + + suspend fun upload( + uri: Uri, + mimeType: String?, + alt: String?, + contentWarningReason: String?, + compressionQuality: CompressorQuality, + server: ServerName, + account: Account, + context: Context, + useH265: Boolean = false, + stripMetadata: Boolean = true, + onStrippingFailed: suspend () -> Boolean = { true }, + convertGifToMp4: Boolean = false, + forcedSigner: NostrSigner? = null, + ): UploadingFinalState { + val compressed = compressIfNeeded(uri, mimeType, compressionQuality, context, useH265, convertGifToMp4) + + val finalUri = + try { + stripAfterCompression(uri, compressed, mimeType, compressionQuality, stripMetadata, onStrippingFailed, context) + } catch (e: AvifMetadataNotVerifiableException) { + return error(UploadError.AVIF_METADATA_STRIP_FAILED, e.message ?: e.javaClass.simpleName).also { + deleteTempUri(compressed.uri, uri) + } + } ?: return error(UploadError.UPLOAD_CANCELLED).also { + deleteTempUri(compressed.uri, uri) + } + + if (compressed.uri != finalUri) deleteTempUri(compressed.uri, uri) + + try { + return when (server.type) { + ServerType.NIP95 -> uploadNIP95(finalUri, compressed.contentType, null, null, context) + ServerType.NIP96 -> uploadNIP96(finalUri, compressed.contentType, compressed.size, alt, contentWarningReason, server.baseUrl, null, null, account, forcedSigner, context) + ServerType.Blossom -> + uploadBlossomWithFallback(server, account) { baseUrl, auth -> + uploadBlossom(finalUri, compressed.contentType, compressed.size, alt, contentWarningReason, baseUrl, null, null, account, forcedSigner, context, auth) + } + } + } finally { + deleteTempUri(finalUri, uri) + } + } + + suspend fun uploadEncrypted( + uri: Uri, + mimeType: String?, + alt: String?, + contentWarningReason: String?, + compressionQuality: CompressorQuality, + encrypt: NostrCipher, + server: ServerName, + account: Account, + context: Context, + useH265: Boolean = false, + stripMetadata: Boolean = true, + onStrippingFailed: suspend () -> Boolean = { true }, + convertGifToMp4: Boolean = false, + forcedSigner: NostrSigner? = null, + ): UploadingFinalState { + val compressed = compressIfNeeded(uri, mimeType, compressionQuality, context, useH265, convertGifToMp4) + + val finalUri = + try { + stripAfterCompression(uri, compressed, mimeType, compressionQuality, stripMetadata, onStrippingFailed, context) + } catch (e: AvifMetadataNotVerifiableException) { + return error(UploadError.AVIF_METADATA_STRIP_FAILED, e.message ?: e.javaClass.simpleName).also { + deleteTempUri(compressed.uri, uri) + } + } ?: return error(UploadError.UPLOAD_CANCELLED).also { + deleteTempUri(compressed.uri, uri) + } + + if (compressed.uri != finalUri) deleteTempUri(compressed.uri, uri) + + val encrypted = EncryptFiles().encryptFile(context, finalUri, encrypt) + deleteTempUri(finalUri, uri) + + try { + return when (server.type) { + ServerType.NIP95 -> uploadNIP95(encrypted.uri, encrypted.contentType, compressed.contentType, encrypted.originalHash, context) + ServerType.NIP96 -> uploadNIP96(encrypted.uri, encrypted.contentType, encrypted.size, alt, contentWarningReason, server.baseUrl, compressed.contentType, encrypted.originalHash, account, forcedSigner, context) + // The same encrypted file goes to every server tried, so its key, nonce and + // hash stay valid whichever one ends up holding it. + ServerType.Blossom -> + uploadBlossomWithFallback(server, account) { baseUrl, auth -> + uploadBlossom(encrypted.uri, encrypted.contentType, encrypted.size, alt, contentWarningReason, baseUrl, compressed.contentType, encrypted.originalHash, account, forcedSigner, context, auth) + } + } + } finally { + deleteTempUri(encrypted.uri, uri) + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaCompressor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaCompressor.kt index c36b42ed1d..d2fa109863 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaCompressor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaCompressor.kt @@ -26,6 +26,8 @@ import android.net.Uri import androidx.core.net.toUri import androidx.media3.common.MimeTypes import com.davotoula.lightcompressor.video.GifToMp4Converter +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaCompressorResult import com.vitorpamplona.amethyst.commons.service.uploads.isAvif import com.vitorpamplona.amethyst.ui.components.util.MediaCompressorFileUtils import com.vitorpamplona.quartz.utils.Log @@ -34,12 +36,6 @@ import id.zelory.compressor.constraint.default import kotlinx.coroutines.CancellationException import java.io.File -class MediaCompressorResult( - val uri: Uri, - val contentType: String?, - val size: Long?, -) - class MediaCompressor { // ALL ERRORS ARE IGNORED. The original file is returned. suspend fun compress( @@ -125,24 +121,4 @@ class MediaCompressor { MediaCompressorResult(uri, contentType, null) } } - - companion object { - fun intToCompressorQuality(mediaQualityFloat: Int): CompressorQuality = - when (mediaQualityFloat) { - 0 -> CompressorQuality.LOW - 1 -> CompressorQuality.MEDIUM - 2 -> CompressorQuality.HIGH - 3 -> CompressorQuality.UNCOMPRESSED - else -> CompressorQuality.MEDIUM - } - } -} - -enum class CompressorQuality { - VERY_LOW, - LOW, - MEDIUM, - HIGH, - VERY_HIGH, - UNCOMPRESSED, } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt deleted file mode 100644 index af41342da5..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt +++ /dev/null @@ -1,602 +0,0 @@ -/* - * 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.service.uploads - -import android.content.Context -import android.net.Uri -import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.commons.model.Account -import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName -import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType -import com.vitorpamplona.amethyst.commons.model.mediaServers.blossomUploadOrder -import com.vitorpamplona.amethyst.commons.resources.Res -import com.vitorpamplona.amethyst.commons.resources.avif_metadata_strip_failed -import com.vitorpamplona.amethyst.commons.resources.blossom_payment_required -import com.vitorpamplona.amethyst.commons.resources.could_not_check_downloaded_file -import com.vitorpamplona.amethyst.commons.resources.could_not_download_from_the_server -import com.vitorpamplona.amethyst.commons.resources.could_not_open_the_compressed_file -import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media -import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_upload -import com.vitorpamplona.amethyst.commons.resources.media_too_big_for_nip95 -import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading -import com.vitorpamplona.amethyst.commons.resources.upload_cancelled -import com.vitorpamplona.amethyst.commons.service.upload.BlossomClient -import com.vitorpamplona.amethyst.commons.service.upload.BlossomPaymentException -import com.vitorpamplona.amethyst.commons.service.upload.FileHeader -import com.vitorpamplona.amethyst.service.uploads.UploadingState.UploadingFinalState -import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader -import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions -import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent -import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent -import com.vitorpamplona.quartz.nipB7Blossom.BlossomServerUrl -import com.vitorpamplona.quartz.utils.Log -import com.vitorpamplona.quartz.utils.ciphers.NostrCipher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import okhttp3.OkHttpClient -import org.jetbrains.compose.resources.StringResource -import java.io.File -import kotlin.coroutines.cancellation.CancellationException - -sealed class UploadingState { - data object Ready : UploadingState() - - data object Compressing : UploadingState() - - data object Uploading : UploadingState() - - data object ServerProcessing : UploadingState() - - data object Downloading : UploadingState() - - data object Hashing : UploadingState() - - sealed class UploadingFinalState : UploadingState() - - class Finished( - val result: UploadOrchestrator.OrchestratorResult, - ) : UploadingFinalState() - - class Error( - val errorResource: StringResource, - val params: Array, - ) : UploadingFinalState() -} - -class UploadOrchestrator { - val progress = MutableStateFlow(0.0) - val progressState = MutableStateFlow(UploadingState.Ready) - - val isUploading = - progressState.map { - progressState.value !is UploadingState.Ready && progressState.value !is UploadingState.Error && progressState.value !is UploadingState.Finished - } - - fun error( - resId: StringResource, - vararg params: String, - ) = UploadingState.Error(resId, params).also { updateState(0.0, it) } - - fun finish(result: OrchestratorResult) = - UploadingState - .Finished(result) - .also { updateState(1.0, it) } - - fun updateState( - newProgress: Double, - newState: UploadingState, - ) { - progress.value = newProgress - progressState.value = newState - } - - private fun uploadNIP95( - fileUri: Uri, - contentType: String?, - originalContentType: String?, - originalHash: String?, - context: Context, - ): UploadingFinalState { - updateState(0.4, UploadingState.Uploading) - - val bytes = - context.contentResolver.openInputStream(fileUri)?.use { - it.readBytes() - } - - if (bytes != null) { - if (bytes.size > 80000) { - return error(Res.string.media_too_big_for_nip95) - } - - updateState(0.8, UploadingState.Hashing) - - val result = - FileHeader.prepare( - bytes, - contentType, - null, - ) - - result.fold( - onSuccess = { - return finish(OrchestratorResult.NIP95Result(it, bytes, originalContentType, originalHash)) - }, - onFailure = { - return error(Res.string.could_not_check_downloaded_file, it.message ?: it.javaClass.simpleName) - }, - ) - } else { - return error(Res.string.could_not_open_the_compressed_file) - } - } - - private suspend fun uploadNIP96( - fileUri: Uri, - contentType: String?, - size: Long?, - alt: String?, - contentWarningReason: String?, - serverBaseUrl: String, - contentTypeForResult: String?, - originalHash: String?, - account: Account, - forcedSigner: NostrSigner?, - context: Context, - ): UploadingFinalState { - updateState(0.2, UploadingState.Uploading) - return try { - val result = - Nip96Uploader().upload( - uri = fileUri, - contentType = contentType, - size = size, - alt = alt, - sensitiveContent = contentWarningReason, - serverBaseUrl = serverBaseUrl, - okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, - onProgress = { percent: Float -> - updateState(0.2 + (0.2 * percent), UploadingState.Uploading) - }, - httpAuth = - if (forcedSigner != null) { - { url, method, body -> forcedSigner.sign(HTTPAuthorizationEvent.build(url, method, body)) } - } else { - account::createHTTPAuthorization - }, - context = context, - ) - - verifyHeader( - uploadResult = result, - localContentType = contentType, - originalContentType = contentTypeForResult, - originalHash = originalHash, - okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, - ) - } catch (_: SignerExceptions.ReadOnlyException) { - error(Res.string.login_with_a_private_key_to_be_able_to_upload) - } catch (e: Exception) { - if (e is CancellationException) throw e - error(Res.string.failed_to_upload_media, e.message ?: e.javaClass.simpleName) - } - } - - /** - * Tries [selected], then the rest of the servers the picker offered (see - * [blossomUploadOrder]) until one stores the blob. Every server gets the same blob, so they - * share one signed upload token: the signer is asked once, and a signer that refused, timed - * out or is read-only stops the fallback instead of prompting again per server. Attempts - * don't publish their errors; when every server fails, the error shown is the selected - * server's, the one the user chose and will recognize. - */ - private suspend fun uploadBlossomWithFallback( - selected: ServerName, - account: Account, - uploadTo: suspend (serverBaseUrl: String, auth: SharedUploadAuth) -> UploadingFinalState, - ): UploadingFinalState { - val order = blossomUploadOrder(selected, account.blossomServers.hostNameFlow.value) - val auth = SharedUploadAuth() - var firstError: UploadingState.Error? = null - for (server in order) { - when (val result = uploadTo(server.baseUrl, auth)) { - is UploadingState.Finished -> return result - is UploadingState.Error -> { - if (firstError == null) firstError = result - if (auth.signerFailed) return result.also { updateState(0.0, it) } - Log.w("UploadOrchestrator", "Upload to ${server.baseUrl} failed, trying the next server") - } - } - } - return firstError!!.also { updateState(0.0, it) } - } - - /** - * The upload token for one blob, signed at most once and reused by every server tried. - * A signer that returned no token is asked again on the next server; only a token or a - * signer failure is kept. - */ - private class SharedUploadAuth { - private var outcome: Result? = null - - var signerFailed = false - private set - - suspend fun get(sign: suspend () -> BlossomAuthorizationEvent?): BlossomAuthorizationEvent? { - outcome?.let { return it.getOrThrow() } - return try { - sign()?.also { outcome = Result.success(it) } - } catch (e: SignerExceptions) { - signerFailed = true - outcome = Result.failure(e) - throw e - } - } - } - - private suspend fun uploadBlossom( - fileUri: Uri, - contentType: String?, - size: Long?, - alt: String?, - contentWarningReason: String?, - serverBaseUrl: String, - contentTypeForResult: String?, - originalHash: String?, - account: Account, - forcedSigner: NostrSigner?, - context: Context, - sharedAuth: SharedUploadAuth, - ): UploadingFinalState { - updateState(0.2, UploadingState.Uploading) - // BUD-05: route through /media (optimize) when the user opted in. The forced-signer - // path (e.g. NIP-46 draft signing) always uses the bit-exact /upload. - val useMedia = forcedSigner == null && account.settings.optimizeMediaOnUpload.value - return try { - val result = - BlossomUploader() - .upload( - uri = fileUri, - contentType = contentType, - size = size, - alt = alt, - sensitiveContent = contentWarningReason, - serverBaseUrl = serverBaseUrl, - okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, - // Use a t=media token when optimizing via /media, otherwise a plain - // t=upload token. Tokens are intentionally NOT server-scoped on the - // upload path: some servers reject an upload whose auth carries a - // `server` tag, and upload-token replay is not the threat scoping - // guards against (delete tokens are — those stay scoped). - httpAuth = { hash, size, alt -> - sharedAuth.get { - when { - forcedSigner != null -> BlossomAuthorizationEvent.createUploadAuth(hash, size, alt, forcedSigner) - useMedia -> account.createBlossomMediaAuth(hash, size, alt) - else -> account.createBlossomUploadAuth(hash, size, alt) - } - } - }, - context = context, - useMediaEndpoint = useMedia, - ) - - val finalState = - verifyHeader( - uploadResult = result, - localContentType = contentType, - okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, - originalHash = originalHash, - originalContentType = contentTypeForResult, - ) - - // BUD-04: replicate the blob to the user's other Blossom servers for redundancy. - // Fire-and-forget on the account scope AFTER the upload is finished: mirroring is - // pure background redundancy, so it must never delay, alter, or fail the upload the - // user already completed, and must not touch the on-screen progress state. - if (finalState is UploadingState.Finished && forcedSigner == null && account.settings.mirrorUploadsToAllServers.value) { - account.scope.launch(Dispatchers.IO) { - try { - mirrorToOtherServers(result, serverBaseUrl, account) - } catch (e: Exception) { - if (e is CancellationException) throw e - Log.w("UploadOrchestrator", "Background mirror failed", e) - } - } - } - - finalState - } catch (_: SignerExceptions.ReadOnlyException) { - UploadingState.Error(Res.string.login_with_a_private_key_to_be_able_to_upload, emptyArray()) - } catch (e: BlossomPaymentException) { - // BUD-07: the server wants payment before it will store the blob. - UploadingState.Error(Res.string.blossom_payment_required, arrayOf(e.payment.reason ?: serverBaseUrl)) - } catch (e: Exception) { - if (e is CancellationException) throw e - UploadingState.Error(Res.string.failed_to_upload_media, arrayOf(e.message?.ifBlank { null } ?: e.javaClass.simpleName)) - } - } - - /** - * BUD-04 mirror fan-out: asks every *other* Blossom server in the account's - * kind-10063 list to pull the freshly-uploaded blob from [result]'s URL. Runs - * in the background after the upload is finished (see caller), so it never - * delays the user's post or touches the upload progress UI; per-server failures - * are swallowed. Requires the blob's sha256 so server B can verify the download. - */ - private suspend fun mirrorToOtherServers( - result: MediaUploadResult, - primaryServerBaseUrl: String, - account: Account, - ) { - val sourceUrl = result.url ?: return - val hash = result.sha256 ?: sourceUrl.substringAfterLast('/').substringBefore('.') - if (hash.length != 64) return - - // Only the user's *explicitly configured* kind-10063 servers (flow), NOT the - // DEFAULT_MEDIA_SERVERS fallback that hostNameFlow injects — we must never fan - // uploads out to public defaults the user never opted into. - val primaryDomain = BlossomServerUrl.domain(primaryServerBaseUrl) - val targets = - account.blossomServers.flow.value - .filter { BlossomServerUrl.domain(it) != primaryDomain } - .distinct() - - if (targets.isEmpty()) return - - val contentType = result.type ?: "application/octet-stream" - targets.forEach { target -> - try { - val auth = account.createBlossomUploadAuth(hash, result.size ?: 0L, "Mirror $hash").toAuthorizationHeader() - BlossomClient(Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForUploads(target)) - .mirrorOrUpload(sourceUrl, hash, contentType, target, auth) - } catch (e: Exception) { - if (e is CancellationException) throw e - Log.w("UploadOrchestrator", "Failed to mirror $hash to $target", e) - } - } - } - - private suspend fun verifyHeader( - uploadResult: MediaUploadResult, - localContentType: String?, - originalContentType: String?, - originalHash: String?, - okHttpClient: (String) -> OkHttpClient, - ): UploadingFinalState { - if (uploadResult.url.isNullOrBlank()) { - return error(Res.string.server_did_not_provide_a_url_after_uploading) - } - - updateState(0.6, UploadingState.Downloading) - - // Use streaming verification for memory efficiency with large files - val verification = - ImageDownloader().waitAndVerifyStream(uploadResult.url, okHttpClient) - ?: return error(Res.string.could_not_download_from_the_server) - - updateState(0.8, UploadingState.Hashing) - - // Create FileHeader with hash from streaming verification - // Note: We skip blurhash/dimensions since we already have them from upload - val fileHeader = - FileHeader( - mimeType = uploadResult.type ?: localContentType ?: verification.contentType, - hash = verification.hash, - size = verification.size.toInt(), - dim = uploadResult.dimension, - blurHash = uploadResult.blurHash, - ) - - return finish( - OrchestratorResult.ServerResult( - fileHeader, - uploadResult.url, - uploadResult.magnet, - uploadResult.sha256, - originalContentType, - originalHash, - ), - ) - } - - sealed class OrchestratorResult { - class NIP95Result( - val fileHeader: FileHeader, - val bytes: ByteArray, - val mimeTypeBeforeEncryption: String?, - val hashBeforeEncryption: String?, - ) : OrchestratorResult() - - class ServerResult( - val fileHeader: FileHeader, - val url: String, - val magnet: String?, - val uploadedHash: String?, - val mimeTypeBeforeEncryption: String?, - val hashBeforeEncryption: String?, - ) : OrchestratorResult() - } - - suspend fun compressIfNeeded( - uri: Uri, - mimeType: String?, - compressionQuality: CompressorQuality, - context: Context, - useH265: Boolean = false, - convertGifToMp4: Boolean = false, - ) = if (compressionQuality != CompressorQuality.UNCOMPRESSED || convertGifToMp4) { - updateState(0.02, UploadingState.Compressing) - MediaCompressor().compress(uri, mimeType, compressionQuality, context.applicationContext, useH265, convertGifToMp4) - } else { - MediaCompressorResult(uri, mimeType, null) - } - - private suspend fun stripAfterCompression( - originalUri: Uri, - compressed: MediaCompressorResult, - mimeType: String?, - compressionQuality: CompressorQuality, - stripMetadata: Boolean, - onStrippingFailed: suspend () -> Boolean, - context: Context, - ): Uri? { - if (!stripMetadata) return compressed.uri - - val effectiveMimeType = compressed.contentType ?: mimeType - val isVideo = effectiveMimeType?.startsWith("video/", ignoreCase = true) == true - val compressionRequested = compressionQuality != CompressorQuality.UNCOMPRESSED - val compressionApplied = compressionRequested && compressed.uri != originalUri - - val strippingResult = - if (isVideo && compressionApplied) { - // Compression was requested and actually applied to a video; - // assume it stripped metadata successfully. - StrippingResult(compressed.uri, true) - } else { - // AvifMetadataNotVerifiableException is allowed to propagate so the caller - // can emit a specific error rather than the generic "Upload cancelled". - MetadataStripper.strip(compressed.uri, effectiveMimeType, context.applicationContext) - } - - if (!strippingResult.stripped && !onStrippingFailed()) return null - - return strippingResult.uri - } - - /** - * Deletes a temporary file created during the upload pipeline if its URI - * differs from the original (meaning it's an intermediate temp file, not the user's content). - */ - internal fun deleteTempUri( - tempUri: Uri, - originalUri: Uri, - ) { - if (tempUri == originalUri) return - try { - val path = tempUri.path ?: return - val file = File(path) - if (file.delete()) { - Log.d("UploadOrchestrator") { "Deleted temp file: $path" } - } - } catch (e: Exception) { - Log.w("UploadOrchestrator", "Failed to delete temp file: ${tempUri.path}", e) - } - } - - suspend fun upload( - uri: Uri, - mimeType: String?, - alt: String?, - contentWarningReason: String?, - compressionQuality: CompressorQuality, - server: ServerName, - account: Account, - context: Context, - useH265: Boolean = false, - stripMetadata: Boolean = true, - onStrippingFailed: suspend () -> Boolean = { true }, - convertGifToMp4: Boolean = false, - forcedSigner: NostrSigner? = null, - ): UploadingFinalState { - val compressed = compressIfNeeded(uri, mimeType, compressionQuality, context, useH265, convertGifToMp4) - - val finalUri = - try { - stripAfterCompression(uri, compressed, mimeType, compressionQuality, stripMetadata, onStrippingFailed, context) - } catch (e: AvifMetadataNotVerifiableException) { - return error(Res.string.avif_metadata_strip_failed, e.message ?: e.javaClass.simpleName).also { - deleteTempUri(compressed.uri, uri) - } - } ?: return error(Res.string.upload_cancelled).also { - deleteTempUri(compressed.uri, uri) - } - - if (compressed.uri != finalUri) deleteTempUri(compressed.uri, uri) - - try { - return when (server.type) { - ServerType.NIP95 -> uploadNIP95(finalUri, compressed.contentType, null, null, context) - ServerType.NIP96 -> uploadNIP96(finalUri, compressed.contentType, compressed.size, alt, contentWarningReason, server.baseUrl, null, null, account, forcedSigner, context) - ServerType.Blossom -> - uploadBlossomWithFallback(server, account) { baseUrl, auth -> - uploadBlossom(finalUri, compressed.contentType, compressed.size, alt, contentWarningReason, baseUrl, null, null, account, forcedSigner, context, auth) - } - } - } finally { - deleteTempUri(finalUri, uri) - } - } - - suspend fun uploadEncrypted( - uri: Uri, - mimeType: String?, - alt: String?, - contentWarningReason: String?, - compressionQuality: CompressorQuality, - encrypt: NostrCipher, - server: ServerName, - account: Account, - context: Context, - useH265: Boolean = false, - stripMetadata: Boolean = true, - onStrippingFailed: suspend () -> Boolean = { true }, - convertGifToMp4: Boolean = false, - forcedSigner: NostrSigner? = null, - ): UploadingFinalState { - val compressed = compressIfNeeded(uri, mimeType, compressionQuality, context, useH265, convertGifToMp4) - - val finalUri = - try { - stripAfterCompression(uri, compressed, mimeType, compressionQuality, stripMetadata, onStrippingFailed, context) - } catch (e: AvifMetadataNotVerifiableException) { - return error(Res.string.avif_metadata_strip_failed, e.message ?: e.javaClass.simpleName).also { - deleteTempUri(compressed.uri, uri) - } - } ?: return error(Res.string.upload_cancelled).also { - deleteTempUri(compressed.uri, uri) - } - - if (compressed.uri != finalUri) deleteTempUri(compressed.uri, uri) - - val encrypted = EncryptFiles().encryptFile(context, finalUri, encrypt) - deleteTempUri(finalUri, uri) - - try { - return when (server.type) { - ServerType.NIP95 -> uploadNIP95(encrypted.uri, encrypted.contentType, compressed.contentType, encrypted.originalHash, context) - ServerType.NIP96 -> uploadNIP96(encrypted.uri, encrypted.contentType, encrypted.size, alt, contentWarningReason, server.baseUrl, compressed.contentType, encrypted.originalHash, account, forcedSigner, context) - // The same encrypted file goes to every server tried, so its key, nonce and - // hash stay valid whichever one ends up holding it. - ServerType.Blossom -> - uploadBlossomWithFallback(server, account) { baseUrl, auth -> - uploadBlossom(encrypted.uri, encrypted.contentType, encrypted.size, alt, contentWarningReason, baseUrl, compressed.contentType, encrypted.originalHash, account, forcedSigner, context, auth) - } - } - } finally { - deleteTempUri(encrypted.uri, uri) - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt index 8b9a7ef3c0..615e23e5a9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt @@ -33,6 +33,8 @@ import com.davotoula.lightcompressor.VideoCompressor import com.davotoula.lightcompressor.config.AppSpecificStorageConfiguration import com.davotoula.lightcompressor.config.Configuration import com.davotoula.lightcompressor.config.VideoResizer +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaCompressorResult import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.LogLevel import kotlinx.coroutines.suspendCancellableCoroutine diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index bf256fd3e1..0cee50de61 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -59,7 +59,6 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextDirection @@ -122,8 +121,6 @@ fun EditPostView( val postViewModel: EditPostViewModel = viewModel() postViewModel.init(accountViewModel) - val context = LocalContext.current - val scrollState = rememberScrollState() val scope = rememberCoroutineScope() @@ -280,7 +277,7 @@ fun EditPostView( accountViewModel.account.settings.defaultFileServer, isUploading = postViewModel.mediaUploadTracker.isUploading, onAdd = { alt, server, sensitiveContent, mediaQuality, _, stripMetadata, _ -> - postViewModel.upload(alt, sensitiveContent, mediaQuality, false, server, accountViewModel.toastManager::toast, context, stripMetadata) + postViewModel.upload(alt, sensitiveContent, mediaQuality, false, server, accountViewModel.toastManager::toast, accountViewModel.host.mediaUploader, stripMetadata) accountViewModel.account.settings.changeDefaultFileServer(server) }, onDelete = postViewModel::deleteMediaToUpload, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt index bd80fe5d5d..68377fd17c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.actions -import android.content.Context import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.runtime.Stable @@ -41,17 +40,19 @@ import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.commons.service.upload.MediaUploadTracker import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +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.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.onUiThread +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -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.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent @@ -170,11 +171,11 @@ open class EditPostViewModel : ViewModel() { isPrivate: Boolean = false, server: ServerName, onError: (String, String) -> Unit, - context: Context, + uploader: MediaUploader, stripMetadata: Boolean = true, ) { try { - uploadUnsafe(alt, sensitiveContent, mediaQuality, isPrivate, server, onError, context, stripMetadata) + uploadUnsafe(alt, sensitiveContent, mediaQuality, isPrivate, server, onError, uploader, stripMetadata) } catch (e: SignerExceptions.ReadOnlyException) { viewModelScope.launch { onError( @@ -192,7 +193,7 @@ open class EditPostViewModel : ViewModel() { isPrivate: Boolean = false, server: ServerName, onError: (String, String) -> Unit, - context: Context, + uploader: MediaUploader, stripMetadata: Boolean = true, ) { accountViewModel.launchSigner { @@ -206,10 +207,10 @@ open class EditPostViewModel : ViewModel() { myMultiOrchestrator.upload( alt, if (sensitiveContent) "" else null, - MediaCompressor.intToCompressorQuality(mediaQuality), + CompressorQuality.fromSlider(mediaQuality), server, myAccount, - context, + uploader, useH265Codec, stripMetadata, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, @@ -218,11 +219,12 @@ open class EditPostViewModel : ViewModel() { if (results.allGood) { val urls = results.successful.mapNotNull { state -> - if (state.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { + val uploaded = state.result + if (uploaded is UploadOrchestrator.OrchestratorResult.NIP95Result) { val nip95 = myAccount.createNip95( - byteArray = state.result.bytes, - headerInfo = state.result.fileHeader, + byteArray = uploaded.bytes, + headerInfo = uploaded.fileHeader, alt = alt, contentWarningReason = if (sensitiveContent) "" else null, ) @@ -232,29 +234,29 @@ open class EditPostViewModel : ViewModel() { note?.let { "nostr:" + it.toNEvent() } - } else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { + } else if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) { val iMeta = - IMetaTagBuilder(state.result.url) + IMetaTagBuilder(uploaded.url) .apply { - hash(state.result.fileHeader.hash) - size(state.result.fileHeader.size) - state.result.fileHeader.mimeType + hash(uploaded.fileHeader.hash) + size(uploaded.fileHeader.size) + uploaded.fileHeader.mimeType ?.let { mimeType(it) } - state.result.fileHeader.dim + uploaded.fileHeader.dim ?.let { dims(it) } - state.result.fileHeader.blurHash + uploaded.fileHeader.blurHash ?.let { blurhash(it.blurhash) } - state.result.fileHeader.thumbHash + uploaded.fileHeader.thumbHash ?.let { thumbhash(it.thumbhash) } - state.result.magnet?.let { magnet(it) } - state.result.uploadedHash?.let { originalHash(it) } + uploaded.magnet?.let { magnet(it) } + uploaded.uploadedHash?.let { originalHash(it) } alt?.let { alt(it) } if (sensitiveContent) sensitiveContent("") }.build() iMetaAttachments = iMetaAttachments.filter { it.url != iMeta.url } + iMeta - state.result.url + uploaded.url } else { null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt index 530db071bf..9ee3618759 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.actions -import android.content.Context import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -38,13 +37,15 @@ import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_ import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +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.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -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.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers @@ -100,13 +101,13 @@ open class NewMediaModel : ViewModel() { ): Boolean = mimeType?.startsWith("image/") == true || RichTextParser.isImageUrl(url) fun upload( - context: Context, + uploader: MediaUploader, accountViewModel: AccountViewModel, onSucess: () -> Unit, onError: (String, String) -> Unit, ) { try { - uploadUnsafe(context, accountViewModel, onSucess, onError) + uploadUnsafe(uploader, accountViewModel, onSucess, onError) } catch (e: SignerExceptions.ReadOnlyException) { viewModelScope.launch { onError( @@ -118,7 +119,7 @@ open class NewMediaModel : ViewModel() { } fun uploadUnsafe( - context: Context, + uploader: MediaUploader, accountViewModel: AccountViewModel, onSucess: () -> Unit, onError: (String, String) -> Unit, @@ -135,10 +136,10 @@ open class NewMediaModel : ViewModel() { myMultiOrchestrator.upload( caption, if (sensitiveContent) "" else null, - MediaCompressor.intToCompressorQuality(mediaQualitySlider), + CompressorQuality.fromSlider(mediaQualitySlider), serverToUse, myAccount, - context, + uploader, useH265Codec, stripMetadata, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt index 3142596c5c..24e986c84b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt @@ -46,7 +46,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -72,7 +71,7 @@ import com.vitorpamplona.amethyst.commons.resources.strip_metadata_label import com.vitorpamplona.amethyst.commons.resources.video_codec_h265_description import com.vitorpamplona.amethyst.commons.resources.video_codec_h265_label import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.components.SetDialogToEdgeToEdge import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer @@ -101,7 +100,6 @@ fun NewMediaView( initialCaption: String = "", ) { val account = accountViewModel.account - val context = LocalContext.current val scrollState = rememberScrollState() @@ -130,7 +128,7 @@ fun NewMediaView( onClose() }, onPost = { - postViewModel.upload(context, accountViewModel, onClose, accountViewModel.toastManager::toast) + postViewModel.upload(accountViewModel.host.mediaUploader, accountViewModel, onClose, accountViewModel.toastManager::toast) postViewModel.selectedServer?.let { account.settings.changeDefaultFileServer(it) } 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 54fade5061..31c146955c 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 @@ -33,11 +33,11 @@ import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_de 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.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MetadataStripper import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromFiles.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromFiles.kt index 58f1acc707..425c25f84e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromFiles.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromFiles.kt @@ -39,7 +39,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.upload_file -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation import com.vitorpamplona.amethyst.commons.ui.stringRes import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SharedMediaResolver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SharedMediaResolver.kt index a7da527781..465afd5ee8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SharedMediaResolver.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SharedMediaResolver.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.actions.uploads import android.content.Context import androidx.core.net.toUri -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/ShowImageUploadItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/ShowImageUploadItem.kt index ddab643263..f72973fde9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/ShowImageUploadItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/ShowImageUploadItem.kt @@ -78,18 +78,20 @@ import com.vitorpamplona.amethyst.commons.resources.uploading_state_hashing import com.vitorpamplona.amethyst.commons.resources.uploading_state_ready import com.vitorpamplona.amethyst.commons.resources.uploading_state_server_processing import com.vitorpamplona.amethyst.commons.resources.uploading_state_uploading -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +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.components.AutoNonlazyGrid import com.vitorpamplona.amethyst.commons.ui.note.CloseIcon import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size40Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.playback.composable.VideoView -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/TakePicture.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/TakePicture.kt index 151d93b0b3..ecade0bf8c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/TakePicture.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/TakePicture.kt @@ -52,7 +52,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.no_camera_app_found import com.vitorpamplona.amethyst.commons.resources.record_a_video import com.vitorpamplona.amethyst.commons.resources.take_a_picture -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/UploadProgressIndicator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/UploadProgressIndicator.kt index 279d636de8..93a48052a0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/UploadProgressIndicator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/UploadProgressIndicator.kt @@ -49,10 +49,11 @@ import com.vitorpamplona.amethyst.commons.resources.uploading_state_hashing import com.vitorpamplona.amethyst.commons.resources.uploading_state_ready import com.vitorpamplona.amethyst.commons.resources.uploading_state_server_processing import com.vitorpamplona.amethyst.commons.resources.uploading_state_uploading +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadingState +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource @Composable fun UploadProgressIndicator( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ThinPaddingTextField.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ThinPaddingTextField.kt index 4316aa294a..00bd0ac935 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ThinPaddingTextField.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ThinPaddingTextField.kt @@ -53,7 +53,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.MediaUri +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUri import com.vitorpamplona.amethyst.commons.ui.components.imageContentReceiver import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt index edd9282f5b..d03fc98f99 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt @@ -90,6 +90,8 @@ import com.vitorpamplona.amethyst.commons.resources.strip_metadata_description import com.vitorpamplona.amethyst.commons.resources.strip_metadata_label import com.vitorpamplona.amethyst.commons.resources.video_codec_h265_description import com.vitorpamplona.amethyst.commons.resources.video_codec_h265_label +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer import com.vitorpamplona.amethyst.commons.ui.note.CancelIcon @@ -102,8 +104,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.subtleBorder import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.ai.MLKitImageLabelService -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery import kotlinx.collections.immutable.toImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index 43ff22bf9c..bb649a217a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.note.nip22Comments -import android.content.Context import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd @@ -55,7 +54,12 @@ import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.service.pow.PoWReplay import com.vitorpamplona.amethyst.commons.service.upload.MediaUploadTracker import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +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.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.notify.IAudience @@ -67,12 +71,9 @@ 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.text.setTextAndPlaceCursorAtBeginning +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.location.LocationState -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.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor @@ -787,11 +788,11 @@ open class CommentPostViewModel : mediaQuality: Int, server: ServerName, onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, stripMetadata: Boolean = true, ) { try { - uploadUnsafe(alt, contentWarningReason, mediaQuality, server, onError, context, stripMetadata) + uploadUnsafe(alt, contentWarningReason, mediaQuality, server, onError, uploader, stripMetadata) } catch (_: SignerExceptions.ReadOnlyException) { viewModelScope.launch { onError( @@ -808,7 +809,7 @@ open class CommentPostViewModel : mediaQuality: Int, server: ServerName, onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, stripMetadata: Boolean = true, ) { viewModelScope.launch(Dispatchers.IO) { @@ -820,10 +821,10 @@ open class CommentPostViewModel : myMultiOrchestrator.upload( alt, contentWarningReason, - MediaCompressor.intToCompressorQuality(mediaQuality), + CompressorQuality.fromSlider(mediaQuality), server, account, - context, + uploader, stripMetadata = stripMetadata, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, forcedSigner = if (wantsAnonymousPost) anonymousSigner() else null, @@ -832,30 +833,31 @@ open class CommentPostViewModel : if (results.allGood) { val urls = results.successful.mapNotNull { state -> - if (state.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { - val nip95 = account.createNip95(state.result.bytes, headerInfo = state.result.fileHeader, alt, contentWarningReason) + val uploaded = state.result + if (uploaded is UploadOrchestrator.OrchestratorResult.NIP95Result) { + val nip95 = account.createNip95(uploaded.bytes, headerInfo = uploaded.fileHeader, alt, contentWarningReason) nip95attachments = nip95attachments + nip95 val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) } note?.let { "nostr:" + it.toNEvent() } - } else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { + } else if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) { val iMeta = - IMetaTagBuilder(state.result.url) + IMetaTagBuilder(uploaded.url) .apply { - hash(state.result.fileHeader.hash) - size(state.result.fileHeader.size) - state.result.fileHeader.mimeType + hash(uploaded.fileHeader.hash) + size(uploaded.fileHeader.size) + uploaded.fileHeader.mimeType ?.let { mimeType(it) } - state.result.fileHeader.dim + uploaded.fileHeader.dim ?.let { dims(it) } - state.result.fileHeader.blurHash + uploaded.fileHeader.blurHash ?.let { blurhash(it.blurhash) } - state.result.fileHeader.thumbHash + uploaded.fileHeader.thumbHash ?.let { thumbhash(it.thumbhash) } - state.result.magnet?.let { magnet(it) } - state.result.uploadedHash?.let { originalHash(it) } + uploaded.magnet?.let { magnet(it) } + uploaded.uploadedHash?.let { originalHash(it) } alt?.let { alt(it) } contentWarningReason?.let { sensitiveContent(contentWarningReason) } @@ -863,7 +865,7 @@ open class CommentPostViewModel : iMetaAttachments.replace(iMeta.url, iMeta) - state.result.url + uploaded.url } else { null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt index 963b9a7e93..e27d53befe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt @@ -78,8 +78,8 @@ import com.vitorpamplona.amethyst.commons.resources.post_anonymously import com.vitorpamplona.amethyst.commons.resources.what_s_on_your_mind import com.vitorpamplona.amethyst.commons.resources.zapraiser import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar @@ -472,7 +472,7 @@ private fun GenericCommentPostBody( accountViewModel.account.settings.defaultFileServer, isUploading = postViewModel.mediaUploadTracker.isUploading, onAdd = { alt, server, sensitiveContent, mediaQuality, _, stripMetadata, _ -> - postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context, stripMetadata) + postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, accountViewModel.host.mediaUploader, stripMetadata) accountViewModel.account.settings.changeDefaultFileServer(server) }, onDelete = postViewModel::deleteMediaToUpload, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsImageScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsImageScreen.kt index 74a9cee072..a218c7d509 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsImageScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsImageScreen.kt @@ -90,6 +90,9 @@ import com.vitorpamplona.amethyst.commons.resources.share_as_image import com.vitorpamplona.amethyst.commons.resources.share_as_image_generating import com.vitorpamplona.amethyst.commons.resources.share_as_image_url import com.vitorpamplona.amethyst.commons.resources.share_as_image_watermark +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState import com.vitorpamplona.amethyst.commons.ui.components.LoadNote import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer @@ -98,10 +101,8 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackBu import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size18Modifier +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -268,7 +269,7 @@ fun ShareNoteAsImageScreen( compressionQuality = CompressorQuality.UNCOMPRESSED, server = server, account = account, - context = context, + uploader = accountViewModel.host.mediaUploader, stripMetadata = false, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModelAndroidActions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModelAndroidActions.kt index 0fcc03aaa3..d4d9d0c2c5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModelAndroidActions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModelAndroidActions.kt @@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.failed_to_save_the_video import com.vitorpamplona.amethyst.commons.resources.video_saved_to_the_gallery import com.vitorpamplona.amethyst.commons.service.isVideoOnline +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader import com.vitorpamplona.amethyst.commons.ui.components.UrlPreviewState import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel @@ -65,8 +66,8 @@ suspend fun AccountViewModel.checkVideoIsOnline(videoUrl: String): Boolean = htt suspend fun AccountViewModel.uploadMarmotGroupIcon( uri: Uri, mimeType: String?, - context: Context, -): MarmotGroupIconUpload = MarmotGroupIconUploader(account).upload(uri, mimeType, account.settings.defaultFileServer, context) + uploader: MediaUploader, +): MarmotGroupIconUpload = MarmotGroupIconUploader(account).upload(uri, mimeType, account.settings.defaultFileServer, uploader) fun AccountViewModel.saveMediaToGallery( videoUri: String?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt index 310308d505..3f7a1d7466 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt @@ -28,10 +28,12 @@ import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.commons.service.lnurl.LnurlHttpTransport import com.vitorpamplona.amethyst.commons.service.lnurl.OkHttpLnurlTransport import com.vitorpamplona.amethyst.commons.service.pow.PoWJobFailure +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader import com.vitorpamplona.amethyst.commons.tor.MoneyOpRelayRouting import com.vitorpamplona.amethyst.commons.tor.TorRelayEvaluation import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModelHost import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.dismissNotificationForEvent +import com.vitorpamplona.amethyst.service.uploads.AndroidMediaUploader import com.vitorpamplona.amethyst.ui.note.payViaIntent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey @@ -70,6 +72,8 @@ class AndroidAccountViewModelHost( override val relayAuthState: StateFlow> get() = modules.authCoordinator.receiver.authStateFlow + override val mediaUploader: MediaUploader by lazy { AndroidMediaUploader(modules.appContext) } + override val savedAccounts: Flow> = LocalPreferences .accountsFlow() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeDialog.kt index 67f73fb063..dd2e9abb71 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeDialog.kt @@ -54,7 +54,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -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 @@ -108,7 +107,6 @@ fun NewBadgeDialog( accountViewModel: AccountViewModel, ) { val account = accountViewModel.account - val context = LocalContext.current val scrollState = rememberScrollState() @@ -152,7 +150,7 @@ fun NewBadgeDialog( }, onPost = { postViewModel.upload( - context, + accountViewModel.host.mediaUploader, onSuccess = onClose, onError = accountViewModel.toastManager::toast, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeModel.kt index b13cef87f8..5bfdc7d5c0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeModel.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.post -import android.content.Context import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -36,11 +35,13 @@ import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_de 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.service.upload.SuspendableConfirmation -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.ui.loadStringRes -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.commons.ui.uploads.errorResource import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip58Badges.definition.tags.ThumbTag import kotlinx.collections.immutable.ImmutableList @@ -110,12 +111,12 @@ class NewBadgeModel : ViewModel() { name.isNotBlank() fun upload( - context: Context, + uploader: MediaUploader, onSuccess: () -> Unit, onError: (String, String) -> Unit, ) { try { - uploadUnsafe(context, onSuccess, onError) + uploadUnsafe(uploader, onSuccess, onError) } catch (e: SignerExceptions.ReadOnlyException) { viewModelScope.launch { onError( @@ -127,7 +128,7 @@ class NewBadgeModel : ViewModel() { } private fun uploadUnsafe( - context: Context, + uploader: MediaUploader, onSuccess: () -> Unit, onError: (String, String) -> Unit, ) { @@ -142,10 +143,10 @@ class NewBadgeModel : ViewModel() { orch.upload( alt = name, contentWarningReason = null, - mediaQuality = MediaCompressor.intToCompressorQuality(mediaQualitySlider), + mediaQuality = CompressorQuality.fromSlider(mediaQualitySlider), server = serverToUse, account = myAccount, - context = context, + uploader = uploader, useH265 = false, stripMetadata = stripMetadata, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt index bca0a57abe..ca9386f767 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt @@ -39,11 +39,11 @@ import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_ 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.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MetadataStripper import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarEventScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarEventScreen.kt index 450b025c40..8a8c97f679 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarEventScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarEventScreen.kt @@ -275,7 +275,7 @@ private fun ImageRow( if (uri == null) return@rememberLauncherForActivityResult val mime = context.contentResolver.getType(uri) scope.launch { - val ok = vm.uploadAndSetImage(uri, mime, context) + val ok = vm.uploadAndSetImage(uri, mime, accountViewModel.host.mediaUploader) if (!ok) { accountViewModel.toastManager.toast( Res.string.calendar_event_image_upload_failed, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarEventViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarEventViewModel.kt index b8f4d1fe12..0467a042a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarEventViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarEventViewModel.kt @@ -20,17 +20,17 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.create -import android.content.Context import android.net.Uri import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip52Calendar.parseIsoDateToUnixSeconds +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags import com.vitorpamplona.quartz.nip01Core.tags.people.PTag @@ -157,7 +157,7 @@ class NewCalendarEventViewModel : ViewModel() { suspend fun uploadAndSetImage( uri: Uri, mimeType: String?, - context: Context, + uploader: MediaUploader, ): Boolean { if (!::account.isInitialized) return false isUploadingImage.value = true @@ -172,7 +172,7 @@ class NewCalendarEventViewModel : ViewModel() { compressionQuality = CompressorQuality.MEDIUM, server = server, account = account, - context = context, + uploader = uploader, ) val serverResult = (result as? UploadingState.Finished)?.result as? UploadOrchestrator.OrchestratorResult.ServerResult diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt index 19114bc47b..d6cd50aaa4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt @@ -103,7 +103,9 @@ import com.vitorpamplona.amethyst.commons.resources.cordn_voice_record import com.vitorpamplona.amethyst.commons.resources.cordn_voice_stop import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlVideo -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +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.components.EmptyState import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.loadStringRes @@ -117,7 +119,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.model.cordn.CordnMediaService -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MetadataStripper import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessageRecorder @@ -141,6 +142,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.io.File /** * One cordn room. @@ -518,7 +520,7 @@ private fun CordnGroupChat( attaching = true attachError = null try { - sendAttachment(context, accountViewModel, room, uploadState) + sendAttachment(context, accountViewModel.host.mediaUploader, accountViewModel, room, uploadState) // Only on success: a failed upload leaves the dialog up // with what you picked still in it, so retrying is one // tap rather than the picker again. @@ -1006,6 +1008,7 @@ private class CordnAttachmentException( */ private suspend fun sendAttachment( context: Context, + uploader: MediaUploader, accountViewModel: AccountViewModel, room: CordnGroupChatroom, state: ChatFileUploadState, @@ -1048,8 +1051,8 @@ private suspend fun sendAttachment( item.orchestrator.compressIfNeeded( uri = uri, mimeType = declaredMime, - compressionQuality = MediaCompressor.intToCompressorQuality(state.mediaQualitySlider), - context = context, + compressionQuality = CompressorQuality.fromSlider(state.mediaQualitySlider), + uploader = uploader, ) } val mime = compressed.contentType ?: declaredMime @@ -1092,8 +1095,8 @@ private suspend fun sendAttachment( // copies of a message that is end-to-end encrypted everywhere else — the // same reason the voice path deletes its recording. Both calls no-op on the // user's own file. - item.orchestrator.deleteTempUri(finalUri, uri) - item.orchestrator.deleteTempUri(compressed.uri, uri) + deleteTempFile(finalUri, uri) + deleteTempFile(compressed.uri, uri) } } finally { // Always, not just on success: a failure leaves the dialog up to retry from, @@ -1331,3 +1334,13 @@ private suspend fun sendVoiceNote( withContext(Dispatchers.IO) { recording.file.delete() } } } + +/** Deletes a pipeline temp file; a no-op on the user's own file, which is [originalUri]. */ +private fun deleteTempFile( + tempUri: Uri, + originalUri: Uri, +) { + if (tempUri == originalUri) return + val path = tempUri.path ?: return + if (!File(path).delete()) Log.w("CordnGroupChat") { "Could not delete temp file $path" } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt index 964a47bc57..6b7216fa01 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt @@ -59,7 +59,7 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_keypackage_relays_not import com.vitorpamplona.amethyst.commons.resources.marmot_keypackage_relays_not_set_title import com.vitorpamplona.amethyst.commons.resources.marmot_skip_for_now import com.vitorpamplona.amethyst.commons.resources.marmot_use_outbox_relays -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -122,7 +122,7 @@ fun CreateGroupScreen( val iconChange = pickedIcon?.let { media -> MarmotGroupIconChange.Set( - accountViewModel.uploadMarmotGroupIcon(media.uri, media.mimeType, context), + accountViewModel.uploadMarmotGroupIcon(media.uri, media.mimeType, accountViewModel.host.mediaUploader), ) } ?: MarmotGroupIconChange.Keep // Always commit an initial metadata extension so that diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/EditGroupInfoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/EditGroupInfoScreen.kt index 27df1823cd..40e271da12 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/EditGroupInfoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/EditGroupInfoScreen.kt @@ -57,7 +57,7 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_group_name import com.vitorpamplona.amethyst.commons.resources.marmot_group_name_placeholder import com.vitorpamplona.amethyst.commons.resources.marmot_legacy_group_no_avatar_url import com.vitorpamplona.amethyst.commons.resources.save -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -114,7 +114,7 @@ fun EditGroupInfoScreen( val iconChange = pickedIcon?.let { media -> MarmotGroupIconChange.Set( - accountViewModel.uploadMarmotGroupIcon(media.uri, media.mimeType, context), + accountViewModel.uploadMarmotGroupIcon(media.uri, media.mimeType, accountViewModel.host.mediaUploader), ) } ?: if (removeIcon) MarmotGroupIconChange.Clear else MarmotGroupIconChange.Keep accountViewModel.updateMarmotGroupMetadata( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt index f2d5fbc593..b902700071 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt @@ -67,8 +67,8 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_out_of_sync_confirm_b import com.vitorpamplona.amethyst.commons.resources.marmot_out_of_sync_confirm_title import com.vitorpamplona.amethyst.commons.resources.marmot_out_of_sync_reset import com.vitorpamplona.amethyst.commons.resources.reply_here +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -389,6 +389,7 @@ private fun MarmotGroupFileUploadDialog( } }, context = context, + uploader = accountViewModel.host.mediaUploader, onceUploaded = { uploads -> MarmotFileSender(nostrGroupId, accountViewModel).send(uploads) onUpload() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.kt index 9aba7e30a3..bb320241ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.kt @@ -44,7 +44,7 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_add_photo import com.vitorpamplona.amethyst.commons.resources.marmot_change_photo import com.vitorpamplona.amethyst.commons.resources.marmot_group_icon import com.vitorpamplona.amethyst.commons.resources.marmot_remove_photo -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileUploader.kt index 3ac1b82a62..004bfa0a64 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileUploader.kt @@ -25,10 +25,12 @@ import android.net.Uri 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.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +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.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadingState +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.quartz.marmot.appComponents.EncryptedMediaPolicyV2 import com.vitorpamplona.quartz.marmot.appComponents.EncryptedMediaReferenceV2 @@ -78,6 +80,7 @@ class MarmotFileUploader( exporterSecret: ByteArray, onError: (title: String, message: String) -> Unit, context: Context, + uploader: MediaUploader, /** * Produce `encrypted-media-v2` references instead of MIP-04 ones. * Decided by the group's policy component, not by the uploader. @@ -88,7 +91,7 @@ class MarmotFileUploader( viewState.mediaUploadTracker.startUpload(multiOrchestrator.hasNonMedia()) val results = mutableListOf() - val quality = MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider) + val quality = CompressorQuality.fromSlider(viewState.mediaQualitySlider) val count = multiOrchestrator.size() for (i in 0 until count) { @@ -130,13 +133,13 @@ class MarmotFileUploader( encrypt = cipher, server = viewState.selectedServer, account = account, - context = context, + uploader = uploader, stripMetadata = viewState.stripMetadata, ) val state = item.orchestrator.progressState.value - if (state is UploadingState.Finished && state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { - val serverResult = state.result + val serverResult = (state as? UploadingState.Finished)?.result as? UploadOrchestrator.OrchestratorResult.ServerResult + if (serverResult != null) { // The reference is built from what the cipher recorded while // encrypting the bytes the pipeline actually uploaded — after // compression and metadata stripping — because that is what the diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotGroupIconUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotGroupIconUploader.kt index c96b1a3cf2..39004e420c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotGroupIconUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotGroupIconUploader.kt @@ -20,16 +20,16 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send -import android.content.Context import android.net.Uri import com.vitorpamplona.amethyst.commons.marmot.MarmotGroupIconUpload import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +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.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadingState +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupImageCipher import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupImageEncryption import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair @@ -53,13 +53,13 @@ class MarmotGroupIconUploader( uri: Uri, mimeType: String?, server: ServerName, - context: Context, + uploader: MediaUploader, ): MarmotGroupIconUpload { // Compress/downscale up front — avatars don't need full resolution, and a smaller // blob is cheaper for every member to fetch. The MIP-01 crypto uses no AAD and does // not bind the MIME type, so the (possibly transcoded) output type is irrelevant to // decryption; we just hand the compressed bytes to the encrypting uploader. - val compressed = MediaCompressor().compress(uri, mimeType, CompressorQuality.MEDIUM, context.applicationContext) + val compressed = UploadOrchestrator().compressIfNeeded(uri, mimeType, CompressorQuality.MEDIUM, uploader) val uploadMime = compressed.contentType ?: mimeType ?: DEFAULT_MIME val cipher = MarmotGroupImageCipher.forNewImage() val uploadKeySeed = MarmotGroupImageEncryption.generateUploadKey() @@ -76,13 +76,13 @@ class MarmotGroupIconUploader( encrypt = cipher, server = server, account = account, - context = context, + uploader = uploader, stripMetadata = true, forcedSigner = uploadSigner, ) - if (state is UploadingState.Finished && state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { - val serverResult = state.result + val serverResult = (state as? UploadingState.Finished)?.result as? UploadOrchestrator.OrchestratorResult.ServerResult + if (serverResult != null) { val hash = serverResult.uploadedHash ?: throw IllegalStateException("Blossom server did not return a content hash for the group icon") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotNewMessageViewModel.kt index feac05bcb6..edc31b853a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotNewMessageViewModel.kt @@ -33,7 +33,7 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.latestMarmotEdit import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupChatroom -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.onUiThread diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt index 21b6cfc90e..f54529cd63 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt @@ -67,8 +67,8 @@ import com.vitorpamplona.amethyst.commons.resources.chat_minichat_title import com.vitorpamplona.amethyst.commons.resources.chat_send_image_title import com.vitorpamplona.amethyst.commons.resources.concord_open_channel import com.vitorpamplona.amethyst.commons.resources.reply_here +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelSubscription @@ -240,7 +240,7 @@ fun MinichatScreen( onEncryptedUploadError = { title, message -> launch(Dispatchers.Main) { Toast.makeText(context, "$title: $message", Toast.LENGTH_LONG).show() } }, - context = context, + uploader = accountViewModel.host.mediaUploader, onceUploaded = { uploads -> val caption = uploadState.caption val imetas = if (isConcord) uploads.mapNotNull { it.toConcordImeta() } else uploads.toPlainImetas() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index 19ff582753..783f35dacd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send -import android.content.Context import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd @@ -48,7 +47,8 @@ import com.vitorpamplona.amethyst.commons.model.location.LocationResult import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.IZapField @@ -473,7 +473,7 @@ class ChatNewMessageViewModel : fun uploadAndHold( onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, onceUploaded: () -> Unit, ) { val uploadState = uploadState ?: return @@ -487,7 +487,7 @@ class ChatNewMessageViewModel : encryptedUploadErrorMessage = message pendingRetryMode = RetryMode.HOLD }, - context, + uploader, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, ) { uploadsWaitingToBeSent += it @@ -499,7 +499,7 @@ class ChatNewMessageViewModel : fun uploadAndSend( onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, onceUploaded: () -> Unit, ) { val room = room.value ?: return @@ -514,10 +514,10 @@ class ChatNewMessageViewModel : encryptedUploadErrorMessage = message pendingRetryMode = RetryMode.SEND pendingRetryOnError = onError - pendingRetryContext = context + pendingRetryUploader = uploader pendingRetryOnceUploaded = onceUploaded }, - context, + uploader, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, ) { ChatFileSender(room, account).sendNIP17(it) @@ -532,7 +532,7 @@ class ChatNewMessageViewModel : var encryptedUploadErrorMessage by mutableStateOf(null) var pendingRetryMode by mutableStateOf(null) var pendingRetryOnError by mutableStateOf<((String, String) -> Unit)?>(null) - var pendingRetryContext by mutableStateOf(null) + var pendingRetryUploader by mutableStateOf(null) var pendingRetryOnceUploaded by mutableStateOf<(() -> Unit)?>(null) enum class RetryMode { HOLD, SEND } @@ -542,21 +542,21 @@ class ChatNewMessageViewModel : encryptedUploadErrorMessage = null pendingRetryMode = null pendingRetryOnError = null - pendingRetryContext = null + pendingRetryUploader = null pendingRetryOnceUploaded = null } fun retryWithoutEncryption() { val mode = pendingRetryMode ?: return val onError = pendingRetryOnError - val context = pendingRetryContext + val uploader = pendingRetryUploader val onceUploaded = pendingRetryOnceUploaded val room = room.value val uploadState = uploadState dismissEncryptedUploadError() - if (room == null || uploadState == null || context == null) return + if (room == null || uploadState == null || uploader == null) return uploadState.encryptFiles = false @@ -566,7 +566,7 @@ class ChatNewMessageViewModel : ChatFileUploader(account).justUploadNIP17Unencrypted( uploadState, onError ?: accountViewModel.toastManager::toast, - context, + uploader, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, ) { uploadsWaitingToBeSent += it @@ -579,7 +579,7 @@ class ChatNewMessageViewModel : ChatFileUploader(account).justUploadNIP17Unencrypted( uploadState, onError ?: accountViewModel.toastManager::toast, - context, + uploader, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, ) { ChatFileSender(room, account).sendNIP17(it) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/IMetaAttachments.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/IMetaAttachments.kt index 8497a8b240..e04b6ce484 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/IMetaAttachments.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/IMetaAttachments.kt @@ -25,7 +25,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.vitorpamplona.amethyst.commons.service.upload.FileHeader -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.service.uploads.prepare import com.vitorpamplona.quartz.nip92IMeta.IMetaTag import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt index e54a13f17e..3622a98c27 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt @@ -87,8 +87,8 @@ import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -308,7 +308,7 @@ fun GroupDMScreenContent( onAdd = { _, server, _, _, _, _, _ -> postViewModel.uploadAndHold( accountViewModel.toastManager::toast, - context, + accountViewModel.host.mediaUploader, onceUploaded = { }, ) accountViewModel.account.settings.changeDefaultFileServer(server) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt index 3ca6a4270d..b8f96867f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt @@ -42,7 +42,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -62,8 +61,8 @@ import com.vitorpamplona.amethyst.commons.resources.retry_without_encryption import com.vitorpamplona.amethyst.commons.resources.this_message_will_disappear_in import com.vitorpamplona.amethyst.commons.resources.upload_without_encryption_warning import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -187,7 +186,6 @@ fun PrivateMessageEditFieldRow( if (channelScreenModel.wantsExpirationDate) { Row(Modifier.fillMaxWidth().padding(vertical = 2.dp), horizontalArrangement = Arrangement.Center) { - val context = LocalContext.current Text( stringRes( Res.string.this_message_will_disappear_in, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt index c34f038e26..ee873c8af1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.uploa import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.quartz.nip01Core.tags.references.references import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt index 14cebf331d..97bacf2bca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt @@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable 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.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -49,7 +48,6 @@ fun RoomChatFileUploadDialog( ) { val roomState = channelScreenModel.room.collectAsStateWithLifecycle() val room = roomState.value ?: return - val context = LocalContext.current ChatFileUploadDialog( state, @@ -67,7 +65,7 @@ fun RoomChatFileUploadDialog( upload = { channelScreenModel.uploadAndSend( onError = accountViewModel.toastManager::toast, - context = context, + uploader = accountViewModel.host.mediaUploader, onceUploaded = onUpload, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt index 98eaf8e91d..65926bac51 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt @@ -20,16 +20,17 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload -import android.content.Context import com.vitorpamplona.amethyst.commons.model.Account 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_encrypted_media_message import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_encrypted_media_title import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.ui.loadStringRes -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.quartz.utils.ciphers.AESGCM @@ -44,7 +45,7 @@ class ChatFileUploader( viewState: ChatFileUploadState, onError: (title: String, message: String) -> Unit, onEncryptedUploadError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, onStrippingFailed: suspend () -> Boolean = { true }, onceUploaded: suspend (List) -> Unit, ) { @@ -58,11 +59,11 @@ class ChatFileUploader( orchestrator.uploadEncrypted( viewState.caption, viewState.contentWarningReason, - MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider), + CompressorQuality.fromSlider(viewState.mediaQualitySlider), cipher, viewState.selectedServer, account, - context, + uploader, stripMetadata = viewState.stripMetadata, onStrippingFailed = onStrippingFailed, ) @@ -70,8 +71,9 @@ class ChatFileUploader( if (results.allGood) { val list = results.successful.mapNotNull { state -> - if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { - SuccessfulUploads(state.result, viewState.caption, viewState.contentWarningReason, cipher) + val uploaded = state.result + if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) { + SuccessfulUploads(uploaded, viewState.caption, viewState.contentWarningReason, cipher) } else { null } @@ -97,7 +99,7 @@ class ChatFileUploader( } } } else { - justUploadNIP17Unencrypted(viewState, onError, context, onStrippingFailed, onceUploaded) + justUploadNIP17Unencrypted(viewState, onError, uploader, onStrippingFailed, onceUploaded) } viewState.mediaUploadTracker.finishUpload() @@ -106,7 +108,7 @@ class ChatFileUploader( suspend fun justUploadNIP17Unencrypted( viewState: ChatFileUploadState, onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, onStrippingFailed: suspend () -> Boolean = { true }, onceUploaded: suspend (List) -> Unit, ) { @@ -117,10 +119,10 @@ class ChatFileUploader( orchestrator.upload( viewState.caption, viewState.contentWarningReason, - MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider), + CompressorQuality.fromSlider(viewState.mediaQualitySlider), viewState.selectedServer, account, - context, + uploader, stripMetadata = viewState.stripMetadata, onStrippingFailed = onStrippingFailed, ) @@ -128,8 +130,9 @@ class ChatFileUploader( if (results.allGood) { val list = results.successful.mapNotNull { state -> - if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { - SuccessfulUploads(state.result, viewState.caption, viewState.contentWarningReason, null) + val uploaded = state.result + if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) { + SuccessfulUploads(uploaded, viewState.caption, viewState.contentWarningReason, null) } else { null } @@ -153,7 +156,7 @@ class ChatFileUploader( suspend fun justUploadNIP04( viewState: ChatFileUploadState, onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, onStrippingFailed: suspend () -> Boolean = { true }, onceUploaded: suspend (List) -> Unit, ) { @@ -164,10 +167,10 @@ class ChatFileUploader( orchestrator.upload( viewState.caption, viewState.contentWarningReason, - MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider), + CompressorQuality.fromSlider(viewState.mediaQualitySlider), viewState.selectedServer, account, - context, + uploader, stripMetadata = viewState.stripMetadata, onStrippingFailed = onStrippingFailed, ) @@ -175,8 +178,9 @@ class ChatFileUploader( if (results.allGood) { val list = results.successful.mapNotNull { state -> - if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { - SuccessfulUploads(state.result, viewState.caption, viewState.contentWarningReason, null) + val uploaded = state.result + if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) { + SuccessfulUploads(uploaded, viewState.caption, viewState.contentWarningReason, null) } else { null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/SuccessfulUploads.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/SuccessfulUploads.kt index c88223ab0f..f7693c91c6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/SuccessfulUploads.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/SuccessfulUploads.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.quartz.utils.ciphers.AESGCM @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt index 976230c024..2964ce1923 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt @@ -73,8 +73,8 @@ import com.vitorpamplona.amethyst.commons.resources.concord_typing_many import com.vitorpamplona.amethyst.commons.resources.concord_typing_one import com.vitorpamplona.amethyst.commons.resources.concord_typing_two import com.vitorpamplona.amethyst.commons.resources.reply_here +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.feeds.DmHistoryLoadingCard import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachCursor import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachMarkers @@ -578,7 +578,7 @@ private fun ConcordFileUploadDialog( onEncryptedUploadError = { title, message -> scope.launch(Dispatchers.Main) { Toast.makeText(context, "$title: $message", Toast.LENGTH_LONG).show() } }, - context = context, + uploader = accountViewModel.host.mediaUploader, onceUploaded = { uploads -> val imetas = uploads.mapNotNull { it.toConcordImeta() } if (imetas.isNotEmpty()) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordImageUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordImageUploader.kt index 8d9cc8de9e..9c33247bb2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordImageUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordImageUploader.kt @@ -26,10 +26,12 @@ import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.mediaServers.DEFAULT_MEDIA_SERVERS import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +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.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadingState +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.quartz.concord.cord02Community.ImagePointer import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.utils.ciphers.AESGCM @@ -60,6 +62,7 @@ class ConcordImageUploader( suspend fun uploadEncrypted( uri: Uri, context: Context, + uploader: MediaUploader, ): ImagePointer = withContext(Dispatchers.IO) { // Fresh random key + nonce per image; we hold onto them to build the pointer below since the @@ -76,7 +79,7 @@ class ConcordImageUploader( encrypt = cipher, server = resolveBlossomServer(), account = account, - context = context, + uploader = uploader, ) val result = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMetadataForm.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMetadataForm.kt index 970e331ae9..7f3ab7b079 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMetadataForm.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMetadataForm.kt @@ -168,7 +168,7 @@ private fun ConcordIconHero( uploading = true scope.launch { try { - icon.value = ConcordImageUploader(accountViewModel.account).uploadEncrypted(uri, context) + icon.value = ConcordImageUploader(accountViewModel.account).uploadEncrypted(uri, context, accountViewModel.host.mediaUploader) } catch (e: CancellationException) { throw e } catch (e: Exception) { @@ -239,7 +239,7 @@ private fun ConcordBannerHero( uploading = true scope.launch { try { - banner.value = ConcordImageUploader(accountViewModel.account).uploadEncrypted(uri, context) + banner.value = ConcordImageUploader(accountViewModel.account).uploadEncrypted(uri, context, accountViewModel.host.mediaUploader) } catch (e: CancellationException) { throw e } catch (e: Exception) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/send/ConcordNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/send/ConcordNewMessageViewModel.kt index 6113ce792f..aa5cf9c9fb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/send/ConcordNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/send/ConcordNewMessageViewModel.kt @@ -34,7 +34,7 @@ import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.onUiThread diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt index 5f1c7228a8..adac712a2b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt @@ -40,13 +40,13 @@ import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_ 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.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MetadataStripper import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader 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 0b9b54a6bf..ab1c218a90 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 @@ -43,11 +43,11 @@ 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.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MetadataStripper import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt index 6ae466e4a3..539f57af85 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt @@ -30,7 +30,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -59,7 +58,6 @@ fun ChannelFileUploadDialog( nav: INav, ) { val channel = channelScreenModel.channel ?: return - val context = LocalContext.current ChatFileUploadDialog( state, @@ -133,7 +131,7 @@ fun ChannelFileUploadDialog( upload = { channelScreenModel.upload( onError = accountViewModel.toastManager::toast, - context = context, + uploader = accountViewModel.host.mediaUploader, onceUploaded = onUpload, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 6c81894517..68ae70fdf0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send -import android.content.Context import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd @@ -59,18 +58,20 @@ import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.richtext.UrlParser import com.vitorpamplona.amethyst.commons.service.pow.PoWReplay import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +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.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.text.currentWord 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.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.ReplyMode import com.vitorpamplona.amethyst.service.location.LocationState -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState @@ -495,11 +496,11 @@ open class ChannelNewMessageViewModel : fun upload( onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, onceUploaded: suspend () -> Unit, ) { try { - uploadUnsafe(onError, context, onceUploaded) + uploadUnsafe(onError, uploader, onceUploaded) } catch (_: SignerExceptions.ReadOnlyException) { viewModelScope.launch { onError( @@ -512,7 +513,7 @@ open class ChannelNewMessageViewModel : fun uploadUnsafe( onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, onceUploaded: suspend () -> Unit, ) { viewModelScope.launch(Dispatchers.IO) { @@ -526,10 +527,10 @@ open class ChannelNewMessageViewModel : myMultiOrchestrator.upload( uploadState.caption, uploadState.contentWarningReason, - MediaCompressor.intToCompressorQuality(uploadState.mediaQualitySlider), + CompressorQuality.fromSlider(uploadState.mediaQualitySlider), uploadState.selectedServer, account, - context, + uploader, stripMetadata = uploadState.stripMetadata, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, ) @@ -537,16 +538,17 @@ open class ChannelNewMessageViewModel : if (results.allGood) { val urls = results.successful.mapNotNull { upload -> - if (upload.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { - val nip95 = account.createNip95(upload.result.bytes, headerInfo = upload.result.fileHeader, uploadState.caption, uploadState.contentWarningReason) + val uploaded = upload.result + if (uploaded is UploadOrchestrator.OrchestratorResult.NIP95Result) { + val nip95 = account.createNip95(uploaded.bytes, headerInfo = uploaded.fileHeader, uploadState.caption, uploadState.contentWarningReason) nip95attachments = nip95attachments + nip95 val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) } note?.toNostrUri() - } else if (upload.result is UploadOrchestrator.OrchestratorResult.ServerResult) { - iMetaAttachments.add(upload.result, uploadState.caption, uploadState.contentWarningReason) + } else if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) { + iMetaAttachments.add(uploaded, uploadState.caption, uploadState.contentWarningReason) - upload.result.url + uploaded.url } else { null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt index ca7c486751..3ad55a0c43 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt @@ -53,8 +53,8 @@ import com.vitorpamplona.amethyst.commons.resources.buzz_editing_banner import com.vitorpamplona.amethyst.commons.resources.cancel import com.vitorpamplona.amethyst.commons.resources.reply_here import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt index cbb708ea7c..2703b316e1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt @@ -28,9 +28,9 @@ import androidx.compose.runtime.setValue import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.commons.service.upload.MediaUploadTracker -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing +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 kotlinx.collections.immutable.ImmutableList @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityModel.kt index 85dc127fa6..0aef57488b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityModel.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.newCommunity -import android.content.Context import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue @@ -39,11 +38,13 @@ import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_de 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.service.upload.SuspendableConfirmation -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.ui.loadStringRes -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.commons.ui.uploads.errorResource import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions @@ -271,12 +272,12 @@ class NewCommunityModel : ViewModel() { @OptIn(ExperimentalUuidApi::class) fun publish( - context: Context, + uploader: MediaUploader, onSuccess: () -> Unit, onError: (String, String) -> Unit, ) { try { - publishUnsafe(context, onSuccess, onError) + publishUnsafe(uploader, onSuccess, onError) } catch (e: SignerExceptions.ReadOnlyException) { viewModelScope.launch { onError( @@ -289,7 +290,7 @@ class NewCommunityModel : ViewModel() { @OptIn(ExperimentalUuidApi::class) private fun publishUnsafe( - context: Context, + uploader: MediaUploader, onSuccess: () -> Unit, onError: (String, String) -> Unit, ) { @@ -300,7 +301,7 @@ class NewCommunityModel : ViewModel() { try { val uploadedUrl = if (multiOrchestrator != null) { - uploadImageIfAny(context, myAccount, onError) ?: return@launch + uploadImageIfAny(uploader, myAccount, onError) ?: return@launch } else { null } @@ -371,7 +372,7 @@ class NewCommunityModel : ViewModel() { } private suspend fun uploadImageIfAny( - context: Context, + uploader: MediaUploader, myAccount: Account, onError: (String, String) -> Unit, ): String? { @@ -382,10 +383,10 @@ class NewCommunityModel : ViewModel() { orch.upload( alt = name.trim().ifBlank { "Community cover" }, contentWarningReason = null, - mediaQuality = MediaCompressor.intToCompressorQuality(mediaQualitySlider), + mediaQuality = CompressorQuality.fromSlider(mediaQualitySlider), server = serverToUse, account = myAccount, - context = context, + uploader = uploader, useH265 = false, stripMetadata = stripMetadata, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityScreen.kt index c0d669ab26..3afb77c81c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityScreen.kt @@ -58,7 +58,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.font.FontWeight import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextAlign @@ -137,7 +136,6 @@ private fun CommunityFormScreen( nav: INav, ) { val model: NewCommunityModel = viewModel() - val context = LocalContext.current LaunchedEffect(accountViewModel.account) { model.init(accountViewModel.account) @@ -185,7 +183,7 @@ private fun CommunityFormScreen( }, onPost = { model.publish( - context = context, + uploader = accountViewModel.host.mediaUploader, onSuccess = { nav.popBack() }, onError = accountViewModel.toastManager::toast, ) @@ -201,7 +199,7 @@ private fun CommunityFormScreen( }, onPost = { model.publish( - 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/discover/nip23LongForm/LongFormPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt index c75226bce1..48889ad2d5 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 @@ -101,9 +101,9 @@ import com.vitorpamplona.amethyst.commons.resources.url_slug_placeholder import com.vitorpamplona.amethyst.commons.resources.your_article_title import com.vitorpamplona.amethyst.commons.resources.zapraiser import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe @@ -238,15 +238,13 @@ private fun MarkdownPostScreenBody( .fillMaxWidth() .verticalScroll(scrollState, reverseScrolling = true), ) { - val context = LocalContext.current - // Banner image area BannerImageArea( coverImageUrl = postViewModel.coverImageUrl, isUploading = postViewModel.isUploadingCoverImage, accountViewModel = accountViewModel, onImageSelected = { - postViewModel.uploadCoverImage(it, context, onError = accountViewModel.toastManager::toast) + postViewModel.uploadCoverImage(it, accountViewModel.host.mediaUploader, onError = accountViewModel.toastManager::toast) }, onClear = { postViewModel.coverImageUrl = "" @@ -498,7 +496,7 @@ private fun MarkdownPostScreenBody( accountViewModel.account.settings.defaultFileServer, isUploading = postViewModel.mediaUploadTracker.isUploading, onAdd = { alt, server, sensitiveContent, mediaQuality, useH265, stripMetadata, convertGifToMp4 -> - postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, uploadContext, useH265, stripMetadata, convertGifToMp4) + postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, accountViewModel.host.mediaUploader, useH265, stripMetadata, convertGifToMp4) accountViewModel.account.settings.changeDefaultFileServer(server) }, onDelete = postViewModel::deleteMediaToUpload, 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 7856a09ef0..5514129da4 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 @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm -import android.content.Context import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd @@ -46,7 +45,6 @@ import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder import com.vitorpamplona.amethyst.commons.model.composer.toZapSplitSetup import com.vitorpamplona.amethyst.commons.model.location.LocationResult import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName -import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState.EmojiMedia import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState import com.vitorpamplona.amethyst.commons.resources.Res @@ -57,7 +55,13 @@ import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url import com.vitorpamplona.amethyst.commons.service.pow.PoWReplay import com.vitorpamplona.amethyst.commons.service.upload.MediaUploadTracker import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +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 @@ -67,15 +71,9 @@ import com.vitorpamplona.amethyst.commons.ui.text.currentWord 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.viewmodels.AccountViewModel 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.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor @@ -122,7 +120,6 @@ 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 @@ -467,7 +464,7 @@ class LongFormPostViewModel : fun uploadCoverImage( uri: SelectedMedia, - context: Context, + uploader: MediaUploader, onError: (String, String) -> Unit, ) { accountViewModel.launchSigner { @@ -475,7 +472,7 @@ class LongFormPostViewModel : try { directUpload( galleryUri = uri, - context = context, + uploader = uploader, onError = onError, )?.let { coverImageUrl = it @@ -488,49 +485,36 @@ class LongFormPostViewModel : private suspend fun directUpload( galleryUri: SelectedMedia, - context: Context, + uploader: MediaUploader, onError: (String, String) -> Unit, ): String? { - val compResult = MediaCompressor().compress(galleryUri.uri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext) + 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 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, - ) + return when (state) { + is UploadingState.Finished -> { + val uploaded = state.result + if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) { + uploaded.url } 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, - ) + onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.server_did_not_provide_a_url_after_uploading)) + null } - - 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 + is UploadingState.Error -> { + onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(state.errorResource, *state.params)) + null + } + else -> null } } @@ -540,13 +524,13 @@ class LongFormPostViewModel : mediaQuality: Int, server: ServerName, onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, useH265: Boolean, stripMetadata: Boolean = true, convertGifToMp4: Boolean = false, ) { try { - uploadUnsafe(alt, contentWarningReason, mediaQuality, server, onError, context, useH265, stripMetadata, convertGifToMp4) + uploadUnsafe(alt, contentWarningReason, mediaQuality, server, onError, uploader, useH265, stripMetadata, convertGifToMp4) } catch (_: SignerExceptions.ReadOnlyException) { viewModelScope.launch { onError( @@ -563,7 +547,7 @@ class LongFormPostViewModel : mediaQuality: Int, server: ServerName, onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, useH265: Boolean, stripMetadata: Boolean = true, convertGifToMp4: Boolean = false, @@ -577,10 +561,10 @@ class LongFormPostViewModel : myMultiOrchestrator.upload( alt, contentWarningReason, - MediaCompressor.intToCompressorQuality(mediaQuality), + CompressorQuality.fromSlider(mediaQuality), server, account, - context, + uploader, useH265, stripMetadata, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, @@ -590,29 +574,30 @@ class LongFormPostViewModel : if (results.allGood) { val urls = results.successful.mapNotNull { state -> - if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { + val uploaded = state.result + if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) { val iMeta = - IMetaTagBuilder(state.result.url) + IMetaTagBuilder(uploaded.url) .apply { - hash(state.result.fileHeader.hash) - size(state.result.fileHeader.size) - state.result.fileHeader.mimeType + hash(uploaded.fileHeader.hash) + size(uploaded.fileHeader.size) + uploaded.fileHeader.mimeType ?.let { mimeType(it) } - state.result.fileHeader.dim + uploaded.fileHeader.dim ?.let { dims(it) } - state.result.fileHeader.blurHash + uploaded.fileHeader.blurHash ?.let { blurhash(it.blurhash) } - state.result.fileHeader.thumbHash + uploaded.fileHeader.thumbHash ?.let { thumbhash(it.thumbhash) } - state.result.magnet?.let { magnet(it) } - state.result.uploadedHash?.let { originalHash(it) } + uploaded.magnet?.let { magnet(it) } + uploaded.uploadedHash?.let { originalHash(it) } alt?.let { alt(it) } contentWarningReason?.let { sensitiveContent(contentWarningReason) } }.build() iMetaAttachments.replace(iMeta.url, iMeta) - val markdownImage = "![${alt ?: ""}](${state.result.url})" + val markdownImage = "![${alt ?: ""}](${uploaded.url})" markdownImage } else { null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt index d8def28c87..8c2f501739 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt @@ -52,8 +52,8 @@ import com.vitorpamplona.amethyst.commons.resources.lightning_invoice import com.vitorpamplona.amethyst.commons.resources.new_product import com.vitorpamplona.amethyst.commons.resources.zapraiser import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -292,7 +292,7 @@ private fun NewProductBody( defaultServer = accountViewModel.account.settings.defaultFileServer, isUploading = postViewModel.mediaUploadTracker.isUploading, onAdd = { alt, server, sensitiveContent, mediaQuality, _, stripMetadata, _ -> - postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context, stripMetadata) + postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, accountViewModel.host.mediaUploader, stripMetadata) accountViewModel.account.settings.changeDefaultFileServer(server) }, onDelete = postViewModel::deleteMediaToUpload, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index 0a19fa5d04..a5b3dfdad5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds -import android.content.Context import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd @@ -52,7 +51,12 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details import com.vitorpamplona.amethyst.commons.service.upload.MediaUploadTracker import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +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.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState @@ -61,12 +65,9 @@ import com.vitorpamplona.amethyst.commons.ui.text.currentWord 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.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.location.LocationState -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.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor @@ -400,7 +401,7 @@ open class NewProductViewModel : mediaQuality: Int, server: ServerName, onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, stripMetadata: Boolean = true, ) { viewModelScope.launch(Dispatchers.IO) { @@ -413,40 +414,41 @@ open class NewProductViewModel : myMultiOrchestrator.upload( alt, contentWarningReason, - MediaCompressor.intToCompressorQuality(mediaQuality), + CompressorQuality.fromSlider(mediaQuality), server, myAccount, - context, + uploader, stripMetadata = stripMetadata, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, ) if (results.allGood) { val urls = - results.successful.mapNotNull { - if (it.result is UploadOrchestrator.OrchestratorResult.ServerResult) { - if (it.result.fileHeader.mimeType + results.successful.mapNotNull { finished -> + val uploaded = finished.result + if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) { + if (uploaded.fileHeader.mimeType ?.startsWith("image") == true ) { productImages = productImages + ProductImageMeta( - url = it.result.url, - mimeType = it.result.fileHeader.mimeType, + url = uploaded.url, + mimeType = uploaded.fileHeader.mimeType, blurhash = - it.result.fileHeader.blurHash + uploaded.fileHeader.blurHash ?.blurhash, - dimension = it.result.fileHeader.dim, + dimension = uploaded.fileHeader.dim, alt = alt, - hash = it.result.fileHeader.hash, - size = it.result.fileHeader.size, + hash = uploaded.fileHeader.hash, + size = uploaded.fileHeader.size, thumbhash = - it.result.fileHeader.thumbHash + uploaded.fileHeader.thumbHash ?.thumbhash, ) } else { - iMetaDescription.add(it.result, alt, contentWarningReason) + iMetaDescription.add(uploaded, alt, contentWarningReason) - it.result.url + uploaded.url } } else { null 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 6f6a51f5e4..751bce6682 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 @@ -38,10 +38,10 @@ import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_ 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.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +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.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MetadataStripper import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader 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 8d004ebec4..964abeef53 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 @@ -42,11 +42,11 @@ 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.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MetadataStripper import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt index 6e244bc1e9..d99e892f54 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt @@ -67,7 +67,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.style.TextAlign import androidx.compose.ui.unit.dp @@ -142,7 +141,6 @@ fun NewGeocacheScreen( ) { val model: NewGeocacheViewModel = viewModel() val scope = rememberCoroutineScope() - val context = LocalContext.current remember(editKind, editPubKeyHex, editDTag, prefillGeohash) { model.init(accountViewModel) @@ -158,7 +156,7 @@ fun NewGeocacheScreen( val photoPicker = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> - if (uri != null) scope.launch { model.uploadImage(uri, null, context) } + if (uri != null) scope.launch { model.uploadImage(uri, null, accountViewModel.host.mediaUploader) } } Scaffold( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheViewModel.kt index e1dfe1e218..1c60602098 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheViewModel.kt @@ -20,17 +20,17 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.create -import android.content.Context import android.net.Uri import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey @@ -177,7 +177,7 @@ class NewGeocacheViewModel : ViewModel() { suspend fun uploadImage( uri: Uri, mimeType: String?, - context: Context, + uploader: MediaUploader, ): Boolean { if (!::account.isInitialized) return false isUploading.value = true @@ -191,7 +191,7 @@ class NewGeocacheViewModel : ViewModel() { compressionQuality = CompressorQuality.MEDIUM, server = account.settings.defaultFileServer, account = account, - context = context, + uploader = uploader, ) val url = ((result as? UploadingState.Finished)?.result as? UploadOrchestrator.OrchestratorResult.ServerResult)?.url diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/log/LogGeocacheFindScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/log/LogGeocacheFindScreen.kt index bbb1bef126..ac8b6974b9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/log/LogGeocacheFindScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/log/LogGeocacheFindScreen.kt @@ -63,7 +63,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel @@ -124,7 +123,6 @@ fun LogGeocacheFindScreen( val address = remember(kind, pubKeyHex, dTag) { Address(kind, pubKeyHex, dTag) } val model: LogGeocacheFindViewModel = viewModel(key = "LogGeocacheFind-${address.toValue()}") val scope = rememberCoroutineScope() - val context = LocalContext.current remember(address) { model.init(accountViewModel, address) @@ -137,7 +135,7 @@ fun LogGeocacheFindScreen( val photoPicker = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> if (uri != null) { - scope.launch { model.uploadImage(uri, null, context) } + scope.launch { model.uploadImage(uri, null, accountViewModel.host.mediaUploader) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/log/LogGeocacheFindViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/log/LogGeocacheFindViewModel.kt index 56711ae236..f231ebdc51 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/log/LogGeocacheFindViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/log/LogGeocacheFindViewModel.kt @@ -20,17 +20,17 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.log -import android.content.Context import android.net.Uri import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair @@ -185,7 +185,7 @@ class LogGeocacheFindViewModel : ViewModel() { suspend fun uploadImage( uri: Uri, mimeType: String?, - context: Context, + uploader: MediaUploader, ): Boolean { if (!::account.isInitialized) return false isUploading.value = true @@ -199,7 +199,7 @@ class LogGeocacheFindViewModel : ViewModel() { compressionQuality = CompressorQuality.MEDIUM, server = account.settings.defaultFileServer, account = account, - context = context, + uploader = uploader, ) val url = ((result as? UploadingState.Finished)?.result as? UploadOrchestrator.OrchestratorResult.ServerResult)?.url diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt index fb41fd271c..c5f9554506 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt @@ -27,7 +27,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt index ebc46566e7..f82ffcca8b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt @@ -27,7 +27,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 8c3f3973e5..b954506508 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -109,8 +109,8 @@ import com.vitorpamplona.amethyst.commons.resources.schedule_post_always_on_prom import com.vitorpamplona.amethyst.commons.resources.what_s_on_your_mind import com.vitorpamplona.amethyst.commons.resources.zapraiser import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar @@ -616,7 +616,7 @@ private fun NewPostScreenBody( accountViewModel.account.settings.defaultFileServer, isUploading = postViewModel.mediaUploadTracker.isUploading, onAdd = { alt, server, sensitiveContent, mediaQuality, useH265, stripMetadata, convertGifToMp4 -> - postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context, useH265, stripMetadata, convertGifToMp4) + postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, accountViewModel.host.mediaUploader, useH265, stripMetadata, convertGifToMp4) accountViewModel.account.settings.changeDefaultFileServer(server) }, onDelete = postViewModel::deleteMediaToUpload, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 09dc1dc05a..b689be1da0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -69,7 +69,13 @@ import com.vitorpamplona.amethyst.commons.service.ai.WritingTone import com.vitorpamplona.amethyst.commons.service.pow.PoWReplay import com.vitorpamplona.amethyst.commons.service.upload.MediaUploadTracker import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +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.notify.IAudience @@ -81,17 +87,12 @@ 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.text.setTextAndPlaceCursorAtBeginning +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState import com.vitorpamplona.amethyst.service.ai.WritingAssistantFactory 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.UploadingState import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationController import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments @@ -1578,13 +1579,13 @@ open class ShortNotePostViewModel : mediaQuality: Int, server: ServerName, onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, useH265: Boolean, stripMetadata: Boolean = true, convertGifToMp4: Boolean = false, ) { try { - uploadUnsafe(alt, contentWarningReason, mediaQuality, server, onError, context, useH265, stripMetadata, convertGifToMp4) + uploadUnsafe(alt, contentWarningReason, mediaQuality, server, onError, uploader, useH265, stripMetadata, convertGifToMp4) } catch (_: SignerExceptions.ReadOnlyException) { viewModelScope.launch { onError( @@ -1601,7 +1602,7 @@ open class ShortNotePostViewModel : mediaQuality: Int, server: ServerName, onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, useH265: Boolean, stripMetadata: Boolean = true, convertGifToMp4: Boolean = false, @@ -1615,10 +1616,10 @@ open class ShortNotePostViewModel : myMultiOrchestrator.upload( alt, contentWarningReason, - MediaCompressor.intToCompressorQuality(mediaQuality), + CompressorQuality.fromSlider(mediaQuality), server, account, - context, + uploader, useH265, stripMetadata, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, @@ -1629,30 +1630,31 @@ open class ShortNotePostViewModel : if (results.allGood) { val urls = results.successful.mapNotNull { state -> - if (state.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { - val nip95 = account.createNip95(state.result.bytes, headerInfo = state.result.fileHeader, alt, contentWarningReason) + val uploaded = state.result + if (uploaded is UploadOrchestrator.OrchestratorResult.NIP95Result) { + val nip95 = account.createNip95(uploaded.bytes, headerInfo = uploaded.fileHeader, alt, contentWarningReason) nip95attachments = nip95attachments + nip95 val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) } note?.let { "nostr:" + it.toNEvent() } - } else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { + } else if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) { val iMeta = - IMetaTagBuilder(state.result.url) + IMetaTagBuilder(uploaded.url) .apply { - hash(state.result.fileHeader.hash) - size(state.result.fileHeader.size) - state.result.fileHeader.mimeType + hash(uploaded.fileHeader.hash) + size(uploaded.fileHeader.size) + uploaded.fileHeader.mimeType ?.let { mimeType(it) } - state.result.fileHeader.dim + uploaded.fileHeader.dim ?.let { dims(it) } - state.result.fileHeader.blurHash + uploaded.fileHeader.blurHash ?.let { blurhash(it.blurhash) } - state.result.fileHeader.thumbHash + uploaded.fileHeader.thumbHash ?.let { thumbhash(it.thumbhash) } - state.result.magnet?.let { magnet(it) } - state.result.uploadedHash?.let { originalHash(it) } + uploaded.magnet?.let { magnet(it) } + uploaded.uploadedHash?.let { originalHash(it) } alt?.let { alt(it) } contentWarningReason?.let { sensitiveContent(contentWarningReason) } @@ -1660,7 +1662,7 @@ open class ShortNotePostViewModel : iMetaAttachments.replace(iMeta.url, iMeta) - state.result.url + uploaded.url } else { null } @@ -1956,7 +1958,7 @@ open class ShortNotePostViewModel : compressionQuality = CompressorQuality.UNCOMPRESSED, server = server, account = account, - context = appContext, + uploader = accountViewModel.host.mediaUploader, useH265 = false, forcedSigner = if (wantsAnonymousPost) anonymousSigner() else null, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index 0dfff06e30..850b450647 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt @@ -38,12 +38,12 @@ import com.vitorpamplona.amethyst.commons.resources.upload_error_voice_message_e import com.vitorpamplona.amethyst.commons.resources.upload_error_voice_message_failed import com.vitorpamplona.amethyst.commons.resources.upload_error_voice_message_nip95_not_supported import com.vitorpamplona.amethyst.commons.service.pow.PoWReplay +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +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.util.deleteOrWarn import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationController import com.vitorpamplona.quartz.nip01Core.tags.people.toPTag @@ -204,7 +204,7 @@ class VoiceReplyViewModel : ViewModel() { compressionQuality = CompressorQuality.UNCOMPRESSED, server = serverToUse, account = accountViewModel.account, - context = Amethyst.instance.appContext, + uploader = accountViewModel.host.mediaUploader, useH265 = false, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataViewModel.kt index 5f38ec2d7b..5b66e7921c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataViewModel.kt @@ -39,11 +39,11 @@ import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_ 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.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MetadataStripper import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataViewModel.kt index 721bd81a87..d3d833908c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataViewModel.kt @@ -39,11 +39,11 @@ import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_ 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.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MetadataStripper import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/NewLongVideoButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/NewLongVideoButton.kt index f5e4fadc27..72ac47929c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/NewLongVideoButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/NewLongVideoButton.kt @@ -50,8 +50,8 @@ import com.vitorpamplona.amethyst.commons.resources.ic_compose import com.vitorpamplona.amethyst.commons.resources.new_long_video import com.vitorpamplona.amethyst.commons.resources.record_a_video import com.vitorpamplona.amethyst.commons.resources.upload_image +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelect -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicComposerUploadUi.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicComposerUploadUi.kt index 7894e15e7e..c535b03b39 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicComposerUploadUi.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicComposerUploadUi.kt @@ -50,9 +50,9 @@ import coil3.compose.rememberAsyncImagePainter import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery import org.jetbrains.compose.resources.StringResource 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 664693d762..2c1d87d16d 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 @@ -27,14 +27,15 @@ import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +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.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -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.UploadingState import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent import com.vitorpamplona.quartz.nip01Core.core.Address import kotlinx.collections.immutable.ImmutableList @@ -214,10 +215,11 @@ class NewMusicPlaylistViewModel : ViewModel() { coverOrchestrator = coverMedia.value, existingCoverUrl = coverUrl.value.trim().ifBlank { null }, server = server, - quality = MediaCompressor.intToCompressorQuality(mediaQualitySlider.value), + quality = CompressorQuality.fromSlider(mediaQualitySlider.value), stripMetadata = stripMetadata.value, loadedEvent = loadedEvent, appContext = context.applicationContext, + uploader = accountViewModel.host.mediaUploader, ) isSending.value = true @@ -265,6 +267,7 @@ class NewMusicPlaylistViewModel : ViewModel() { val stripMetadata: Boolean, val loadedEvent: MusicPlaylistEvent?, val appContext: Context, + val uploader: MediaUploader, ) private suspend fun runCoverUpload( @@ -279,13 +282,13 @@ class NewMusicPlaylistViewModel : ViewModel() { mediaQuality = snapshot.quality, server = server, account = account, - context = snapshot.appContext, + uploader = snapshot.uploader, useH265 = false, stripMetadata = snapshot.stripMetadata, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, ) if (!res.allGood) { - throw UploadException(formatUploadErrors(res.errors, snapshot.appContext)) + throw UploadException(formatUploadErrors(res.errors)) } return firstUploadedUrl(res.successful) ?: throw UploadException("Server didn't return a URL for the uploaded cover.") @@ -300,10 +303,7 @@ class NewMusicPlaylistViewModel : ViewModel() { .firstNotNullOfOrNull { it.result as? UploadOrchestrator.OrchestratorResult.ServerResult } ?.url - private suspend fun formatUploadErrors( - errors: List, - context: Context, - ): String = + private suspend fun formatUploadErrors(errors: List): String = errors .map { loadStringRes(it.errorResource, *it.params) } .distinct() 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 900b47f545..a8b4306067 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 @@ -84,8 +84,8 @@ import com.vitorpamplona.amethyst.commons.resources.music_track_title_placeholde import com.vitorpamplona.amethyst.commons.resources.music_track_uploading_banner import com.vitorpamplona.amethyst.commons.resources.new_music_track import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav 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 281cbf3855..e4b010cf45 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 @@ -30,13 +30,15 @@ import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +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.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -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.quartz.experimental.music.track.MusicTrackEvent import com.vitorpamplona.quartz.nip01Core.core.Address import kotlinx.collections.immutable.ImmutableList @@ -288,10 +290,11 @@ class NewMusicTrackViewModel : ViewModel() { existingCoverUrl = coverUrl.value.trim().ifBlank { null }, existingAudioUrl = audioUrl.value.trim(), server = server, - quality = MediaCompressor.intToCompressorQuality(mediaQualitySlider.value), + quality = CompressorQuality.fromSlider(mediaQualitySlider.value), stripMetadata = stripMetadata.value, loadedEvent = loadedEvent, appContext = context.applicationContext, + uploader = accountViewModel.host.mediaUploader, ) isSending.value = true @@ -363,6 +366,7 @@ class NewMusicTrackViewModel : ViewModel() { val stripMetadata: Boolean, val loadedEvent: MusicTrackEvent?, val appContext: Context, + val uploader: MediaUploader, ) /** @@ -403,13 +407,13 @@ class NewMusicTrackViewModel : ViewModel() { mediaQuality = snapshot.quality, server = snapshot.server, account = account, - context = snapshot.appContext, + uploader = snapshot.uploader, useH265 = false, stripMetadata = snapshot.stripMetadata, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, ) if (!res.allGood) { - throw UploadException(kind, formatUploadErrors(res.errors, snapshot.appContext)) + throw UploadException(kind, formatUploadErrors(res.errors)) } return firstUploadedUrl(res.successful) ?: throw UploadException(kind, "Server didn't return a URL for the uploaded $kind.") @@ -420,15 +424,12 @@ class NewMusicTrackViewModel : ViewModel() { details: String, ) : Exception("$kind upload failed: $details") - private fun firstUploadedUrl(successful: List): String? = + private fun firstUploadedUrl(successful: List): String? = successful .firstNotNullOfOrNull { it.result as? UploadOrchestrator.OrchestratorResult.ServerResult } ?.url - private suspend fun formatUploadErrors( - errors: List, - context: Context, - ): String = + private suspend fun formatUploadErrors(errors: List): String = errors .map { loadStringRes(it.errorResource, *it.params) } .distinct() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/create/CreateNestViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/create/CreateNestViewModel.kt index 09bd48895a..e72eba5d8d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/create/CreateNestViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/create/CreateNestViewModel.kt @@ -31,11 +31,11 @@ import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_de 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.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MetadataStripper import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt index 2f33c77186..ac621ee4df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt @@ -40,8 +40,8 @@ import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggesti import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.reply_here import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.commons.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestFileUploadDialog.kt index 8c8c0e6afc..111e99080d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestFileUploadDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestFileUploadDialog.kt @@ -29,7 +29,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -56,7 +55,6 @@ fun NestFileUploadDialog( nav: INav, ) { val room = nestScreenModel.room ?: return - val context = LocalContext.current val host = room.author ?: return val title = (room.event as? MeetingSpaceEvent)?.room().orEmpty() @@ -93,7 +91,7 @@ fun NestFileUploadDialog( upload = { nestScreenModel.upload( onError = accountViewModel.toastManager::toast, - context = context, + uploader = accountViewModel.host.mediaUploader, onceUploaded = onUpload, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt index 6201c18504..378d7a8aa3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.chat -import android.content.Context import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd @@ -50,17 +49,19 @@ import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_ import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.richtext.UrlParser import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +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.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.text.currentWord 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.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.location.LocationState -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState @@ -349,11 +350,11 @@ open class NestNewMessageViewModel : fun upload( onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, onceUploaded: suspend () -> Unit, ) { try { - uploadUnsafe(onError, context, onceUploaded) + uploadUnsafe(onError, uploader, onceUploaded) } catch (_: SignerExceptions.ReadOnlyException) { viewModelScope.launch { onError( @@ -366,7 +367,7 @@ open class NestNewMessageViewModel : fun uploadUnsafe( onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, onceUploaded: suspend () -> Unit, ) { viewModelScope.launch(Dispatchers.IO) { @@ -380,10 +381,10 @@ open class NestNewMessageViewModel : myMultiOrchestrator.upload( uploadState.caption, uploadState.contentWarningReason, - MediaCompressor.intToCompressorQuality(uploadState.mediaQualitySlider), + CompressorQuality.fromSlider(uploadState.mediaQualitySlider), uploadState.selectedServer, account, - context, + uploader, stripMetadata = uploadState.stripMetadata, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, ) @@ -391,16 +392,17 @@ open class NestNewMessageViewModel : if (results.allGood) { val urls = results.successful.mapNotNull { upload -> - if (upload.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { - val nip95 = account.createNip95(upload.result.bytes, headerInfo = upload.result.fileHeader, uploadState.caption, uploadState.contentWarningReason) + val uploaded = upload.result + if (uploaded is UploadOrchestrator.OrchestratorResult.NIP95Result) { + val nip95 = account.createNip95(uploaded.bytes, headerInfo = uploaded.fileHeader, uploadState.caption, uploadState.contentWarningReason) nip95attachments = nip95attachments + nip95 val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) } note?.toNostrUri() - } else if (upload.result is UploadOrchestrator.OrchestratorResult.ServerResult) { - iMetaAttachments.add(upload.result, uploadState.caption, uploadState.contentWarningReason) + } else if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) { + iMetaAttachments.add(uploaded, uploadState.caption, uploadState.contentWarningReason) - upload.result.url + uploaded.url } else { null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt index cc158ece0c..248c8cb6a7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt @@ -47,7 +47,6 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -60,8 +59,8 @@ import com.vitorpamplona.amethyst.commons.resources.messages_new_message_to_capt import com.vitorpamplona.amethyst.commons.resources.public_message import com.vitorpamplona.amethyst.commons.resources.zapraiser import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -260,13 +259,12 @@ fun PublicMessageScreenContent( verticalAlignment = CenterVertically, modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp), ) { - val context = LocalContext.current ImageVideoDescription( it, accountViewModel.account.settings.defaultFileServer, isUploading = postViewModel.mediaUploadTracker.isUploading, onAdd = { alt, server, sensitiveContent, mediaQuality, _, stripMetadata, _ -> - postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context, stripMetadata) + postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, accountViewModel.host.mediaUploader, stripMetadata) accountViewModel.account.settings.changeDefaultFileServer(server) }, onDelete = postViewModel::deleteMediaToUpload, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index bfdf88f3bf..4031174597 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages -import android.content.Context import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd @@ -54,7 +53,12 @@ import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_ import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.service.upload.MediaUploadTracker import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +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.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState @@ -63,12 +67,9 @@ import com.vitorpamplona.amethyst.commons.ui.text.currentWord 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.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.location.LocationState -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.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor @@ -430,11 +431,11 @@ class NewPublicMessageViewModel : mediaQuality: Int, server: ServerName, onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, stripMetadata: Boolean = true, ) { try { - uploadUnsafe(alt, contentWarningReason, mediaQuality, server, onError, context, stripMetadata) + uploadUnsafe(alt, contentWarningReason, mediaQuality, server, onError, uploader, stripMetadata) } catch (e: SignerExceptions.ReadOnlyException) { viewModelScope.launch { onError( @@ -451,7 +452,7 @@ class NewPublicMessageViewModel : mediaQuality: Int, server: ServerName, onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, stripMetadata: Boolean = true, ) { viewModelScope.launch(Dispatchers.IO) { @@ -463,10 +464,10 @@ class NewPublicMessageViewModel : myMultiOrchestrator.upload( alt, contentWarningReason, - MediaCompressor.intToCompressorQuality(mediaQuality), + CompressorQuality.fromSlider(mediaQuality), server, account, - context, + uploader, stripMetadata = stripMetadata, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, ) @@ -474,30 +475,31 @@ class NewPublicMessageViewModel : if (results.allGood) { val urls = results.successful.mapNotNull { state -> - if (state.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { - val nip95 = account.createNip95(state.result.bytes, headerInfo = state.result.fileHeader, alt, contentWarningReason) + val uploaded = state.result + if (uploaded is UploadOrchestrator.OrchestratorResult.NIP95Result) { + val nip95 = account.createNip95(uploaded.bytes, headerInfo = uploaded.fileHeader, alt, contentWarningReason) nip95attachments = nip95attachments + nip95 val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) } note?.let { "nostr:" + it.toNEvent() } - } else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { + } else if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) { val iMeta = - IMetaTagBuilder(state.result.url) + IMetaTagBuilder(uploaded.url) .apply { - hash(state.result.fileHeader.hash) - size(state.result.fileHeader.size) - state.result.fileHeader.mimeType + hash(uploaded.fileHeader.hash) + size(uploaded.fileHeader.size) + uploaded.fileHeader.mimeType ?.let { mimeType(it) } - state.result.fileHeader.dim + uploaded.fileHeader.dim ?.let { dims(it) } - state.result.fileHeader.blurHash + uploaded.fileHeader.blurHash ?.let { blurhash(it.blurhash) } - state.result.fileHeader.thumbHash + uploaded.fileHeader.thumbHash ?.let { thumbhash(it.thumbhash) } - state.result.magnet?.let { magnet(it) } - state.result.uploadedHash?.let { originalHash(it) } + uploaded.magnet?.let { magnet(it) } + uploaded.uploadedHash?.let { originalHash(it) } alt?.let { alt(it) } contentWarningReason?.let { sensitiveContent(contentWarningReason) } @@ -505,7 +507,7 @@ class NewPublicMessageViewModel : iMetaAttachments.replace(iMeta.url, iMeta) - state.result.url + uploaded.url } else { null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/NewPictureButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/NewPictureButton.kt index 173f7abcdd..bf80d12fbb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/NewPictureButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/NewPictureButton.kt @@ -51,8 +51,8 @@ import com.vitorpamplona.amethyst.commons.resources.ic_compose import com.vitorpamplona.amethyst.commons.resources.new_picture import com.vitorpamplona.amethyst.commons.resources.take_a_picture import com.vitorpamplona.amethyst.commons.resources.upload_image +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelect -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes 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 8a0c2c99ba..4566c8f96b 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 @@ -28,10 +28,11 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.podcasts.V4VSplitEditorState import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nipXXPodcasting20.metadata.Podcasting20PodcastMetadata @@ -168,9 +169,10 @@ class EditPodcastShowViewModel : ViewModel() { ), coverOrchestrator = coverOrch, server = server, - quality = MediaCompressor.intToCompressorQuality(mediaQualitySlider.value), + quality = CompressorQuality.fromSlider(mediaQualitySlider.value), stripMetadata = stripMetadata.value, appContext = context.applicationContext, + uploader = accountViewModel.host.mediaUploader, ) isSending.value = true @@ -186,7 +188,7 @@ class EditPodcastShowViewModel : ViewModel() { quality = snapshot.quality, stripMetadata = snapshot.stripMetadata, alt = snapshot.content.title, - context = snapshot.appContext, + uploader = snapshot.uploader, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, ) } @@ -219,9 +221,10 @@ class EditPodcastShowViewModel : ViewModel() { val content: Podcasting20PodcastMetadata.Content, val coverOrchestrator: MultiOrchestrator?, val server: ServerName?, - val quality: com.vitorpamplona.amethyst.service.uploads.CompressorQuality, + 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 45a242d68c..51cd3f9789 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 @@ -90,8 +90,8 @@ import com.vitorpamplona.amethyst.commons.resources.podcast_episode_video_label import com.vitorpamplona.amethyst.commons.resources.podcast_new_episode import com.vitorpamplona.amethyst.commons.resources.podcast_publishing_banner import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SendingTopBar 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 72f55d4a5f..d19af8e576 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 @@ -29,10 +29,11 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.podcasts.V4VSplitEditorState import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nipXXPodcasting20.episode.Podcasting20EpisodeEvent import com.vitorpamplona.quartz.podcasts.PodcastAudio @@ -205,9 +206,10 @@ class NewPodcastEpisodeViewModel : ViewModel() { existingCoverUrl = coverUrl.value.trim().ifBlank { null }, existingAudioUrl = audioUrl.value.trim(), server = server, - quality = MediaCompressor.intToCompressorQuality(mediaQualitySlider.value), + quality = CompressorQuality.fromSlider(mediaQualitySlider.value), stripMetadata = stripMetadata.value, appContext = context.applicationContext, + uploader = accountViewModel.host.mediaUploader, ) isSending.value = true @@ -265,9 +267,10 @@ class NewPodcastEpisodeViewModel : ViewModel() { val existingCoverUrl: String?, val existingAudioUrl: String, val server: ServerName, - val quality: com.vitorpamplona.amethyst.service.uploads.CompressorQuality, + val quality: CompressorQuality, val stripMetadata: Boolean, val appContext: Context, + val uploader: MediaUploader, ) private suspend fun performParallelUploads(snapshot: Snapshot): Pair = @@ -294,7 +297,7 @@ class NewPodcastEpisodeViewModel : ViewModel() { quality = snapshot.quality, stripMetadata = snapshot.stripMetadata, alt = snapshot.title.ifBlank { null }, - context = snapshot.appContext, + uploader = snapshot.uploader, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, ) 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 dc9bba01bd..3cf81e31a5 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 @@ -71,7 +71,7 @@ import com.vitorpamplona.amethyst.commons.resources.podcast_trailer_upload_cta import com.vitorpamplona.amethyst.commons.resources.podcast_trailer_upload_hint import com.vitorpamplona.amethyst.commons.resources.podcast_trailer_url_label import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SendingTopBar 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 d1e649b0ba..ba63ff107b 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 @@ -26,10 +26,10 @@ import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator import com.vitorpamplona.quartz.nipXXPodcasting20.trailer.Podcasting20TrailerEvent import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers @@ -110,7 +110,7 @@ class NewPodcastTrailerViewModel : ViewModel() { val seasonSnap = season.value.trim().toIntOrNull() val mimeSnap = pickedMimeType.value val mediaSnap = media.value - val quality = MediaCompressor.intToCompressorQuality(mediaQualitySlider.value) + val quality = CompressorQuality.fromSlider(mediaQualitySlider.value) val strip = stripMetadata.value val appContext = context.applicationContext @@ -127,7 +127,7 @@ class NewPodcastTrailerViewModel : ViewModel() { quality = quality, stripMetadata = strip, alt = titleSnap.ifBlank { null }, - context = appContext, + uploader = accountViewModel.host.mediaUploader, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, ) } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/PodcastComposerMedia.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/PodcastComposerMedia.kt index 5198d8b943..43cdbf5921 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/PodcastComposerMedia.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/PodcastComposerMedia.kt @@ -25,11 +25,13 @@ import android.media.MediaMetadataRetriever import android.net.Uri import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +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.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadingState +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import java.time.ZoneId import java.time.ZonedDateTime import java.time.format.DateTimeFormatter @@ -58,7 +60,7 @@ object PodcastComposerMedia { quality: CompressorQuality, stripMetadata: Boolean, alt: String?, - context: Context, + uploader: MediaUploader, onStrippingFailed: suspend () -> Boolean, ): String { val res = @@ -68,12 +70,12 @@ object PodcastComposerMedia { mediaQuality = quality, server = server, account = account, - context = context, + uploader = uploader, useH265 = false, stripMetadata = stripMetadata, onStrippingFailed = onStrippingFailed, ) - if (!res.allGood) throw UploadException(kind, formatUploadErrors(res.errors, context)) + if (!res.allGood) throw UploadException(kind, formatUploadErrors(res.errors)) return firstUploadedUrl(res.successful) ?: throw UploadException(kind, "Server didn't return a URL for the uploaded $kind.") } @@ -83,10 +85,7 @@ object PodcastComposerMedia { .firstNotNullOfOrNull { it.result as? UploadOrchestrator.OrchestratorResult.ServerResult } ?.url - private suspend fun formatUploadErrors( - errors: List, - context: Context, - ): String = + private suspend fun formatUploadErrors(errors: List): String = errors .map { loadStringRes(it.errorResource, *it.params) } .distinct() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/NewShortVideoButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/NewShortVideoButton.kt index 5bc79a1bec..234c89f792 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/NewShortVideoButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/NewShortVideoButton.kt @@ -52,8 +52,8 @@ import com.vitorpamplona.amethyst.commons.resources.ic_compose import com.vitorpamplona.amethyst.commons.resources.new_short_video import com.vitorpamplona.amethyst.commons.resources.record_a_video import com.vitorpamplona.amethyst.commons.resources.upload_image +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelect -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlPostScreen.kt index 1709d2341d..1d10a9eab3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlPostScreen.kt @@ -27,7 +27,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewVideoFeedButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewVideoFeedButton.kt index 77f429b696..fa0450900f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewVideoFeedButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewVideoFeedButton.kt @@ -52,8 +52,8 @@ import com.vitorpamplona.amethyst.commons.resources.new_media import com.vitorpamplona.amethyst.commons.resources.record_a_video import com.vitorpamplona.amethyst.commons.resources.take_a_picture import com.vitorpamplona.amethyst.commons.resources.upload_image +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelect -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectedMediaTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectedMediaTest.kt index ed8b30593e..59a2470b09 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectedMediaTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectedMediaTest.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.actions.uploads import android.net.Uri -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import io.mockk.mockk import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/components/MediaCompressorTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/components/MediaCompressorTest.kt index b0fd8b73c5..9ff95b81a9 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/components/MediaCompressorTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/components/MediaCompressorTest.kt @@ -24,7 +24,7 @@ import android.content.Context import android.net.Uri import android.os.Looper import com.davotoula.lightcompressor.VideoCompressor -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.ui.components.util.MediaCompressorFileUtils import id.zelory.compressor.Compressor diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/MediaUri.android.kt b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.android.kt similarity index 94% rename from commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/MediaUri.android.kt rename to commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.android.kt index 41ef7a7878..5a7e949672 100644 --- a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/MediaUri.android.kt +++ b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.android.kt @@ -18,7 +18,7 @@ * 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.actions.uploads +package com.vitorpamplona.amethyst.commons.service.uploads import android.net.Uri diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/CompressorQuality.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/CompressorQuality.kt new file mode 100644 index 0000000000..246f907736 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/CompressorQuality.kt @@ -0,0 +1,43 @@ +/* + * 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.service.uploads + +enum class CompressorQuality { + VERY_LOW, + LOW, + MEDIUM, + HIGH, + VERY_HIGH, + UNCOMPRESSED, + ; + + companion object { + /** The media-quality slider's position (0 low … 3 uncompressed) as a quality. */ + fun fromSlider(position: Int): CompressorQuality = + when (position) { + 0 -> LOW + 1 -> MEDIUM + 2 -> HIGH + 3 -> UNCOMPRESSED + else -> MEDIUM + } + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaCompressorResult.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaCompressorResult.kt new file mode 100644 index 0000000000..e1d63e4e8b --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaCompressorResult.kt @@ -0,0 +1,28 @@ +/* + * 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.service.uploads + +/** The file to upload after compression: the original when nothing was compressed. */ +class MediaCompressorResult( + val uri: MediaUri, + val contentType: String?, + val size: Long?, +) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUploader.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUploader.kt new file mode 100644 index 0000000000..a20a1de7bc --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUploader.kt @@ -0,0 +1,124 @@ +/* + * 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.service.uploads + +import com.vitorpamplona.amethyst.commons.model.Account +import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.utils.ciphers.NostrCipher + +/** + * The platform's media pipeline: compresses, strips metadata from, optionally encrypts, and + * uploads a picked file to a NIP-95, NIP-96 or Blossom server, reporting each stage on + * [progress]. Reached through `AccountViewModelHost.mediaUploader`; [UploadOrchestrator] and + * [MultiOrchestrator] are the shared front ends composers use. + */ +interface MediaUploader { + suspend fun upload( + progress: UploadOrchestrator, + uri: MediaUri, + mimeType: String?, + alt: String?, + contentWarningReason: String?, + compressionQuality: CompressorQuality, + server: ServerName, + account: Account, + useH265: Boolean, + stripMetadata: Boolean, + onStrippingFailed: suspend () -> Boolean, + convertGifToMp4: Boolean, + forcedSigner: NostrSigner?, + ): UploadingState.UploadingFinalState + + suspend fun uploadEncrypted( + progress: UploadOrchestrator, + uri: MediaUri, + mimeType: String?, + alt: String?, + contentWarningReason: String?, + compressionQuality: CompressorQuality, + encrypt: NostrCipher, + server: ServerName, + account: Account, + useH265: Boolean, + stripMetadata: Boolean, + onStrippingFailed: suspend () -> Boolean, + convertGifToMp4: Boolean, + forcedSigner: NostrSigner?, + ): UploadingState.UploadingFinalState + + /** Compresses [uri] when [compressionQuality] or [convertGifToMp4] asks for it, else returns it as is. */ + suspend fun compressIfNeeded( + progress: UploadOrchestrator, + uri: MediaUri, + mimeType: String?, + compressionQuality: CompressorQuality, + useH265: Boolean, + convertGifToMp4: Boolean, + ): MediaCompressorResult +} + +/** No media pipeline on this front end yet: every upload fails with a clear message. */ +object UnavailableMediaUploader : MediaUploader { + private const val REASON = "Uploads are not available on this platform yet" + + override suspend fun upload( + progress: UploadOrchestrator, + uri: MediaUri, + mimeType: String?, + alt: String?, + contentWarningReason: String?, + compressionQuality: CompressorQuality, + server: ServerName, + account: Account, + useH265: Boolean, + stripMetadata: Boolean, + onStrippingFailed: suspend () -> Boolean, + convertGifToMp4: Boolean, + forcedSigner: NostrSigner?, + ): UploadingState.UploadingFinalState = progress.error(UploadError.FAILED_TO_UPLOAD_MEDIA, REASON) + + override suspend fun uploadEncrypted( + progress: UploadOrchestrator, + uri: MediaUri, + mimeType: String?, + alt: String?, + contentWarningReason: String?, + compressionQuality: CompressorQuality, + encrypt: NostrCipher, + server: ServerName, + account: Account, + useH265: Boolean, + stripMetadata: Boolean, + onStrippingFailed: suspend () -> Boolean, + convertGifToMp4: Boolean, + forcedSigner: NostrSigner?, + ): UploadingState.UploadingFinalState = progress.error(UploadError.FAILED_TO_UPLOAD_MEDIA, REASON) + + override suspend fun compressIfNeeded( + progress: UploadOrchestrator, + uri: MediaUri, + mimeType: String?, + compressionQuality: CompressorQuality, + useH265: Boolean, + convertGifToMp4: Boolean, + ): MediaCompressorResult = MediaCompressorResult(uri, mimeType, null) +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/MediaUri.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.kt similarity index 95% rename from commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/MediaUri.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.kt index fd3fca4400..f7459cf988 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/MediaUri.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.kt @@ -18,7 +18,7 @@ * 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.actions.uploads +package com.vitorpamplona.amethyst.commons.service.uploads /** * A picked file's address as the platform hands it over: Android's `content://` [android.net.Uri] diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MultiOrchestrator.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MultiOrchestrator.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MultiOrchestrator.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MultiOrchestrator.kt index 38547b435f..7958b58003 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MultiOrchestrator.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MultiOrchestrator.kt @@ -18,20 +18,18 @@ * 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.service.uploads +package com.vitorpamplona.amethyst.commons.service.uploads -import android.content.Context import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.utils.ciphers.NostrCipher import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch @@ -67,7 +65,7 @@ class MultiOrchestrator( mediaQuality: CompressorQuality, server: ServerName, account: Account, - context: Context, + uploader: MediaUploader, useH265: Boolean = false, stripMetadata: Boolean = true, onStrippingFailed: suspend () -> Boolean = { true }, @@ -86,7 +84,7 @@ class MultiOrchestrator( mediaQuality, server, account, - context, + uploader, useH265, stripMetadata, onStrippingFailed, @@ -109,7 +107,7 @@ class MultiOrchestrator( cipher: NostrCipher, server: ServerName, account: Account, - context: Context, + uploader: MediaUploader, useH265: Boolean = false, stripMetadata: Boolean = true, onStrippingFailed: suspend () -> Boolean = { true }, @@ -129,7 +127,7 @@ class MultiOrchestrator( cipher, server, account, - context, + uploader, useH265, stripMetadata, onStrippingFailed, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/SelectedMedia.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/SelectedMedia.kt new file mode 100644 index 0000000000..195105ebf1 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/SelectedMedia.kt @@ -0,0 +1,65 @@ +/* + * 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.service.uploads + +import androidx.compose.runtime.Stable + +@Stable +class SelectedMedia( + val uri: MediaUri, + val mimeType: String?, +) { + fun isImage() = mimeType?.startsWith("image") + + fun isGif() = mimeType?.equals("image/gif", ignoreCase = true) == true + + fun isVideo() = mimeType?.startsWith("video") + + fun isAudio() = mimeType?.startsWith("audio") + + fun isNotMedia() = + mimeType?.let { + !(it.startsWith("image") || it.startsWith("video") || it.startsWith("audio")) + } ?: true + + fun isDocument() = mimeType == "application/pdf" + + /** + * Returns true if [MediaCompressor.compress] would actually compress this file when a + * non-UNCOMPRESSED quality is selected. AVIF, GIF, SVG, and unknown MIME types pass + * through MediaCompressor unchanged — the compression-quality slider has no effect on + * them, so the UI hides the slider when no selected files are compressible. + * + * Keep this in sync with the branching in MediaCompressor.compress() — if either side + * drifts, the UI will lie to the user. + */ + fun isCompressible(): Boolean { + val mt = mimeType?.lowercase() ?: return false + return when { + mt.startsWith("video") -> true + mt.startsWith("image") -> + !mt.contains("gif") && + !mt.contains("svg") && + !isAvif(mt) + else -> false + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectedMediaProcessing.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/SelectedMediaProcessing.kt similarity index 86% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectedMediaProcessing.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/SelectedMediaProcessing.kt index 8f5335300e..c07ae92cfa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectedMediaProcessing.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/SelectedMediaProcessing.kt @@ -18,10 +18,7 @@ * 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.ui.actions.uploads - -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectedMedia -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator +package com.vitorpamplona.amethyst.commons.service.uploads class SelectedMediaProcessing( val media: SelectedMedia, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/UploadError.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/UploadError.kt new file mode 100644 index 0000000000..48168136b1 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/UploadError.kt @@ -0,0 +1,35 @@ +/* + * 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.service.uploads + +/** Why an upload failed. The UI names it with `UploadingState.Error.errorResource`. */ +enum class UploadError { + MEDIA_TOO_BIG_FOR_NIP95, + COULD_NOT_CHECK_DOWNLOADED_FILE, + COULD_NOT_OPEN_COMPRESSED_FILE, + LOGIN_WITH_PRIVATE_KEY, + FAILED_TO_UPLOAD_MEDIA, + BLOSSOM_PAYMENT_REQUIRED, + SERVER_DID_NOT_PROVIDE_URL, + COULD_NOT_DOWNLOAD_FROM_SERVER, + AVIF_METADATA_STRIP_FAILED, + UPLOAD_CANCELLED, +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/UploadOrchestrator.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/UploadOrchestrator.kt new file mode 100644 index 0000000000..dfd672ce8b --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/UploadOrchestrator.kt @@ -0,0 +1,121 @@ +/* + * 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.service.uploads + +import com.vitorpamplona.amethyst.commons.model.Account +import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName +import com.vitorpamplona.amethyst.commons.service.upload.FileHeader +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.utils.ciphers.NostrCipher +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map + +/** + * One file's upload, as the composer sees it: its progress and final state. The work itself is + * the [MediaUploader]'s, which reports each stage back through [updateState]. + */ +class UploadOrchestrator { + val progress = MutableStateFlow(0.0) + val progressState = MutableStateFlow(UploadingState.Ready) + + val isUploading = + progressState.map { + progressState.value !is UploadingState.Ready && progressState.value !is UploadingState.Error && progressState.value !is UploadingState.Finished + } + + fun error( + error: UploadError, + vararg params: String, + ) = UploadingState.Error(error, params).also { updateState(0.0, it) } + + fun finish(result: OrchestratorResult) = + UploadingState + .Finished(result) + .also { updateState(1.0, it) } + + fun updateState( + newProgress: Double, + newState: UploadingState, + ) { + progress.value = newProgress + progressState.value = newState + } + + suspend fun compressIfNeeded( + uri: MediaUri, + mimeType: String?, + compressionQuality: CompressorQuality, + uploader: MediaUploader, + useH265: Boolean = false, + convertGifToMp4: Boolean = false, + ): MediaCompressorResult = uploader.compressIfNeeded(this, uri, mimeType, compressionQuality, useH265, convertGifToMp4) + + suspend fun upload( + uri: MediaUri, + mimeType: String?, + alt: String?, + contentWarningReason: String?, + compressionQuality: CompressorQuality, + server: ServerName, + account: Account, + uploader: MediaUploader, + useH265: Boolean = false, + stripMetadata: Boolean = true, + onStrippingFailed: suspend () -> Boolean = { true }, + convertGifToMp4: Boolean = false, + forcedSigner: NostrSigner? = null, + ): UploadingState.UploadingFinalState = uploader.upload(this, uri, mimeType, alt, contentWarningReason, compressionQuality, server, account, useH265, stripMetadata, onStrippingFailed, convertGifToMp4, forcedSigner) + + suspend fun uploadEncrypted( + uri: MediaUri, + mimeType: String?, + alt: String?, + contentWarningReason: String?, + compressionQuality: CompressorQuality, + encrypt: NostrCipher, + server: ServerName, + account: Account, + uploader: MediaUploader, + useH265: Boolean = false, + stripMetadata: Boolean = true, + onStrippingFailed: suspend () -> Boolean = { true }, + convertGifToMp4: Boolean = false, + forcedSigner: NostrSigner? = null, + ): UploadingState.UploadingFinalState = uploader.uploadEncrypted(this, uri, mimeType, alt, contentWarningReason, compressionQuality, encrypt, server, account, useH265, stripMetadata, onStrippingFailed, convertGifToMp4, forcedSigner) + + sealed class OrchestratorResult { + class NIP95Result( + val fileHeader: FileHeader, + val bytes: ByteArray, + val mimeTypeBeforeEncryption: String?, + val hashBeforeEncryption: String?, + ) : OrchestratorResult() + + class ServerResult( + val fileHeader: FileHeader, + val url: String, + val magnet: String?, + val uploadedHash: String?, + val mimeTypeBeforeEncryption: String?, + val hashBeforeEncryption: String?, + ) : OrchestratorResult() + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/UploadingState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/UploadingState.kt new file mode 100644 index 0000000000..0c861368ae --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/UploadingState.kt @@ -0,0 +1,46 @@ +/* + * 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.service.uploads + +sealed class UploadingState { + data object Ready : UploadingState() + + data object Compressing : UploadingState() + + data object Uploading : UploadingState() + + data object ServerProcessing : UploadingState() + + data object Downloading : UploadingState() + + data object Hashing : UploadingState() + + sealed class UploadingFinalState : UploadingState() + + class Finished( + val result: UploadOrchestrator.OrchestratorResult, + ) : UploadingFinalState() + + class Error( + val error: UploadError, + val params: Array, + ) : UploadingFinalState() +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt index 1ef92d7047..8f497e176e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.commons.viewmodels import com.vitorpamplona.amethyst.commons.service.lnurl.LnurlHttpTransport import com.vitorpamplona.amethyst.commons.service.pow.PoWJobFailure +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader import com.vitorpamplona.amethyst.commons.tor.MoneyOpRelayRouting import com.vitorpamplona.amethyst.commons.tor.TorRelayEvaluation import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -66,6 +67,9 @@ interface AccountViewModelHost { /** NIP-42 authentication state per relay, across every logged-in account. */ val relayAuthState: StateFlow> + /** Compresses, strips, encrypts and uploads picked media for the composers. */ + val mediaUploader: MediaUploader + /** Public keys of every account saved on this device. */ val savedAccounts: Flow> diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/MediaUri.ios.kt b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.ios.kt similarity index 95% rename from commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/MediaUri.ios.kt rename to commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.ios.kt index f9a5d2cf33..eebaa3945a 100644 --- a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/MediaUri.ios.kt +++ b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.ios.kt @@ -18,7 +18,7 @@ * 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.actions.uploads +package com.vitorpamplona.amethyst.commons.service.uploads actual abstract class MediaUri { actual abstract override fun toString(): String diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/MediaUri.jvm.kt b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.jvm.kt similarity index 95% rename from commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/MediaUri.jvm.kt rename to commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.jvm.kt index f9a5d2cf33..eebaa3945a 100644 --- a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/MediaUri.jvm.kt +++ b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.jvm.kt @@ -18,7 +18,7 @@ * 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.actions.uploads +package com.vitorpamplona.amethyst.commons.service.uploads actual abstract class MediaUri { actual abstract override fun toString(): String diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/GallerySelect.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/GallerySelect.android.kt index 44aa42fa16..443a941c23 100644 --- a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/GallerySelect.android.kt +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/GallerySelect.android.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import java.util.concurrent.atomic.AtomicBoolean diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.android.kt index b7aa2f604a..20af3c3130 100644 --- a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.android.kt +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.android.kt @@ -28,7 +28,7 @@ import androidx.compose.foundation.content.consume import androidx.compose.foundation.content.contentReceiver import androidx.compose.foundation.content.hasMediaType import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.MediaUri +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUri @OptIn(ExperimentalFoundationApi::class) actual fun Modifier.imageContentReceiver(onImage: (MediaUri, String?) -> Unit): Modifier = diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SelectFromGallery.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SelectFromGallery.kt index 922bbc3537..9b96226c67 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SelectFromGallery.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SelectFromGallery.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.commons.ui.actions.uploads import androidx.compose.foundation.layout.height import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -35,53 +34,11 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.upload_image -import com.vitorpamplona.amethyst.commons.service.uploads.isAvif +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation import com.vitorpamplona.amethyst.commons.ui.stringRes import kotlinx.collections.immutable.ImmutableList -@Stable -class SelectedMedia( - val uri: MediaUri, - val mimeType: String?, -) { - fun isImage() = mimeType?.startsWith("image") - - fun isGif() = mimeType?.equals("image/gif", ignoreCase = true) == true - - fun isVideo() = mimeType?.startsWith("video") - - fun isAudio() = mimeType?.startsWith("audio") - - fun isNotMedia() = - mimeType?.let { - !(it.startsWith("image") || it.startsWith("video") || it.startsWith("audio")) - } ?: true - - fun isDocument() = mimeType == "application/pdf" - - /** - * Returns true if [MediaCompressor.compress] would actually compress this file when a - * non-UNCOMPRESSED quality is selected. AVIF, GIF, SVG, and unknown MIME types pass - * through MediaCompressor unchanged — the compression-quality slider has no effect on - * them, so the UI hides the slider when no selected files are compressible. - * - * Keep this in sync with the branching in MediaCompressor.compress() — if either side - * drifts, the UI will lie to the user. - */ - fun isCompressible(): Boolean { - val mt = mimeType?.lowercase() ?: return false - return when { - mt.startsWith("video") -> true - mt.startsWith("image") -> - !mt.contains("gif") && - !mt.contains("svg") && - !isAvif(mt) - else -> false - } - } -} - @Composable fun SelectFromGallery( isUploading: Boolean, diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.kt index d603ebb1b1..eeabee2c7a 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.commons.ui.components import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.MediaUri +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUri /** * Accepts images pasted or dragged into the field (keyboard GIFs, stickers, clipboard images), diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/uploads/UploadErrorText.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/uploads/UploadErrorText.kt new file mode 100644 index 0000000000..6ffa633dda --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/uploads/UploadErrorText.kt @@ -0,0 +1,52 @@ +/* + * 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.resources.Res +import com.vitorpamplona.amethyst.commons.resources.avif_metadata_strip_failed +import com.vitorpamplona.amethyst.commons.resources.blossom_payment_required +import com.vitorpamplona.amethyst.commons.resources.could_not_check_downloaded_file +import com.vitorpamplona.amethyst.commons.resources.could_not_download_from_the_server +import com.vitorpamplona.amethyst.commons.resources.could_not_open_the_compressed_file +import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media +import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_upload +import com.vitorpamplona.amethyst.commons.resources.media_too_big_for_nip95 +import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading +import com.vitorpamplona.amethyst.commons.resources.upload_cancelled +import com.vitorpamplona.amethyst.commons.service.uploads.UploadError +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState +import org.jetbrains.compose.resources.StringResource + +/** The message template for this upload failure; fill it with [UploadingState.Error.params]. */ +val UploadingState.Error.errorResource: StringResource + get() = + when (error) { + UploadError.MEDIA_TOO_BIG_FOR_NIP95 -> Res.string.media_too_big_for_nip95 + UploadError.COULD_NOT_CHECK_DOWNLOADED_FILE -> Res.string.could_not_check_downloaded_file + UploadError.COULD_NOT_OPEN_COMPRESSED_FILE -> Res.string.could_not_open_the_compressed_file + UploadError.LOGIN_WITH_PRIVATE_KEY -> Res.string.login_with_a_private_key_to_be_able_to_upload + UploadError.FAILED_TO_UPLOAD_MEDIA -> Res.string.failed_to_upload_media + UploadError.BLOSSOM_PAYMENT_REQUIRED -> Res.string.blossom_payment_required + UploadError.SERVER_DID_NOT_PROVIDE_URL -> Res.string.server_did_not_provide_a_url_after_uploading + UploadError.COULD_NOT_DOWNLOAD_FROM_SERVER -> Res.string.could_not_download_from_the_server + UploadError.AVIF_METADATA_STRIP_FAILED -> Res.string.avif_metadata_strip_failed + UploadError.UPLOAD_CANCELLED -> Res.string.upload_cancelled + } diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/GallerySelect.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/GallerySelect.ios.kt index 0b211f11fa..8c661fd52a 100644 --- a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/GallerySelect.ios.kt +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/GallerySelect.ios.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.commons.ui.actions.uploads import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.ios.kt index 6c08448555..43da45fe05 100644 --- a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.ios.kt +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.ios.kt @@ -21,6 +21,6 @@ package com.vitorpamplona.amethyst.commons.ui.components import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.MediaUri +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUri actual fun Modifier.imageContentReceiver(onImage: (MediaUri, String?) -> Unit): Modifier = this diff --git a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelPreviews.jvmAndroid.kt b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelPreviews.jvmAndroid.kt index a7b91243e5..4ac1c41d37 100644 --- a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelPreviews.jvmAndroid.kt +++ b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelPreviews.jvmAndroid.kt @@ -37,6 +37,8 @@ import com.vitorpamplona.amethyst.commons.service.http.EmptyRoleBasedHttpClientB import com.vitorpamplona.amethyst.commons.service.http.EncryptionKeyCache import com.vitorpamplona.amethyst.commons.service.lnurl.LnurlHttpTransport import com.vitorpamplona.amethyst.commons.service.pow.PoWJobFailure +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.UnavailableMediaUploader import com.vitorpamplona.amethyst.commons.state.UiSettingsState import com.vitorpamplona.amethyst.commons.tor.MoneyOpRelayRouting import com.vitorpamplona.amethyst.commons.tor.TorRelayEvaluation @@ -198,6 +200,7 @@ private object PreviewAccountViewModelHost : AccountViewModelHost { override val relayStats = RelayStats(EmptyNostrClient()) override val torRelayEvaluation: StateFlow = MutableStateFlow(TorRelayEvaluation(TorRelaySettings())) override val relayAuthState: StateFlow> = MutableStateFlow(persistentMapOf()) + override val mediaUploader: MediaUploader = UnavailableMediaUploader override val websocketBuilder = BasicOkHttpWebSocket.Builder { OkHttpClient() } override val lnurlTransport = LnurlHttpTransport { throw IllegalStateException("Previews do not reach the network") } override val moneyOpRelays = MoneyOpRelayRouting.None diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/GallerySelect.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/GallerySelect.jvm.kt index 0b211f11fa..8c661fd52a 100644 --- a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/GallerySelect.jvm.kt +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/GallerySelect.jvm.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.commons.ui.actions.uploads import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.jvm.kt index 6c08448555..43da45fe05 100644 --- a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.jvm.kt +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.jvm.kt @@ -21,6 +21,6 @@ package com.vitorpamplona.amethyst.commons.ui.components import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.commons.ui.actions.uploads.MediaUri +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUri actual fun Modifier.imageContentReceiver(onImage: (MediaUri, String?) -> Unit): Modifier = this