mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 03:38:23 +00:00
refactor: MediaUploader port for the upload pipeline
Move the platform-neutral upload model into commons (commons.service.uploads): MediaUri, SelectedMedia, CompressorQuality, MediaCompressorResult, UploadError, UploadingState, UploadOrchestrator, MultiOrchestrator and the MediaUploader port. Error states carry an UploadError enum; commonsUI maps it to a string resource. The Android compress/strip/upload pipeline stays in the app as AndroidMediaUploader, reached through AccountViewModelHost.mediaUploader. Composer view models and screens pass the uploader instead of a Context. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P
This commit is contained in:
+1
@@ -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
|
||||
|
||||
+592
@@ -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<BlossomAuthorizationEvent>? = 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-26
@@ -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,
|
||||
}
|
||||
|
||||
-602
@@ -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<out String>,
|
||||
) : UploadingFinalState()
|
||||
}
|
||||
|
||||
class UploadOrchestrator {
|
||||
val progress = MutableStateFlow(0.0)
|
||||
val progressState = MutableStateFlow<UploadingState>(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<BlossomAuthorizationEvent>? = 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+6
-4
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+3
-2
@@ -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(
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
|
||||
+26
-24
@@ -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
|
||||
}
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+5
-4
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
+3
-2
@@ -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?,
|
||||
|
||||
+4
@@ -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<PersistentMap<NormalizedRelayUrl, RelayAuthSnapshot>>
|
||||
get() = modules.authCoordinator.receiver.authStateFlow
|
||||
|
||||
override val mediaUploader: MediaUploader by lazy { AndroidMediaUploader(modules.appContext) }
|
||||
|
||||
override val savedAccounts: Flow<Set<HexKey>> =
|
||||
LocalPreferences
|
||||
.accountsFlow()
|
||||
|
||||
+1
-3
@@ -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,
|
||||
)
|
||||
|
||||
+11
-10
@@ -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,
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+6
-6
@@ -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
|
||||
|
||||
+20
-7
@@ -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" }
|
||||
}
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-2
@@ -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(
|
||||
|
||||
+2
-1
@@ -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()
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+10
-7
@@ -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<Mip04UploadResult>()
|
||||
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
|
||||
|
||||
+10
-10
@@ -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")
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+2
-2
@@ -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()
|
||||
|
||||
+13
-13
@@ -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<String?>(null)
|
||||
var pendingRetryMode by mutableStateOf<RetryMode?>(null)
|
||||
var pendingRetryOnError by mutableStateOf<((String, String) -> Unit)?>(null)
|
||||
var pendingRetryContext by mutableStateOf<Context?>(null)
|
||||
var pendingRetryUploader by mutableStateOf<MediaUploader?>(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)
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
+1
-3
@@ -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,
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-3
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
+23
-19
@@ -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<SuccessfulUploads>) -> 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<SuccessfulUploads>) -> 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<SuccessfulUploads>) -> 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
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+2
-2
@@ -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()) {
|
||||
|
||||
+7
-4
@@ -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 =
|
||||
|
||||
+2
-2
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+1
-3
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
+16
-14
@@ -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
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
+13
-12
@@ -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,
|
||||
|
||||
+2
-4
@@ -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,
|
||||
)
|
||||
|
||||
+3
-5
@@ -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,
|
||||
|
||||
+51
-66
@@ -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 = ""
|
||||
val markdownImage = ""
|
||||
markdownImage
|
||||
} else {
|
||||
null
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+23
-21
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+1
-3
@@ -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(
|
||||
|
||||
+6
-6
@@ -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
|
||||
|
||||
+1
-3
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-6
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+28
-26
@@ -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,
|
||||
)
|
||||
|
||||
+4
-4
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+13
-13
@@ -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<UploadingState.Error>,
|
||||
context: Context,
|
||||
): String =
|
||||
private suspend fun formatUploadErrors(errors: List<UploadingState.Error>): String =
|
||||
errors
|
||||
.map { loadStringRes(it.errorResource, *it.params) }
|
||||
.distinct()
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+14
-13
@@ -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<com.vitorpamplona.amethyst.service.uploads.UploadingState.Finished>): String? =
|
||||
private fun firstUploadedUrl(successful: List<UploadingState.Finished>): String? =
|
||||
successful
|
||||
.firstNotNullOfOrNull { it.result as? UploadOrchestrator.OrchestratorResult.ServerResult }
|
||||
?.url
|
||||
|
||||
private suspend fun formatUploadErrors(
|
||||
errors: List<com.vitorpamplona.amethyst.service.uploads.UploadingState.Error>,
|
||||
context: Context,
|
||||
): String =
|
||||
private suspend fun formatUploadErrors(errors: List<UploadingState.Error>): String =
|
||||
errors
|
||||
.map { loadStringRes(it.errorResource, *it.params) }
|
||||
.distinct()
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-3
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
+16
-14
@@ -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
|
||||
}
|
||||
|
||||
+2
-4
@@ -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,
|
||||
|
||||
+26
-24
@@ -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
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+9
-6
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+9
-6
@@ -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<String?, String?> =
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+5
-5
@@ -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 {
|
||||
|
||||
+10
-11
@@ -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<UploadingState.Error>,
|
||||
context: Context,
|
||||
): String =
|
||||
private suspend fun formatUploadErrors(errors: List<UploadingState.Error>): String =
|
||||
errors
|
||||
.map { loadStringRes(it.errorResource, *it.params) }
|
||||
.distinct()
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+43
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -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?,
|
||||
)
|
||||
+124
@@ -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)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user