mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-12 09:13:23 +00:00
feat(blossom): blob-manager per-file controls + clear 402 on upload
- Blob manager rows now show an image thumbnail (image/* blobs) and Copy-link / Open controls, alongside the existing mirror-to-missing / delete / report. - BUD-07: BlossomUploader now raises a typed BlossomPaymentException on a 402, and UploadOrchestrator surfaces a specific "server requires payment" message instead of a generic upload failure (auto-settlement via the NIP-60 wallet is still TBD). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ckbnz1N94W1hnNC9xpsCNP
This commit is contained in:
+4
@@ -25,6 +25,7 @@ import android.net.Uri
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.service.upload.BlossomClient
|
||||
import com.vitorpamplona.amethyst.commons.service.upload.BlossomPaymentException
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.uploads.UploadingState.UploadingFinalState
|
||||
import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader
|
||||
@@ -246,6 +247,9 @@ class UploadOrchestrator {
|
||||
finalState
|
||||
} catch (_: SignerExceptions.ReadOnlyException) {
|
||||
error(R.string.login_with_a_private_key_to_be_able_to_upload)
|
||||
} catch (e: BlossomPaymentException) {
|
||||
// BUD-07: the server wants payment before it will store the blob.
|
||||
error(R.string.blossom_payment_required, e.payment.reason ?: serverBaseUrl)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
error(R.string.failed_to_upload_media, e.message ?: e.javaClass.simpleName)
|
||||
|
||||
+6
@@ -26,6 +26,7 @@ import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import android.webkit.MimeTypeMap
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.service.upload.BlossomPaymentException
|
||||
import com.vitorpamplona.amethyst.service.HttpStatusMessages
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult
|
||||
@@ -36,6 +37,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.JsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomPaymentRequired
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomServerUrl
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomUploadResult
|
||||
import com.vitorpamplona.quartz.utils.RandomInstance
|
||||
@@ -204,6 +206,10 @@ class BlossomUploader {
|
||||
response.body.use { body ->
|
||||
convertToMediaResult(parseResults(body.string()))
|
||||
}
|
||||
} else if (response.code == 402) {
|
||||
// BUD-07: paid server. Surface the payment challenge so the caller can
|
||||
// tell the user (auto-settlement via the NIP-60 wallet is a follow-up).
|
||||
throw BlossomPaymentException(serverBaseUrl, BlossomPaymentRequired.fromHeaders { response.headers[it] })
|
||||
} else {
|
||||
val errorMessage = response.headers.get(BlossomServerUrl.REASON_HEADER)
|
||||
|
||||
|
||||
+50
-11
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.actions.mediaServers
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
@@ -31,6 +32,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.AssistChip
|
||||
import androidx.compose.material3.AssistChipDefaults
|
||||
@@ -54,10 +56,17 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
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.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
@@ -152,23 +161,40 @@ private fun BlobCard(
|
||||
) {
|
||||
var deleteMenuOpen by remember { mutableStateOf(false) }
|
||||
var reportOpen by remember { mutableStateOf(false) }
|
||||
val clipboard = LocalClipboardManager.current
|
||||
val context = LocalContext.current
|
||||
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
Text(
|
||||
text = row.hash.take(16) + "…",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
Text(
|
||||
text = listOfNotNull(row.type, row.size?.let { humanBytes(it) }).joinToString(" · "),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
if (row.url != null && row.type?.startsWith("image/") == true) {
|
||||
AsyncImage(
|
||||
model = row.url,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.size(44.dp).clip(RoundedCornerShape(8.dp)),
|
||||
)
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = row.hash.take(16) + "…",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
Text(
|
||||
text = listOfNotNull(row.type, row.size?.let { humanBytes(it) }).joinToString(" · "),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
row.serversPresent.forEach { server ->
|
||||
@@ -228,6 +254,19 @@ private fun BlobCard(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (row.url != null) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
TextButton(onClick = { clipboard.setText(AnnotatedString(row.url)) }) {
|
||||
Text(stringRes(R.string.copy), style = MaterialTheme.typography.labelMedium)
|
||||
}
|
||||
TextButton(onClick = {
|
||||
runCatching { context.startActivity(Intent(Intent.ACTION_VIEW, row.url.toUri())) }
|
||||
}) {
|
||||
Text(stringRes(R.string.blossom_open), style = MaterialTheme.typography.labelMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1555,6 +1555,8 @@
|
||||
<string name="blossom_mirror_to_missing">Mirror to missing</string>
|
||||
<string name="blossom_delete_from">Delete from…</string>
|
||||
<string name="blossom_report">Report</string>
|
||||
<string name="blossom_open">Open</string>
|
||||
<string name="blossom_payment_required">This server requires payment to upload: %1$s</string>
|
||||
<string name="blossom_report_title">Report blob</string>
|
||||
<string name="blossom_report_comment_hint">Reason (optional)</string>
|
||||
<string name="blossom_send">Send</string>
|
||||
|
||||
Reference in New Issue
Block a user