- Adds i18n for error messages when uploading images.

- Forces nip95 to be under 80Kb
- Fixes bug that wouldn't show an error when uploading images in the reels page.
- Only saves default server if not NIP95
This commit is contained in:
Vitor Pamplona
2023-12-17 17:26:17 -05:00
parent 238ade9ccb
commit 9026c877ac
6 changed files with 71 additions and 50 deletions
@@ -28,7 +28,7 @@ class FileHeader(
mimeType: String?, mimeType: String?,
dimPrecomputed: String?, dimPrecomputed: String?,
onReady: (FileHeader) -> Unit, onReady: (FileHeader) -> Unit,
onError: () -> Unit onError: (String?) -> Unit
) { ) {
try { try {
val imageData: ByteArray? = ImageDownloader().waitAndGetImage(fileUrl) val imageData: ByteArray? = ImageDownloader().waitAndGetImage(fileUrl)
@@ -36,11 +36,11 @@ class FileHeader(
if (imageData != null) { if (imageData != null) {
prepare(imageData, mimeType, dimPrecomputed, onReady, onError) prepare(imageData, mimeType, dimPrecomputed, onReady, onError)
} else { } else {
onError() onError(null)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ImageDownload", "Couldn't download image from server: ${e.message}") Log.e("ImageDownload", "Couldn't download image from server: ${e.message}")
onError() onError(e.message)
} }
} }
@@ -49,7 +49,7 @@ class FileHeader(
mimeType: String?, mimeType: String?,
dimPrecomputed: String?, dimPrecomputed: String?,
onReady: (FileHeader) -> Unit, onReady: (FileHeader) -> Unit,
onError: () -> Unit onError: (String?) -> Unit
) { ) {
try { try {
val hash = CryptoUtils.sha256(data).toHexKey() val hash = CryptoUtils.sha256(data).toHexKey()
@@ -123,7 +123,7 @@ class FileHeader(
onReady(FileHeader(mimeType, hash, size, dim, blurHash)) onReady(FileHeader(mimeType, hash, size, dim, blurHash))
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ImageDownload", "Couldn't convert image in to File Header: ${e.message}") Log.e("ImageDownload", "Couldn't convert image in to File Header: ${e.message}")
onError() onError(e.message)
} }
} }
} }
@@ -9,6 +9,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.FileHeader import com.vitorpamplona.amethyst.service.FileHeader
import com.vitorpamplona.amethyst.service.Nip96MediaServers import com.vitorpamplona.amethyst.service.Nip96MediaServers
@@ -16,8 +17,6 @@ import com.vitorpamplona.amethyst.service.Nip96Uploader
import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.MediaCompressor import com.vitorpamplona.amethyst.ui.components.MediaCompressor
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
data class ServerOption( data class ServerOption(
@@ -30,7 +29,6 @@ open class NewMediaModel : ViewModel() {
var account: Account? = null var account: Account? = null
var isUploadingImage by mutableStateOf(false) var isUploadingImage by mutableStateOf(false)
val imageUploadingError = MutableSharedFlow<String?>(0, 3, onBufferOverflow = BufferOverflow.DROP_OLDEST)
var mediaType by mutableStateOf<String?>(null) var mediaType by mutableStateOf<String?>(null)
var selectedServer by mutableStateOf<ServerOption?>(null) var selectedServer by mutableStateOf<ServerOption?>(null)
@@ -44,12 +42,14 @@ open class NewMediaModel : ViewModel() {
var uploadingDescription = mutableStateOf<String?>(null) var uploadingDescription = mutableStateOf<String?>(null)
var onceUploaded: () -> Unit = {} var onceUploaded: () -> Unit = {}
var onError: (String) -> Unit = {}
open fun load(account: Account, uri: Uri, contentType: String?) { open fun load(account: Account, uri: Uri, contentType: String?, onError: (String) -> Unit) {
this.account = account this.account = account
this.galleryUri = uri this.galleryUri = uri
this.mediaType = contentType this.mediaType = contentType
this.selectedServer = ServerOption(defaultServer(), false) this.selectedServer = ServerOption(defaultServer(), false)
this.onError = onError
} }
fun upload(context: Context, relayList: List<Relay>? = null) { fun upload(context: Context, relayList: List<Relay>? = null) {
@@ -73,11 +73,11 @@ open class NewMediaModel : ViewModel() {
uploadingPercentage.value = 0.2f uploadingPercentage.value = 0.2f
uploadingDescription.value = "Loading" uploadingDescription.value = "Loading"
contentResolver.openInputStream(fileUri)?.use { contentResolver.openInputStream(fileUri)?.use {
createNIP95Record(it.readBytes(), contentType, alt, sensitiveContent, relayList = relayList) createNIP95Record(it.readBytes(), contentType, alt, sensitiveContent, relayList = relayList, context)
} }
?: run { ?: run {
viewModelScope.launch { viewModelScope.launch {
imageUploadingError.emit("Failed to upload the image / video") onError(context.getString(R.string.could_not_open_the_compressed_file))
isUploadingImage = false isUploadingImage = false
uploadingPercentage.value = 0.00f uploadingPercentage.value = 0.00f
uploadingDescription.value = null uploadingDescription.value = null
@@ -106,15 +106,14 @@ open class NewMediaModel : ViewModel() {
localContentType = contentType, localContentType = contentType,
alt = alt, alt = alt,
sensitiveContent = sensitiveContent, sensitiveContent = sensitiveContent,
relayList = relayList relayList = relayList,
context
) )
} catch (e: Exception) { } catch (e: Exception) {
isUploadingImage = false isUploadingImage = false
uploadingPercentage.value = 0.00f uploadingPercentage.value = 0.00f
uploadingDescription.value = null uploadingDescription.value = null
viewModelScope.launch { onError(context.getString(R.string.failed_to_upload_media, e.message))
imageUploadingError.emit("Failed to upload: ${e.message}")
}
} }
} }
} }
@@ -123,9 +122,7 @@ open class NewMediaModel : ViewModel() {
isUploadingImage = false isUploadingImage = false
uploadingPercentage.value = 0.00f uploadingPercentage.value = 0.00f
uploadingDescription.value = null uploadingDescription.value = null
viewModelScope.launch { onError(context.getString(R.string.error_when_compressing_media, it))
imageUploadingError.emit("Failed to upload the image / video")
}
} }
) )
} }
@@ -151,7 +148,8 @@ open class NewMediaModel : ViewModel() {
localContentType: String?, localContentType: String?,
alt: String, alt: String,
sensitiveContent: Boolean, sensitiveContent: Boolean,
relayList: List<Relay>? = null relayList: List<Relay>? = null,
context: Context
) { ) {
uploadingPercentage.value = 0.40f uploadingPercentage.value = 0.40f
uploadingDescription.value = "Server Processing" uploadingDescription.value = "Server Processing"
@@ -169,9 +167,7 @@ open class NewMediaModel : ViewModel() {
uploadingPercentage.value = 0.00f uploadingPercentage.value = 0.00f
uploadingDescription.value = null uploadingDescription.value = null
isUploadingImage = false isUploadingImage = false
viewModelScope.launch { onError(context.getString(R.string.server_did_not_provide_a_url_after_uploading))
imageUploadingError.emit("Failed to upload the image / video")
}
return return
} }
@@ -203,9 +199,7 @@ open class NewMediaModel : ViewModel() {
uploadingPercentage.value = 0.00f uploadingPercentage.value = 0.00f
uploadingDescription.value = null uploadingDescription.value = null
isUploadingImage = false isUploadingImage = false
viewModelScope.launch { onError(context.getString(R.string.could_not_prepare_local_file_to_upload, it))
imageUploadingError.emit("Failed to upload the image / video")
}
} }
) )
} else { } else {
@@ -214,13 +208,28 @@ open class NewMediaModel : ViewModel() {
uploadingPercentage.value = 0.00f uploadingPercentage.value = 0.00f
uploadingDescription.value = null uploadingDescription.value = null
isUploadingImage = false isUploadingImage = false
viewModelScope.launch { onError(context.getString(R.string.could_not_download_from_the_server))
imageUploadingError.emit("Failed to upload the image / video")
}
} }
} }
fun createNIP95Record(bytes: ByteArray, mimeType: String?, alt: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) { fun createNIP95Record(
bytes: ByteArray,
mimeType: String?,
alt: String,
sensitiveContent: Boolean,
relayList: List<Relay>? = null,
context: Context
) {
if (bytes.size > 80000) {
viewModelScope.launch {
onError("Media is too big for NIP-95")
isUploadingImage = false
uploadingPercentage.value = 0.00f
uploadingDescription.value = null
}
return
}
uploadingPercentage.value = 0.30f uploadingPercentage.value = 0.30f
uploadingDescription.value = "Hashing" uploadingDescription.value = "Hashing"
@@ -248,9 +257,7 @@ open class NewMediaModel : ViewModel() {
uploadingPercentage.value = 0.00f uploadingPercentage.value = 0.00f
isUploadingImage = false isUploadingImage = false
cancel() cancel()
viewModelScope.launch { onError(context.getString(R.string.could_not_prepare_local_file_to_upload, it))
imageUploadingError.emit("Failed to upload the image / video")
}
} }
) )
} }
@@ -5,7 +5,6 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import android.util.Size import android.util.Size
import android.widget.Toast
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -55,7 +54,6 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Composable @Composable
fun NewMediaView(uri: Uri, onClose: () -> Unit, postViewModel: NewMediaModel, accountViewModel: AccountViewModel, nav: (String) -> Unit) { fun NewMediaView(uri: Uri, onClose: () -> Unit, postViewModel: NewMediaModel, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
@@ -67,14 +65,8 @@ fun NewMediaView(uri: Uri, onClose: () -> Unit, postViewModel: NewMediaModel, ac
LaunchedEffect(uri) { LaunchedEffect(uri) {
val mediaType = resolver.getType(uri) ?: "" val mediaType = resolver.getType(uri) ?: ""
postViewModel.load(account, uri, mediaType) postViewModel.load(account, uri, mediaType) {
accountViewModel.toast(context.getString(R.string.failed_to_upload_media_no_details), it)
launch(Dispatchers.IO) {
postViewModel.imageUploadingError.collect { error ->
withContext(Dispatchers.Main) {
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
}
}
} }
} }
@@ -146,7 +138,11 @@ fun NewMediaView(uri: Uri, onClose: () -> Unit, postViewModel: NewMediaModel, ac
onPost = { onPost = {
onClose() onClose()
postViewModel.upload(context, relayList) postViewModel.upload(context, relayList)
postViewModel.selectedServer?.let { account.changeDefaultFileServer(it.server) } postViewModel.selectedServer?.let {
if (!it.isNip95) {
account.changeDefaultFileServer(it.server)
}
}
}, },
isActive = postViewModel.canPost() isActive = postViewModel.canPost()
) )
@@ -211,7 +207,6 @@ fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewMo
try { try {
bitmap = resolver.loadThumbnail(it, Size(1200, 1000), null) bitmap = resolver.loadThumbnail(it, Size(1200, 1000), null)
} catch (e: Exception) { } catch (e: Exception) {
postViewModel.imageUploadingError.emit("Unable to load thumbnail, but the video can be uploaded")
Log.w("NewPostView", "Couldn't create thumbnail, but the video can be uploaded", e) Log.w("NewPostView", "Couldn't create thumbnail, but the video can be uploaded", e)
} }
} }
@@ -405,8 +405,10 @@ fun NewPostView(
url, url,
accountViewModel.account.defaultFileServer, accountViewModel.account.defaultFileServer,
onAdd = { alt, server, sensitiveContent -> onAdd = { alt, server, sensitiveContent ->
postViewModel.upload(url, alt, sensitiveContent, false, server, context, relayList) postViewModel.upload(url, alt, sensitiveContent, false, server, context)
if (!server.isNip95) {
accountViewModel.account.changeDefaultFileServer(server.server) accountViewModel.account.changeDefaultFileServer(server.server)
}
}, },
onCancel = { onCancel = {
postViewModel.contentToAddUrl = null postViewModel.contentToAddUrl = null
@@ -44,6 +44,7 @@ import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.ZapSplitSetup import com.vitorpamplona.quartz.events.ZapSplitSetup
import com.vitorpamplona.quartz.events.findURLs import com.vitorpamplona.quartz.events.findURLs
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@@ -206,7 +207,7 @@ open class NewPostViewModel() : ViewModel() {
val dmUsers = toUsersTagger.mentions val dmUsers = toUsersTagger.mentions
val zapReceiver = if (wantsForwardZapTo) { val zapReceiver = if (wantsForwardZapTo) {
forwardZapTo?.items?.map { forwardZapTo.items.map {
ZapSplitSetup( ZapSplitSetup(
lnAddressOrPubKeyHex = it.key.pubkeyHex, lnAddressOrPubKeyHex = it.key.pubkeyHex,
relay = it.key.relaysBeingUsed.keys.firstOrNull(), relay = it.key.relaysBeingUsed.keys.firstOrNull(),
@@ -361,8 +362,7 @@ open class NewPostViewModel() : ViewModel() {
sensitiveContent: Boolean, sensitiveContent: Boolean,
isPrivate: Boolean = false, isPrivate: Boolean = false,
server: ServerOption, server: ServerOption,
context: Context, context: Context
relayList: List<Relay>? = null
) { ) {
isUploadingImage = true isUploadingImage = true
contentToAddUrl = null contentToAddUrl = null
@@ -569,7 +569,7 @@ open class NewPostViewModel() : ViewModel() {
TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length) TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length)
) )
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) { } else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
forwardZapTo?.addItem(item) forwardZapTo.addItem(item)
forwardZapToEditting = TextFieldValue("") forwardZapToEditting = TextFieldValue("")
/* /*
val lastWord = forwardZapToEditting.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ") val lastWord = forwardZapToEditting.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ")
@@ -740,6 +740,14 @@ open class NewPostViewModel() : ViewModel() {
alt: String?, alt: String?,
sensitiveContent: Boolean sensitiveContent: Boolean
) { ) {
if (bytes.size > 80000) {
viewModelScope.launch {
imageUploadingError.emit("Media is too big for NIP-95")
isUploadingImage = false
}
return
}
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
FileHeader.prepare( FileHeader.prepare(
bytes, bytes,
@@ -773,6 +781,7 @@ open class NewPostViewModel() : ViewModel() {
contentToAddUrl = uri contentToAddUrl = uri
} }
@OptIn(ExperimentalCoroutinesApi::class)
fun startLocation(context: Context) { fun startLocation(context: Context) {
locUtil = LocationUtil(context) locUtil = LocationUtil(context)
locUtil?.let { locUtil?.let {
+8
View File
@@ -684,4 +684,12 @@
<string name="classifieds_category_food">Food</string> <string name="classifieds_category_food">Food</string>
<string name="classifieds_category_misc">Miscellaneous</string> <string name="classifieds_category_misc">Miscellaneous</string>
<string name="classifieds_category_other">Other</string> <string name="classifieds_category_other">Other</string>
<string name="failed_to_upload_media_no_details">Failed to upload media</string>
<string name="could_not_open_the_compressed_file">Could not open the compressed file</string>
<string name="error_when_compressing_media">Error when compressing media: %1$s</string>
<string name="failed_to_upload_media">Uploading error: %1$s</string>
<string name="server_did_not_provide_a_url_after_uploading">Server did not provide a URL after uploading</string>
<string name="could_not_download_from_the_server">Could not download uploaded media from the server</string>
<string name="could_not_prepare_local_file_to_upload">Could not prepare local file to upload: %1$s</string>
</resources> </resources>